Python-特征工程秘籍第三版-三-

67 阅读1小时+

Python 特征工程秘籍第三版(三)

原文:annas-archive.org/md5/86f7f4009ce12f06cb3f3594f063591b

译者:飞龙

协议:CC BY-NC-SA 4.0

第六章:从日期和时间变量中提取特征

日期和时间变量包含有关日期、时间或两者的信息,在编程中,我们将其统称为 datetime 特征。出生日期、事件发生的时间以及最后付款的日期和时间都是 datetime 变量的例子。

由于其本质,datetime 特征通常具有高基数。这意味着它们包含大量唯一的值,每个值对应于特定的日期和/或时间组合。我们通常不会以原始格式使用 datetime 变量进行机器学习模型。相反,我们通过从这些变量中提取多个特征来丰富数据集。这些新特征通常具有较低的基数,并允许我们捕捉到有意义的 信息,如趋势、季节性和重要事件和趋势。

在本章中,我们将探讨如何通过利用 pandasdt 模块来从日期和时间中提取特征,然后使用 feature-engine 自动化此过程。

本章将涵盖以下食谱:

  • 使用 pandas 从日期中提取特征

  • 使用 pandas 从时间中提取特征

  • 捕获 datetime 变量之间的经过时间

  • 在不同时区使用时间

  • 使用 feature-engine 自动化 datetime 特征提取

技术要求

在本章中,我们将使用 pandasnumpyfeature-engine Python 库。

使用 pandas 从日期中提取特征

datetime 变量的值可以是日期、时间或两者兼有。我们将首先关注包含日期的变量。我们很少使用原始日期与机器学习算法结合。相反,我们提取更简单的特征,如年份、月份或星期几,这些特征使我们能够捕捉到季节性、周期性和趋势等洞察。

pandas Python 库非常适合处理日期和时间。通过使用 pandasdt 模块,我们可以访问 pandas Series 的 datetime 属性以提取许多特征。然而,为了利用此功能,变量需要转换为支持这些操作的数据类型,例如 datetimetimedelta

注意

当我们从 CSV 文件加载数据时,datetime 变量可以转换为对象。为了提取本章中将要讨论的日期和时间特征,有必要将变量重新转换为 datetime

在这个食谱中,我们将学习如何通过使用 pandas 来提取日期特征。

准备工作

以下是我们可以使用 pandas 直接从 datetime 变量的 date 部分提取的一些特征:

  • pandas.Series.dt.year

  • pandas.Series.dt.quarter

  • pandas.Series.dt.month

  • pandas.Series.dt.isocalendar().week

  • pandas.Series.dt.day

  • pandas.Series.dt.day_of_week

  • pandas.Series.dt.weekday

  • pandas.Series.dt.dayofyear

  • pandas.Series.dt.day_of_year

我们可以使用 pandas 获得的特征来创建更多的特征,例如学期或是否是周末。我们将在下一节中学习如何做到这一点。

如何做到这一点...

为了继续这个食谱,让我们导入 pandasnumpy,并创建一个样本 DataFrame:

  1. 让我们导入库:

    import numpy as np
    import pandas as pd
    
  2. 我们将首先创建从 2024-05-17 中午开始,然后每天增加 1 天的 20 个 datetime 值。然后,我们将这些值捕获在一个 DataFrame 实例中,并显示前五行:

    rng_ = pd.date_range(
        "2024-05-17", periods=20, freq="D")
    data = pd.DataFrame({"date": rng_})
    data.head()
    

    在下面的输出中,我们可以看到我们在 步骤 2 中创建的包含日期的变量:

图 6.1 – 仅包含日期的 datetime 变量的 DataFrame 的顶部行

图 6.1 – 仅包含日期的 datetime 变量的 DataFrame 的顶部行

注意

我们可以通过执行 data["date"].dtypes 来检查变量的数据格式。如果变量被转换为对象类型,我们可以通过执行 data["date_dt"] = pd.to_datetime(data["date"]) 来将其转换为 datetime 格式。

  1. 让我们将日期的年份部分提取到一个新列中,并显示结果 DataFrame 的前五行:

    data["year"] = data["date"].dt.year
    data.head()
    

    在下面的输出中,我们可以看到新的 year 变量:

图 6.2 – 从日期中提取的年份变量的 DataFrame 的前五行

图 6.2 – 从日期中提取的年份变量的 DataFrame 的前五行

  1. 让我们将日期的季度提取到一个新列中,并显示前五行:

    data["quarter"] = data["date"].dt.quarter
    data[["date", "quarter"]].head()
    

    在下面的输出中,我们可以看到新的 quarter 变量:

图 6.3 – 从日期中提取的季度变量的 DataFrame 的前五行

图 6.3 – 从日期中提取的季度变量的 DataFrame 的前五行

  1. 使用 quarter,我们现在可以创建 semester 特征:

    data["semester"] = np.where(data["quarter"] < 3, 1, 2)
    

注意

您可以使用 pandasunique() 方法来探索新变量的不同值,例如,通过执行 df["quarter"].unique()df["semester"].unique()

  1. 让我们将日期的 month 部分提取到一个新列中,并显示 DataFrame 的前五行:

    data["month"] = data["date"].dt.month
    data[["date", "month"]].head()
    

    在下面的输出中,我们可以看到新的 month 变量:

图 6.4 – 包含新月份变量的 DataFrame 的前五行

图 6.4 – 包含新月份变量的 DataFrame 的前五行

  1. 让我们将日期的周数(一年有 52 周)提取出来:

    data["week"] = data["date"].dt.isocalendar().week
    data[["date", "week"]].head()
    

    在下面的输出中,我们可以看到 week 变量:

图 6.5 – 包含新周变量的 DataFrame 的前五行

图 6.5 – 包含新周变量的 DataFrame 的前五行

  1. 让我们将月份的日期提取出来,它可以取 131 之间的值,作为一个新列:

    data["day_mo"] = data["date"].dt.day
    data[["date", "day_mo"]].head()
    

    在下面的输出中,我们可以看到 day_mo 变量:

图 6.6 – 捕获月份日期的新变量的 DataFrame 的顶部行

图 6.6 – DataFrame 顶部行,包含表示月份天数的变量

  1. 让我们提取星期几,其值在06之间(从星期一到星期日),在新的列中,然后显示顶部行:

    data["day_week"] = data["date"].dt.dayofweek
    data[["date", "day_mo", "day_week"]].head()
    

    我们在以下输出中看到day_week变量:

图 6.7 – DataFrame 顶部行,包含表示一周天数的变量

图 6.7 – DataFrame 顶部行,包含表示一周天数的变量

  1. 使用步骤 9中的变量,我们可以创建一个二元变量,表示是否为周末:

    data["is_weekend"] = (
        data[«date»].dt.dayofweek > 4).astype(int)
    data[["date", "day_week", "is_weekend"]].head()
    

    我们在以下输出中看到新的is_weekend变量:

图 6.8 – 包含新变量 is_weekend 的 DataFrame 的前五行

图 6.8 – 包含新变量 is_weekend 的 DataFrame 的前五行

注意

我们可以通过使用feature-engine来自动提取所有这些特征。查看本章中的使用 feature-engine 自动化日期时间特征提取配方以获取更多详细信息。

通过这样,我们已经使用pandasdatetime变量的日期部分提取了许多新特征。这些特征对数据分析、可视化和预测建模很有用。

它是如何工作的...

在本配方中,我们通过使用pandasdt模块从datetime变量中提取了许多与日期相关的特征。首先,我们创建了一个包含日期的变量的样本 DataFrame。我们使用pandasdate_range()从任意日期开始创建一个值范围,并按1天的时间间隔增加。通过periods参数,我们指明了要创建的值范围的数量——即日期的数量。通过freq参数,我们指明了日期之间的步长大小。在我们的例子中,我们使用了D代表天数。最后,我们使用pandasDataFrame()将日期范围转换为一个 DataFrame。

为了提取日期的不同部分,我们使用了pandasdt来访问pandas Series 的datetime属性,然后利用不同的属性。我们使用yearmonthquarter将年份、月份和季度捕获到 DataFrame 的新列中。为了找到学期,我们使用 NumPy 的where()结合新创建的quarter变量创建了一个布尔值。NumPy 的where()扫描quarter变量的值;如果它们小于3,则返回第一个学期的1值;否则,返回第二个学期的2值。

为了提取日期和周的不同表示形式,我们使用了isocalender().weekdaydayofweek属性。利用周几,我们进一步创建了一个二元变量来表示是否为周末。我们使用where()函数扫描周几,如果值大于4,即周六和周日,函数返回True,否则返回False。最后,我们将这个布尔向量转换为整数,以得到一个由 1 和 0 组成的二元变量。有了这个,我们就从日期中创建了多个特征,这些特征可以用于数据分析与预测建模。

还有更多...

使用pandasdt模块,我们可以直接从日期中提取更多特征。例如,我们可以提取月份、季度或年份的开始和结束,是否为闰年,以及一个月中的天数。这些函数允许你做到这一点:

  • pandas.Series.dt.is_month_start

  • pandas.Series.dt.is_month_end

  • pandas.Series.dt.is_quarter_start

  • pandas.Series.dt.is_quarter_end

  • pandas.Series.dt.is_year_start

  • pandas.Series.dt.is_year_end

  • pandas.Series.dt.is_leap_year

  • pandas.Series.dt.days_in_month

我们也可以使用pd.dt.days_in_month返回特定月份的天数,以及一年中的某一天(从1365)使用pd.dt.dayofyear

想要了解更多详情,请访问pandasdatetime文档:pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#time-date-components.

参考以下内容

想要了解如何使用pandasdate_ranges()创建不同的datetime范围,请访问pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases.

想要了解更多关于pandasdt的信息,请访问pandas.pydata.org/pandas-docs/stable/reference/series.html#datetime-properties.

使用 pandas 从时间中提取特征

一些事件在一天中的特定时间发生得更频繁——例如,欺诈活动更有可能在夜间或清晨发生。空气污染物浓度也随着一天中的时间变化,在交通高峰时段达到峰值,此时街道上有更多车辆。因此,从时间中提取特征对于数据分析和预测建模非常有用。在本例中,我们将通过使用pandas和 NumPy 来提取datetime变量的不同时间部分。

准备工作

我们可以使用以下pandasdatetime属性提取小时、分钟和秒:

  • pandas.Series.dt.hour

  • pandas.Series.dt.minute

  • pandas.Series.dt.second

如何做到这一点...

在本例中,我们将提取time变量的hourminutesecond部分。让我们首先导入库并创建一个样本数据集:

  1. 让我们导入pandasnumpy

    import numpy as np
    import pandas as pd
    
  2. 让我们从创建 20 个datetime观测值开始,从2024-05-17午夜开始,然后以 1 小时、15 分钟和 10 秒的增量增加。接下来,我们将时间范围捕获到 DataFrame 中,并显示前五行:

    rng_ = pd.date_range(
        "2024-05-17", periods=20, freq="1h15min10s")
    df = pd.DataFrame({"date": rng_})
    df.head()
    

    在以下输出中,我们看到步骤 2中的变量,包含日期部分和时间部分,值以 1 小时、15 分钟和 10 秒的间隔增加:

图 6.9 – 包含日期时间变量的样本 DataFrame 的前五行

图 6.9 – 包含日期时间变量的样本 DataFrame 的前五行

  1. 让我们提取hourminutesecond部分,并将它们捕获到三个新列中,然后显示 DataFrame 的前五行:

    df["hour"] = df["date"].dt.hour
    df["min"] = df["date"].dt.minute
    df["sec"] = df["date"].dt.second
    df.head()
    

    在以下输出中,我们看到我们在步骤 3中提取的三个time特征:

图 6.10 – 从时间派生出的 DataFrame 的前五行

图 6.10 – 从时间派生出的 DataFrame 的前五行

注意

记住,pandasdt需要一个datetime对象来工作。您可以使用pandasto_datetime()函数将对象变量的数据类型更改为datetime

  1. 让我们执行与步骤 3中相同的操作,但现在是在一行代码中:

    df[["h", "m", "s"]] = pd.DataFrame(
        [(x.hour, x.minute, x.second) for x in df["date"]]
    )
    df.head()
    

    在以下输出中,我们看到新创建的变量:

图 6.11 – 从时间派生出的 DataFrame 的前五行

图 6.11 – 从时间派生出的 DataFrame 的前五行

注意

您可以使用pandasunique()检查新变量的唯一值,例如,通过执行df['hour'].unique()

  1. 最后,让我们创建一个二进制变量,标记早上 6 点至中午 12 点之间发生的事件:

    df["is_morning"] = np.where(
        (df[«hour»] < 12) & (df[«hour»] > 6), 1, 0 )
    df.head()
    

    我们在以下输出中看到is_morning变量:

图 6.12 – 从时间派生出的新变量的 DataFrame 的前几行

图 6.12 – 从时间派生出的新变量的 DataFrame 的前几行

因此,我们从datetime变量的时间部分提取了多个特征。这些特征可用于数据分析预测建模。

它是如何工作的...

在本配方中,我们创建了捕获时间表示的特征。首先,我们创建了一个包含datetime变量的样本 DataFrame。我们使用pandasdate_range()函数创建了一个从任意日期开始,以 1 小时、15 分钟和 10 秒为间隔的 20 个值的范围。我们使用1h15min10s字符串作为freq参数的频率项,以指示所需的增量。接下来,我们使用pandasDataFrame()将日期范围转换为 DataFrame。

为了提取不同时间部分,我们使用了 pandasdt 来访问 hourminutesecond 时间属性。从 time 中提取 hour 后,我们使用它通过 NumPy 的 where() 创建一个新特征,以表示是否是上午。NumPy 的 where() 检查 hour 变量;如果其值小于 12 且大于 6,则分配值为 1;否则,分配值为 0。通过这些操作,我们在 DataFrame 中添加了几个可用于数据分析和训练机器学习模型的特征。

更多...

我们还可以使用以下 pandas 属性提取微秒和纳秒:

  • pandas.Series.dt.microsecond

  • pandas.Series.dt.nanosecond

更多详情,请访问 pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#time-date-components

捕捉 datetime 变量之间的经过时间

我们可以像在前两个菜谱中做的那样,单独从每个 datetime 变量中提取强大的特征。我们可以通过组合多个 datetime 变量来创建额外的特征。一个常见的例子是通过比较 出生日期事件日期 来提取事件发生时的 年龄

在本菜谱中,我们将学习如何通过利用 pandasfeature-engine 来捕捉两个 datetime 变量之间的时间。

如何操作...

为了继续本菜谱,我们将创建一个包含两个 datatime 变量的 DataFrame:

  1. 让我们从导入 pandasnumpydatetime 开始:

    import datetime
    import numpy as np
    import pandas as pd
    
  2. 我们将首先创建两个具有 20 个值的 datetime 变量;第一个变量的值从 2024-05-17 开始,以 1 小时的间隔增加,第二个变量以 1 个月的间隔增加。然后,我们将变量捕捉到 DataFrame 中,添加列名,并显示前几行:

    date = "2024-05-17"
    rng_hr = pd.date_range(date, periods=20, freq="h")
    rng_month = pd.date_range(date, periods=20, freq="ME")
    df = pd.DataFrame(
        {"date1": rng_hr, "date2": rng_month})
    df.head()
    

    我们在以下输出中看到 步骤 2 中的 DataFrame 的前五行:

图 6.13 – 包含两个 datetime 变量的 DataFrame 的前五行

图 6.13 – 包含两个 datetime 变量的 DataFrame 的前五行

  1. 让我们在新特征中捕捉两个变量之间的天数差异,然后显示 DataFrame 的前几行:

    df["elapsed_days"] = (
        df["date2"] - df["date1"]).dt.days
    df.head()
    

    我们在以下输出中看到天数差异:

图 6.14 – 包含捕捉两个 datetime 特征时间差的新的变量的 DataFrame 的前几行

图 6.14 – 包含捕捉两个 datetime 特征时间差的新的变量的 DataFrame 的前几行

  1. 让我们捕捉两个 datetime 变量之间的周数差异,然后显示 DataFrame 的前几行:

    df["weeks_passed"] = (
        (df[«date2»] - df[«date1»]) / np.timedelta64(1, "W"))
    df.head()
    

    我们在以下屏幕截图中看到变量之间的周数差异:

图 6.15 – 以天数和周数表示的 datetime 变量时间差的 DataFrame

图 6.15 – 一个 DataFrame,其中包含两个日期时间变量之间的时间差,以天数和周数表示

  1. 现在,让我们计算变量之间的时间差(以分钟和秒为单位),然后显示 DataFrame 的前几行:

    df["diff_seconds"] = (
        df[«date2»] - df[«date1»])/np.timedelta64(1, «s»)
    df["diff_minutes"] = (
        df[«date2»] - df[«date1»])/ np.timedelta64(1,»m»)
    df.head()
    

    我们在以下输出中看到新变量:

图 6.16 – 以不同的时间单位表示两个日期时间变量之间时间差的 DataFrame

图 6.16 – 一个 DataFrame,其中包含两个日期时间变量之间的时间差,以不同的时间单位表示

  1. 最后,让我们计算一个变量与当前日期之间的差异,以天数表示,然后显示 DataFrame 的前五行:

    df["to_today"] = (
        datetime.datetime.today() - df["date1"])
    df.head()
    

    我们可以在以下输出中的 DataFrame 的最后一列找到新变量:

图 6.17 – 包含 date1 与执行此代码的日期之间差异的新变量的 DataFrame

图 6.17 – 一个 DataFrame,其中包含包含 date1 与执行此代码的日期之间差异的新变量

注意

您计算机上的to_today变量将与此书中的不同,这是由于当前日期(写作时)与您执行代码时的差异。

那就是全部!我们现在已经通过比较两个datetime变量来创建新特征,丰富了我们的数据集。

它是如何工作的...

在这个菜谱中,我们捕捉了两个datetime变量之间时间差异的不同表示。为了继续这个菜谱,我们创建了一个包含两个变量的样本 DataFrame,每个变量从任意日期开始,有 20 个日期。第一个变量以1小时的间隔增加,而第二个变量以1个月的间隔增加。我们使用pandasdate_range()创建了这些变量,我们在本章前两个菜谱中讨论了它。

要确定变量之间的差异——即确定它们之间的时间——我们直接从一个datetime变量减去另一个——即从一个pandas Series 减去另一个。两个pandas Series 之间的差异返回了一个新的pandas Series。为了捕获天数差异,我们使用了pandasdt,然后是days。要将时间差转换为月份,我们使用了 NumPy 的timedelta(),表示我们想要以周为单位传递W到方法的第二个参数。为了捕获秒和分钟的差异,我们分别传递了sm字符串到timedelta()

注意

NumPy 的timedelta的参数是一个数字,例如在我们的例子中是-1,表示单位数,以及一个datetime单位,如天(D)、周(W)、小时(h)、分钟(m)或秒(s)。

最后,我们捕捉了一个datetime变量与今天日期之间的差异。我们通过使用内置的datetime Python 库中的today()获得了今天(写作时)的日期和时间。

还有更多...

我们可以通过使用 feature-engine 的转换器 DatetimeSubstraction() 自动化创建捕获变量之间时间的特征。

  1. 让我们导入 pandasfeature-engine 的转换器:

    import pandas as pd
    from feature_engine.datetime import (
        DatetimeSubtraction
    )
    
  2. 让我们重新创建我们在 如何做 it… 部分的 步骤 2 中描述的示例数据集:

    date = "2024-05-17"
    rng_hr = pd.date_range(date, periods=20, freq="h")
    rng_month = pd.date_range(date, periods=20, freq="ME")
    df = pd.DataFrame(
        {"date1": rng_hr, "date2": rng_month})
    
  3. 让我们设置 DatetimeSubstraction() 以返回第二个日期和第一个日期之间以天为单位的时间差:

    ds = DatetimeSubtraction(
        variables="date2",
        reference="date1",
        output_unit="D",
    )
    

注意

我们可以通过在 variablesreference 参数中传递变量列表来获取两个以上变量的差异。

  1. 让我们创建并显示新的特征:

    dft = ds.fit_transform(df)
    dft.head()
    

    在以下输出中,我们看到捕获两个 datetime 变量之间时间差的变量:

图 6.18 – 包含两个日期时间变量之间差异的新变量的 DataFrame

图 6.18 – 包含两个日期时间变量之间差异的新变量的 DataFrame

更多详情,请查看 feature-engine.trainindata.com/en/latest/api_doc/datetime/DatetimeSubtraction.html

参见

要了解更多关于 NumPy 的 timedelta,请访问 numpy.org/devdocs/reference/arrays.datetime.html#datetime-and-timedelta-arithmetic

在不同时区处理时间

一些组织在国际上运营;因此,他们收集关于事件的信息可能记录在事件发生地区的时区旁边。为了能够比较发生在不同时区的事件,我们通常必须将所有变量设置在同一个时区。在本教程中,我们将学习如何统一 datetime 变量的时区,以及如何使用 pandas 将变量重新分配到不同的时区。

如何做...

要继续本教程,我们将创建一个包含两个不同时区变量的样本 DataFrame:

  1. 让我们导入 pandas

    import pandas as pd
    
  2. 让我们创建一个包含不同时区值的单个变量的 DataFrame:

    df = pd.DataFrame()
    df['time1'] = pd.concat([
        pd.Series(
            pd.date_range(
                start='2024-06-10 09:00',
                freq='h',
                periods=3,
                tz='Europe/Berlin')),
        pd.Series(
            pd.date_range(
                start='2024-09-10 09:00',
                freq='h',
                periods=3,
                tz='US/Central'))
        ], axis=0)
    
  3. 让我们在 DataFrame 中添加另一个 datetime 变量,它也包含不同时区的值:

    df['time2'] = pd.concat([
        pd.Series(
            pd.date_range(
                start='2024-07-01 09:00',
                freq='h',
                periods=3,
                tz='Europe/Berlin')),
        pd.Series(
            pd.date_range(
                start='2024-08-01 09:00',
                freq='h',
                periods=3,
                tz='US/Central'))
        ], axis=0)
    

    如果我们现在执行 df,我们将看到具有不同时区变量的 DataFrame,如下面的输出所示:

图 6.19 – 包含两个不同时区日期时间变量的 DataFrame

图 6.19 – 包含两个不同时区日期时间变量的 DataFrame

注意

时区用 +02-05 的值表示,分别表示与协调世界时(UTC)的时间差。

  1. 要处理不同的时区,我们通常将变量设置在同一个时区,在这种情况下,我们选择了 UTC:

    df['time1_utc'] = pd.to_datetime(
        df['time1'], utc=True)
    df['time2_utc'] = pd.to_datetime(
        df['time2'], utc=True)
    

如果我们现在执行df,我们将看到新的变量,它们与 UTC 相比有00小时的差异:

图 6.20 – 包含 UTC 中新的变量的 DataFrame

图 6.20 – 包含 UTC 中新的变量的 DataFrame

  1. 让我们计算变量之间的天数差异,然后显示 DataFrame 的前五行:

    df['elapsed_days'] = (
        df[‹time2_utc›] - df[‹time1_utc›]). dt.days
    df['elapsed_days'].head()
    

    在以下输出中,我们可以看到变量之间的时间差异:

    0    21
    1    21
    2    21
    0   -40
    1   -40
    datetime variables to the London and Berlin time zones, and then display the resulting variables:
    
    

    df['time1_london'] = df[

    ‹time1_utc›].dt.tz_convert('Europe/London')

    df['time2_berlin'] = df[

    ‹time1_utc›].dt.tz_convert('Europe/Berlin')

    df[['time1_london', 'time2_berlin']]

    
    We see the variables in their respective time zones in the following output:
    

图 6.21 – 将变量重新格式化为不同的时区

图 6.21 – 将变量重新格式化为不同的时区

在更改时区时,不仅时区的值会改变——即,如图像中的+01+02值——而且小时的值也会改变。

它是如何工作的...

在这个示例中,我们更改了时区,并在不同时区的变量之间执行操作。首先,我们创建了一个包含两个变量的 DataFrame,这些变量的值从一个任意日期开始,每小时增加;这些变量设置在不同的时区。为了将不同的时区变量合并到一个 DataFrame 列中,我们利用pandasconcat()函数连接了pandasdate_range()返回的序列。我们将axis参数设置为0,表示我们想要将序列垂直连接到一个列中。我们已经在本章前面的示例中广泛介绍了pandasdate_range()的参数;有关更多详细信息,请参阅使用 pandas 从日期中提取特征使用 pandas 从时间中提取特征的示例。

为了将变量的时区重置为中央时区,我们使用了pandasto_datetime(),传递utc=True。最后,我们通过从一个序列减去另一个序列并捕获天数差异来确定变量之间的时间差异。为了重新分配不同的时区,我们使用了pandastz_convert(),将新的时区作为参数指定。

相关内容

要了解更多关于pandasto_datetime()的信息,请访问pandas.pydata.org/pandas-docs/stable/reference/api/pandas.to_datetime.html

要了解更多关于pandastz_convert()的信息,请访问pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.dt.tz_convert.html

使用 Feature-engine 自动化日期时间特征提取

feature-engine是一个适用于与pandas DataFrame 一起工作的特征工程和选择的 Python 库。DatetimeFeatures()类可以通过使用pandasdt自动从日期和时间中提取特征。DatetimeFeatures()允许您提取以下特征:

  • 月份

  • 季度

  • 学期

  • 年份

  • 周几

  • 月份中的日

  • 年份中的日

  • 周末

  • 月份开始

  • 月份结束

  • 季度开始

  • 季度结束

  • 年度开始

  • 年度结束

  • 闰年

  • 一个月中的天数

  • 小时

  • 分钟

在这个菜谱中,我们将通过利用 feature-engine 自动从日期和时间创建特征。

如何操作...

为了展示 feature-engine 的功能,我们将创建一个包含 datetime 变量的样本 DataFrame:

  1. 让我们先导入 pandasDatetimeFeatures()

    import pandas as pd
    from feature_engine.datetime import DatetimeFeatures
    
  2. 让我们创建一个包含 20 个值的 datetime 变量,从 2024-05-17 凌晨开始,然后以 1 天的增量递增。然后,我们将这个变量存储在一个 DataFrame 中:

    rng_ = pd.date_range(
        '2024-05-17', periods=20, freq='D')
    data = pd.DataFrame({'date': rng_})
    
  3. 我们将首先设置转换器以提取所有支持的 datetime 特征:

    dtfs = DatetimeFeatures(
        variables=None,
        features_to_extract= "all",
    )
    

备注

DatetimeFeatures() 自动查找 datetime 类型的变量,或者当 variables 参数设置为 None 时可以解析为 datetime 的变量。或者,您可以传递一个包含您想要提取 datetime 特征的变量名称的列表。

  1. 让我们添加 datetime 特征到数据中:

    dft = dtfs.fit_transform(data)
    

备注

默认情况下,DatetimeFeatures() 从每个 datetime 变量中提取以下特征:monthyearday_of_weekday_of_monthhourminute,和 second。我们可以通过 features_to_extract 参数修改此行为,就像我们在 步骤 3 中所做的那样。

  1. 让我们将新变量的名称记录在列表中:

    vars_ = [v for v in dft.columns if "date" in v]
    

备注

DatetimeFeatures() 使用原始变量名称(在这种情况下为 date)后跟一个下划线和创建的特征类型来命名新变量,例如,date_day_of_week 包含从 date 变量中提取的星期几。

如果我们执行 vars_,我们将看到创建的特征名称:

['date_month',
 'date_quarter',
 'date_semester',
 'date_year',
 'date_week',
 'date_day_of_week',
 'date_day_of_month',
 'date_day_of_year',
 'date_weekend',
 'date_month_start',
 'date_month_end',
 'date_quarter_start',
 'date_quarter_end',
'date_year_start',
 'date_year_end',
 'date_leap_year',
 'date_days_in_month',
 'date_hour',
 'date_minute',
dft[vars_].head(). We can’t show the resulting DataFrame in the book because it is too big.
Note
We can create specific features by passing their names to the `features_to_extract` parameter.
For example, to extract `week` and `year`, we set the transformer like this: `dtfs = DatetimeFeatures(features_to_extract=["week", "year"])`. We can also extract all supported features by setting the `features_to_extract` parameter to `"all"`.
`DatetimeFeatures()` can also create features from variables in different time zones. Let’s learn how to correctly set up the transformer in this situation.

1.  Let’s create a sample DataFrame with a variable’s values in different time zones:

    ```

    df = pd.DataFrame()

    df["time"] = pd.concat(

    [

    pd.Series(

    pd.date_range(

    start="2024-08-01 09:00",

    freq="h",

    periods=3,

    tz="Europe/Berlin"

    )

    ),

    pd.Series(

    pd.date_range(

    start="2024-08-01 09:00",

    freq="h",

    periods=3, tz="US/Central"

    )

    ),

    ],

    axis=0,

    )

    ```py

    If we execute `df`, we will see the DataFrame from *Step 6*, as shown in the following output:

![Figure 6.22 – A DataFrame with a variable’s values in different time zones](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/5c5b818c438145a29fc10ab264bbd1bd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=3AyDTMs3DvGWh3l8EHdqRLM76i8%3D)

Figure 6.22 – A DataFrame with a variable’s values in different time zones

1.  We’ll set the transformer to extract three specific features from this variable after setting it to the UTC:

    ```

    dfts = DatetimeFeatures(

    features_to_extract=

    ["day_of_week", "hour","minute"],

    drop_original=False,

    utc=True,

    )

    ```py

     2.  Let’s create the new features:

    ```

    dft = dfts.fit_transform(df)

    ```py

    `DatetimeFeatures()` will set all variables into UTC before deriving the features. With `dft.head()`, we can see the resulting DataFrame:

![Figure 6.23 – A DataFrame with the original and new variables](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a92ba92ea2694b8ba4264c24f4b6e1d5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=mqkiIEH22rFoGkewTgWXxt5%2BDtc%3D)

Figure 6.23 – A DataFrame with the original and new variables
With that, we’ve created multiple date and time-related features in a few lines of code. `feature-engine` offers a great alternative to manually creating features per feature with `pandas`. In addition, `DatetimeFeatures()` can be integrated into scikit-learn’s `Pipeline` and `GridSearchCV`, among other classes.
How it works...
`DatetimeFeatures()` extracts several date and time features from `datetime` variables automatically by utilizing `pandas`’ `dt` under the hood. It works with variables whose original data types are `datetime`, as well as with object-like and categorical variables, provided that they can be parsed into a `datetime` format.
`DatetimeFeatures()` extracts the following features by default: `month`, `year`, `day_of_week`, `day_of_month`, `hour`, `minute` and `second`. We can make the transformer return all the features it supports by setting the parameter `features_to_extract` to `all`. In addition, we can extract a specific subset of features by passing the feature names in a list, as we did in *Step 7*.
`DatetimeFeatures()` automatically finds `datetime` variables or variables that can be parsed as `datetime` in the DataFrame passed to the `fit()` method. To extract features from a selected variable or group of variables, we can pass their name in a list to the `variables` parameter when we set up the transformer.
With `fit()`, `DatetimeFeatures()` does not learn any parameters; instead, it checks that the variables entered by the user are, or can be, parsed into a `datetime` format. If the user does not indicate variable names, `DatetimeFeatures()` finds the `datetime` variables automatically. With `transform()`, the transformer adds the date and time-derived variables to the DataFrame.

第七章:执行特征缩放

许多机器学习算法对变量尺度很敏感。例如,线性模型的系数取决于特征的尺度——也就是说,改变特征尺度将改变系数的值。在线性模型以及依赖于距离计算的算法(如聚类和主成分分析)中,值范围较大的特征往往会支配值范围较小的特征。因此,将特征放在相似的尺度上允许我们比较特征的重要性,并可能帮助算法更快收敛,从而提高性能和训练时间。

通常,缩放技术将变量除以某个常数;因此,重要的是要强调,当我们重新缩放变量时,变量分布的形状不会改变。如果你想改变分布形状,请查看第三章转换 数值变量

在本章中,我们将描述不同的方法来设置特征在相似的尺度上。

本章将涵盖以下食谱:

  • 标准化特征

  • 缩放到最大值和最小值

  • 使用中位数和分位数进行缩放

  • 执行均值归一化

  • 实现最大绝对缩放

  • 缩放到向量单位长度

技术要求

本章中我们使用的库主要有用于缩放的 scikit-learn(sklearn),用于处理数据的pandas,以及用于绘图的matplotlib

标准化特征

标准化是将变量中心在0并标准化方差为1的过程。为了标准化特征,我们从每个观测值中减去均值,然后将结果除以标准差:

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow><mrow><msub><mi>x</mi><mrow><mi>s</mi><mi>c</mi><mi>a</mi><mi>l</mi><mi>e</mi><mi>d</mi></mrow></msub><mo>=</mo><mfrac><mrow><mi>x</mi><mo>−</mo><mi>m</mi><mi>e</mi><mi>a</mi><mi>n</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow><mrow><mi>s</mi><mi>t</mi><mi>d</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow></mfrac></mrow></mrow></math>

前一个转换的结果被称为z 分数,表示给定观测值与平均值相差多少个标准差。

当模型需要变量以零为中心且数据不是稀疏的(稀疏数据的中心化会破坏其稀疏性)时,标准化通常很有用。然而,标准化对异常值敏感,并且如果变量高度偏斜,z 分数不会保持对称属性,正如我们在下一节中讨论的。

准备工作

使用标准化,变量分布不会改变;改变的是它们值的幅度,正如我们在以下图中看到的:

图 7.1 – 标准化前后正态和偏斜变量的分布

图 7.1 – 标准化前后正态分布和偏态变量的分布。

z 分数(底部面板的x轴)表示一个观测值与均值偏离多少个标准差。当 z 分数为1时,观测值位于均值右侧 1 个标准差处,而当 z 分数为-1时,样本位于均值左侧 1 个标准差处。

在正态分布的变量中,我们可以估计一个值大于或小于给定 z 分数的概率,并且这种概率分布是对称的。观测值小于 z 分数-1的概率等同于值大于1的概率(底部左面板中的水平线)。这种对称性是许多统计测试的基础。在偏态分布中,这种对称性不成立。如图 7.1 底部右面板所示(水平线),值小于-1的概率与大于1的概率不同。

注意

均值和标准差对异常值敏感;因此,在使用标准化时,特征可能彼此之间有不同的缩放比例。

在实践中,我们经常在忽略分布形状的情况下应用标准化。然而,请记住,如果您使用的模型或测试对数据的分布有假设,您可能需要在标准化之前转换变量,或者尝试不同的缩放方法。

如何实现...

在本食谱中,我们将对加利福尼亚住房数据集的变量应用标准化:

  1. 让我们先导入所需的 Python 包、类和函数:

    import pandas as pd
    from sklearn.datasets import fetch_california_housing
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import StandardScaler
    
  2. 让我们将加利福尼亚住房数据集从 scikit-learn 加载到 DataFrame 中,并删除LatitudeLongitude变量:

    X, y = fetch_california_housing(
        return_X_y=True, as_frame=True)
    X.drop(labels=["Latitude", "Longitude"], axis=1,
        inplace=True)
    
  3. 现在,让我们将数据分为训练集和测试集:

    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.3, random_state=0)
    
  4. 接下来,我们将设置 scikit-learn 中的StandardScaler()函数并将其拟合到训练集中,以便它学习每个变量的均值和标准差:

    scaler = StandardScaler().set_output(
        transform="pandas")
    scaler.fit(X_train)
    

注意

Scikit-learn 缩放器,就像任何 scikit-learn 转换器一样,默认返回 NumPy 数组。要返回pandaspolars DataFrame,我们需要使用set_output()方法指定输出容器,就像我们在步骤 4中所做的那样。

  1. 现在,让我们使用训练好的 scaler 对训练集和测试集进行标准化:

    X_train_scaled = scaler.transform(X_train)
    X_test_scaled = scaler.transform(X_test)
    

    StandardScaler()fit()过程中存储从训练集中学习到的均值和标准差。让我们可视化学习到的参数。

  2. 首先,我们将打印出由scaler学习到的平均值:

    scaler.mean_
    

    在以下输出中,我们可以看到每个变量的平均值:

    scaler:
    
    

    scaler.scale_

    array([1.89109236e+00, 1.25962585e+01, 2.28754018e+00, 4.52736275e-01, 1.14954037e+03, 6.86792905e+00])

    
    Let’s compare the transformed data with the original data to understand the changes.
    
  3. 让我们打印出测试集中原始变量的描述性统计信息:

    X_test.describe()
    

    在以下输出中,我们可以看到变量的平均值与零不同,方差各不相同:

图 7.2 – 缩放前的变量的描述性统计参数

图 7.2 – 缩放前的变量的描述性统计参数

  1. 现在我们来打印转换变量的描述性统计值:

    X_test_scaled.describe()
    

    在以下输出中,我们可以看到变量的均值现在集中在 0,方差约为 1

图 7.3 – 缩放变量的描述性统计参数,显示均值为 0 和方差约为 1

图 7.3 – 缩放变量的描述性统计参数,显示均值为 0 和方差约为 1

注意

AveRoomsAveBedrmsAveOccup 变量高度偏斜,这可能导致测试集中的观察值远大于或远小于训练集中的值,因此我们看到方差偏离 1。这是可以预料的,因为标准化对异常值和非常偏斜的分布很敏感。

准备就绪 部分中,我们提到分布的形状不会随着标准化而改变。通过执行 X_test.hist() 然后执行 X_test_scaled.hist() 来验证这一点,并比较转换前后的变量分布。

它是如何工作的...

在这个示例中,我们通过使用 scikit-learn 对加利福尼亚住房数据集的变量进行了标准化。我们将数据分为训练集和测试集,因为标准化的参数应该从训练集中学习。这是为了避免在预处理步骤中将测试集的数据泄露到训练集中,并确保测试集对所有特征转换过程保持无知的。

为了标准化这些特征,我们使用了 scikit-learn 的 StandardScaler() 函数,该函数能够学习并存储在转换中使用的参数。使用 fit(),缩放器学习每个变量的均值和标准差,并将它们存储在其 mean_scale_ 属性中。使用 transform(),缩放器对训练集和测试集中的变量进行了标准化。StandardScaler() 的默认输出是 NumPy 数组,但通过 set_output() 参数,我们可以将输出容器更改为 pandas DataFrame,就像我们在 步骤 4 中所做的那样,或者通过设置 transform="polars" 来更改为 polars

注意

StandardScaler() 默认会减去均值并除以标准差。如果我们只想对分布进行中心化而不进行标准化,我们可以在初始化转换器时设置 with_std=False。如果我们想在 步骤 4 中将方差设置为 1,而不对分布进行中心化,我们可以通过设置 with_mean=False 来实现。

缩放到最大值和最小值

将变量缩放到最小值和最大值会压缩变量的值在01之间。要实现这种缩放方法,我们从所有观测值中减去最小值,然后将结果除以值范围——即最大值和最小值之间的差值:

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow><mrow><msub><mi>x</mi><mrow><mi>s</mi><mi>c</mi><mi>a</mi><mi>l</mi><mi>e</mi><mi>d</mi></mrow></msub><mo>=</mo><mfrac><mrow><mi>x</mi><mo>−</mo><mi mathvariant="normal">m</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">n</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow><mrow><mi>max</mi><mfenced open="(" close=")"><mi>x</mi></mfenced><mo>−</mo><mi mathvariant="normal">m</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">n</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow></mfrac></mrow></mrow></math>

将变量缩放到最小值和最大值适用于标准差非常小的变量,当模型不需要数据中心化到零,以及我们希望在稀疏数据中保留零条目时,例如在独热编码变量。然而,它的缺点是敏感于异常值。

准备工作

缩放到最小值和最大值不会改变变量的分布,如下面的图所示:

图 7.4 – 缩放到最小值和最大值前后的正态和偏斜变量的分布

图 7.4 – 缩放到最小值和最大值前后的正态和偏斜变量的分布

这种缩放方法将变量的最大值标准化为单位大小。将变量缩放到最小值和最大值通常是标准化的首选替代方案,适用于标准差非常小的变量,以及我们希望在稀疏数据中保留零条目时,例如在独热编码变量或来自计数的变量(如词袋)中。然而,此过程不会将变量中心化到零,因此如果算法有此要求,这种方法可能不是最佳选择。

注意

将变量缩放到最小值和最大值对异常值敏感。如果训练集中存在异常值,缩放会将值压缩到一端。相反,如果测试集中存在异常值,变量在缩放后将会显示大于1或小于0的值,具体取决于异常值是在左尾还是右尾。

如何操作...

在这个示例中,我们将把加利福尼亚住房数据集的变量缩放到01之间的值:

  1. 让我们先导入pandas和所需的类和函数:

    import pandas as pd
    from sklearn.datasets import fetch_california_housing
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import MinMaxScaler
    
  2. 让我们从 scikit-learn 中加载加利福尼亚住房数据集到一个pandas DataFrame 中,并丢弃LatitudeLongitude变量:

    X, y = fetch_california_housing(
        return_X_y=True, as_frame=True)
    X.drop(labels=["Latitude", "Longitude"], axis=1,
        inplace=True)
    
  3. 让我们将数据分为训练集和测试集:

    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.3, random_state=0)
    
  4. 让我们设置缩放器并将其拟合到训练集,以便它学习每个变量的最小值和最大值以及值范围:

    scaler = MinMaxScaler().set_output(
        transform="pandas"")
    scaler.fit(X_train)
    
  5. 最后,让我们使用训练好的缩放器对训练集和测试集中的变量进行缩放:

    X_train_scaled = scaler.transform(X_train)
    X_test_scaled = scaler.transform(X_test)
    

注意

MinMaxScaler()将最大值和最小值以及值范围分别存储在其data_max_min_data_range_属性中。

我们可以通过执行X_test_scaled.min()来验证变换后变量的最小值,它将返回以下输出:

MedInc           0.000000
HouseAge        0.000000
AveRooms        0.004705
AveBedrms      0.004941
Population     0.000140
AveOccup      -0.000096
X_test_scaled.max(), we see that the maximum values of the variables are around 1:

MedInc           1.000000

HouseAge        1.000000

AveRooms        1.071197

AveBedrms      0.750090

Population     0.456907

AveOccup        2.074553

dtype: float64


 Note
If you check the maximum values of the variables in the train set after the transformation, you’ll see that they are exactly `1`. Yet, in the test set, we see values greater and smaller than `1`. This occurs because, in the test set, there are observations with larger or smaller magnitudes than those in the train set. In fact, we see the greatest differences in the variables that deviate the most from the normal distribution (the last four variables in the dataset). This behavior is expected because scaling to the minimum and maximum values is sensitive to outliers and very skewed distributions.
Scaling to the minimum and maximum value does not change the shape of the variable’s distribution. You can corroborate that by displaying the histograms before and after the transformation.
How it works...
In this recipe, we scaled the variables of the California housing dataset to values between `0` and `1`.
`MinMaxScaler()` from scikit-learn learned the minimum and maximum values and the value range of each variable when we called the `fit()` method and stored these parameters in its `data_max_`, `min_`, and `data_range_` attributes. By using `transform()`, we made the scaler remove the minimum value from each variable in the train and test sets and divide the result by the value range.
Note
`MinMaxScaler()` will scale all variables by default. To scale only a subset of the variables in the dataset, you can use `ColumnTransformer()` from scikit-learn or `SklearnTransformerWrapper()` from `Feature-engine`.
`MinMaxScaler()` will scale the variables between `0` and `1` by default. However, we have the option to scale to a different range by adjusting the tuple passed to the `feature_range` parameter.
By default, `MinMaxScaler()` returns NumPy arrays, but we can modify this behavior to return `pandas` DataFrames with the `set_output()` method, as we did in *Step 4*.
Scaling with the median and quantiles
When scaling variables to the median and quantiles, the median value is removed from the observations, and the result is divided by the **Inter-Quartile Range** (**IQR**). The IQR is the difference between the 3rd quartile and the 1st quartile, or, in other words, the difference between the 75th percentile and the 25th percentile:
![<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow><mrow><mi>x</mi><mo>_</mo><mi>s</mi><mi>c</mi><mi>a</mi><mi>l</mi><mi>e</mi><mi>d</mi><mo>=</mo><mfrac><mrow><mi>x</mi><mo>−</mo><mi>m</mi><mi>e</mi><mi>d</mi><mi>i</mi><mi>a</mi><mi>n</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow><mrow><mn>3</mn><mi>r</mi><mi>d</mi><mi>q</mi><mi>u</mi><mi>a</mi><mi>r</mi><mi>i</mi><mi>t</mi><mi>l</mi><mi>e</mi><mfenced open="(" close=")"><mi>x</mi></mfenced><mo>−</mo><mn>1</mn><mi>s</mi><mi>t</mi><mi>q</mi><mi>u</mi><mi>a</mi><mi>r</mi><mi>t</mi><mi>i</mi><mi>l</mi><mi>e</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow></mfrac></mrow></mrow></math>](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/09ec90d4b793436c9138099065651243~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=BkY0HgyisC5vMFKCJ7hYb8a4KXk%3D)
This method is known as **robust scaling** because it produces more robust estimates for the center and value range of the variable. Robust scaling is a suitable alternative to standardization when models require the variables to be centered and the data contains outliers. It is worth noting that robust scaling will not change the overall shape of the variable distribution.
How to do it...
In this recipe, we will implement scaling with the median and IQR by utilizing scikit-learn:

1.  Let’s start by importing `pandas` and the required scikit-learn classes and functions:

    ```

    导入 pandas 库作为 pd

    从 sklearn.datasets 导入 fetch_california_housing

    从 sklearn.model_selection 导入 train_test_split

    从 sklearn.preprocessing 导入 RobustScaler

    ```py

     2.  Let’s load the California housing dataset into a `pandas` DataFrame and drop the `Latitude` and `Longitude` variables:

    ```

    X, y = fetch_california_housing(

    return_X_y=True, as_frame=True)

    X.drop(labels=[     "Latitude", "Longitude"], axis=1,

    inplace=True)

    ```py

     3.  Let’s divide the data into train and test sets:

    ```

    X_train, X_test, y_train, y_test = train_test_split(

    X, y, test_size=0.3, random_state=0)

    ```py

     4.  Let’s set up scikit-learn’s `RobustScaler()`and fit it to the train set so that it learns and stores the median and IQR:

    ```

    scaler = RobustScaler().set_output(

    transform="pandas")

    scaler.fit(X_train)

    ```py

     5.  Finally, let’s scale the variables in the train and test sets with the trained scaler:

    ```

    X_train_scaled = scaler.transform(X_train)

    X_test_scaled = scaler.transform(X_test)

    ```py

     6.  Let’s print the variable median values learned by `RobustScaler()`:

    ```

    scaler.center_

    ```py

    We see the parameters learned by `RobustScaler()` in the following output:

    ```

    RobustScaler():

    ```py
    scaler.scale_
    array([2.16550000e+00, 1.90000000e+01, 1.59537022e+00,                 9.41284380e-02, 9.40000000e+02, 8.53176853e-01])
    ```

    这种缩放过程不会改变变量的分布。请使用直方图比较变换前后变量的分布。

    ```py

How it works...
To scale the features using the median and IQR, we created an instance of `RobustScaler()`. With `fit()`, the scaler learned the median and IQR for each variable from the train set. With `transform()`, the scaler subtracted the median from each variable in the train and test sets and divided the result by the IQR.
After the transformation, the median values of the variables were centered at `0`, but the overall shape of the distributions did not change. You can corroborate the effect of the transformation by displaying the histograms of the variables before and after the transformation and by printing out the main statistical parameters through `X_test.describe()` and `X_test_scaled.b()`.
Performing mean normalization
In mean normalization, we center the variable at `0` and rescale the distribution to the value range, so that its values lie between `-1` and `1`. This procedure involves subtracting the mean from each observation and then dividing the result by the difference between the minimum and maximum values, as shown here:
![<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow><mrow><msub><mi>x</mi><mrow><mi>s</mi><mi>c</mi><mi>a</mi><mi>l</mi><mi>e</mi><mi>d</mi></mrow></msub><mo>=</mo><mfrac><mrow><mi>x</mi><mo>−</mo><mi>m</mi><mi>e</mi><mi>a</mi><mi>n</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow><mrow><mi>max</mi><mfenced open="(" close=")"><mi>x</mi></mfenced><mo>−</mo><mi mathvariant="normal">m</mi><mi mathvariant="normal">i</mi><mi mathvariant="normal">n</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow></mfrac></mrow></mrow></math>](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/b31765ccbc874441a68b53a61846d7d7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=%2FcdtIUYoa41jh6qlHk7%2BZJsCFXk%3D)
Note
Mean normalization is an alternative to standardization. In both cases, the variables are centered at `0`. In mean normalization, the variance varies, while the values lie between `-1` and `1`. On the other hand, in standardization, the variance is set to `1` and the value range varies.
Mean normalization is a suitable alternative for models that need the variables to be centered at zero. However, it is sensitive to outliers and not a suitable option for sparse data, as it will destroy the sparse nature.
How to do it...
In this recipe, we will implement mean normalization with `pandas`:

1.  Let’s import `pandas` and the required scikit-learn class and function:

    ```

    导入 pandas 库作为 pd

    从 sklearn.datasets 导入 fetch_california_housing

    从 sklearn.model_selection 导入 train_test_split

    ```py

     2.  Let’s load the California housing dataset from scikit-learn into a `pandas` DataFrame, dropping the `Latitude` and `Longitude` variables:

    ```

    X, y = fetch_california_housing(

    return_X_y=True, as_frame=True)

    X.drop(labels=[

    "Latitude", "Longitude"], axis=1, inplace=True)

    ```py

     3.  Let’s divide the data into train and test sets:

    ```

    X_train, X_test, y_train, y_test = train_test_split(

    X, y, test_size=0.3, random_state=0)

    ```py

     4.  Let’s learn the mean values from the variables in the train set:

    ```

    means = X_train.mean(axis=0)

    ```py

Note
We set `axis=0` to take the mean of the rows – that is, of the observations in each variable. If we set `axis=1` instead, `pandas` will calculate the mean value per observation across all the columns.
By executing `print(mean)`, we display the mean values per variable:

MedInc           3.866667

HouseAge        28.618702

AveRooms         5.423404

AveBedrms        1.094775

Population    1425.157323

AveOccup         3.040518

dtype: float64


1.  Now, let’s determine the difference between the maximum and minimum values per variable:

    ```

    ranges = X_train.max(axis=0)-X_train.min(axis=0)

    ```py

    By executing `print(ranges)`, we display the value ranges per variable:

    ```

    MedInc           14.500200

    HouseAge         51.000000

    AveRooms        131.687179

    AveBedrms        33.733333

    Population    35679.000000

    AveOccup        598.964286

    dtype: float64

    ```py

Note
The `pandas` `mean()`, `max()`, and `min()` methods return a `pandas` series.

1.  Now, we’ll apply mean normalization to the train and test sets by utilizing the learned parameters:

    ```

    X_train_scaled = (X_train - means) / ranges

    X_test_scaled = (X_test - means) / ranges

    ```py

Note
In order to transform future data, you will need to store these parameters, for example, in a `.txt` or `.``csv` file.
*Step 6* returns `pandas` DataFrames with the transformed train and test sets. Go ahead and compare the variables before and after the transformations. You’ll see that the distributions did not change, but the variables are centered at `0`, and their values lie between `-1` and `1`.
How it works…
To implement mean normalization, we captured the mean values of the numerical variables in the train set using `mean()` from `pandas`. Next, we determined the difference between the maximum and minimum values of the numerical variables in the train set by utilizing `max()` and `min()` from `pandas`. Finally, we used the `pandas` series returned by these functions containing the mean values and the value ranges to normalize the train and test sets. We subtracted the mean from each observation in our train and test sets and divided the result by the value ranges. This returned the normalized variables in a `pandas` DataFrame.
There’s more...
There is no dedicated scikit-learn transformer to implement mean normalization, but we can combine the use of two transformers to do so.
To do this, we need to import `pandas` and load the data, just like we did in *Steps 1* to *3* in the *How to do it...* section of this recipe. Then, follow these steps:

1.  Import the scikit-learn transformers:

    ```

    从 sklearn.preprocessing 导入(

    StandardScaler, RobustScaler

    )

    ```py

     2.  Let’s set up `StandardScaler()` to learn and subtract the mean without dividing the result by the standard deviation:

    ```

    scaler_mean = StandardScaler(

    with_mean=True, with_std=False,

    ).set_output(transform="pandas")

    ```py

     3.  Now, let’s set up `RobustScaler()` so that it does not remove the median from the values but divides them by the value range – that is, the difference between the maximum and minimum values:

    ```

    scaler_minmax = RobustScaler(

    with_centering=False,

    with_scaling=True,

    quantile_range=(0, 100)

    ).设置输出为 transform="pandas"

    ```py

Note
To divide by the difference between the minimum and maximum values, we need to specify `(0, 100)` in the `quantile_range` argument of `RobustScaler()`.

1.  Let’s fit the scalers to the train set so that they learn and store the mean, maximum, and minimum values:

    ```

    scaler_mean.fit(X_train)

    scaler_minmax.fit(X_train)

    ```py

     2.  Finally, let’s apply mean normalization to the train and test sets:

    ```

    X_train_scaled = scaler_minmax.transform(

    scaler_mean.transform(X_train)

    )

    X_test_scaled = scaler_minmax.transform(

    scaler_mean.transform(X_test)

    )

    ```py

We transformed the data with `StandardScaler()` to remove the mean and then transformed the resulting DataFrame with `RobustScaler()` to divide the result by the range between the minimum and maximum values. We described the functionality of `StandardScaler()` in this chapter’s *Standardizing the features* recipe and `RobustScaler()` in the *Scaling with the median and quantiles* recipe of this chapter.
Implementing maximum absolute scaling
Maximum absolute scaling scales the data to its maximum value – that is, it divides every observation by the maximum value of the variable:
![<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow><mrow><msub><mi>x</mi><mrow><mi>s</mi><mi>c</mi><mi>a</mi><mi>l</mi><mi>e</mi><mi>d</mi></mrow></msub><mo>=</mo><mfrac><mi>x</mi><mrow><mi mathvariant="normal">m</mi><mi mathvariant="normal">a</mi><mi mathvariant="normal">x</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow></mfrac></mrow></mrow></math>](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/7449e3cf6e5d4ee39be3bcd67b930cbb~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=dfjZjqWKZ%2BkeComRQ%2BE4V9Cy6ck%3D)
As a result, the maximum value of each feature will be `1.0`. Note that maximum absolute scaling does not center the data, and hence, it’s suitable for scaling sparse data. In this recipe, we will implement maximum absolute scaling with scikit-learn.
Note
Scikit-learn recommends using this transformer on data that is centered at `0` or on sparse data.
Getting ready
Maximum absolute scaling was specifically designed to scale sparse data. Thus, we will use a bag-of-words dataset that contains sparse variables for the recipe. In this dataset, the variables are words, the observations are documents, and the values are the number of times each word appears in the document. Most entries in the data are `0`.
We will use a dataset consisting of a bag of words, which is available in the UCI Machine Learning Repository (https://archive.ics.uci.edu/ml/datasets/Bag+of+Words), which is licensed under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/legalcode).
I downloaded and prepared a small bag of words representing a simplified version of one of those datasets. You will find this dataset in the accompanying GitHub repository: [`github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/tree/main/ch07-scaling`](https://github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/tree/main/ch07-scaling).
How to do it...
Let’s begin by importing the required packages and loading the dataset:

1.  Let’s import the required libraries and the scaler:

    ```

    导入 matplotlib.pyplot 库作为 plt

    导入 pandas 库作为 pd

    从 sklearn.preprocessing 导入 MaxAbsScaler

    ```py

     2.  Let’s load the bag-of-words dataset:

    ```

    data = pd.read_csv("bag_of_words.csv")

    ```py

    If we execute `data.head()`, we will see the DataFrame consisting of the words as columns, the documents as rows, and the number of times each word appeared in a document as values:

![Figure 7.5 – DataFrame with the bag of words](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/06e0a25779d745b0945d390d23004a2b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=grdP%2FnIU%2BSJOgB6w4rod2HmwuJg%3D)

Figure 7.5 – DataFrame with the bag of words
Note
Although we omit this step in the recipe, remember that the maximum absolute values should be learned from a training dataset only. Split the dataset into train and test sets when carrying out your analysis.

1.  Let’s set up `MaxAbsScaler()` and fit it to the data so that it learns the variables’ maximum values:

    ```

    scaler = MaxAbsScaler().set_output(

    transform="pandas")

    scaler.fit(data)

    ```py

     2.  Now, let’s scale the variables by utilizing the trained scaler:

    ```

    data_scaled = scaler.transform(data)

    ```py

Note
`MaxAbsScaler ()` stores the maximum values in its `max_abs_` attribute.

1.  Let’s display the maximum values stored by the scaler:

    ```

    scaler.max_abs_

    array([ 7.,  6.,  2.,  2., 11.,  4.,  3.,  6., 52.,  2.])

    ```py

    To follow up, let’s plot the distributions of the original and scaled variables.

     2.  Let’s make a histogram with the bag of words before the scaling:

    ```

    data.hist(bins=20, figsize=(20, 20))

    plt.show()

    ```py

    In the following output, we see histograms with the number of times each word appears in a document:

![Figure 7.6 – Histograms with different word counts](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/600cec951cf34d229fa1413f8c2e8063~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=SzFp%2Bs9EiVuY8OtpehHk7XoR1nI%3D)

Figure 7.6 – Histograms with different word counts

1.  Now, let’s make a histogram with the scaled variables:

    ```

    data_scaled.hist(bins=20, figsize=(20, 20))

    plt.show()

    ```py

    In the following output, we can corroborate the change of scale of the variables, but their distribution shape remains the same:

![Figure 7.7 – Histograms of the word counts after the scaling](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c6a42764c1a448019d8e143c74c15a53~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=lOlf6TZUIvtuTCaeIUploZDdVnI%3D)

Figure 7.7 – Histograms of the word counts after the scaling
With scaling to the maximum absolute value, we linearly scale down the magnitude of the features.
How it works...
In this recipe, we scaled the sparse variables of a bag of words to their absolute maximum values by using `MaxAbsScaler()`. With `fit()`, the scaler learned the maximum absolute values for each variable and stored them in its `max_abs_` attribute. With `transform()`, the scaler divided the variables by their absolute maximum values, returning a `pandas` DataFrame.
Note
Remember that you can change the output container to a NumPy array or a `polars` DataFrame through the `set_output()` method of the scikit-learn library’s transformers.
There’s more...
If you want to center the variables’ distribution at `0` and then scale them to their absolute maximum, you can do so by combining the use of two scikit-learn transformers within a pipeline:

1.  Let’s import the required libraries, transformers, and functions:

    ```

    import pandas as pd

    from sklearn.datasets import fetch_california_housing

    from sklearn.model_selection import train_test_split

    from sklearn.preprocessing import (

    MaxAbsScaler, StandardScaler)

    from sklearn.pipeline import Pipeline

    ```py

     2.  Let’s load the California housing dataset and split it into train and test sets:

    ```

    X, y = fetch_california_housing(

    return_X_y=True, as_frame=True)

    X.drop( labels=[ "纬度",

    "经度"], axis=1, inplace=True)

    X_train, X_test, y_train, y_test = train_test_split(

    X, y, test_size=0.3, random_state=0)

    ```py

     3.  Let’s set up `StandardScaler()` from scikit-learn so that it learns and subtracts the mean but does not divide the result by the standard deviation:

    ```

    scaler_mean = StandardScaler(

    with_mean=True, with_std=False)

    ```py

     4.  Now, let’s set up `MaxAbsScaler()` with its default parameters:

    ```

    scaler_maxabs = MaxAbsScaler()

    ```py

     5.  Let’s include both scalers within a pipeline that returns pandas DataFrames:

    ```

    scaler = Pipeline([

    ("scaler_mean", scaler_mean),

    ("scaler_max", scaler_maxabs),

    ]).set_output(transform="pandas")

    ```py

     6.  Let’s fit the scalers to the train set so that they learn the required parameters:

    ```

    scaler.fit(X_train)

    ```py

     7.  Finally, let’s transform the train and test sets:

    ```

    X_train_scaled = scaler.transform(X_train)

    X_test_scaled = scaler.transform(X_test)

    ```py

    The pipeline applies `StandardScaler()` and `MaxAbsScaler()` in sequence to first remove the mean and then scale the resulting variables to their maximum values.

Scaling to vector unit length
Scaling to the vector unit length involves scaling individual observations (not features) to have a unit norm. Each sample (that is, each row of the data) is rescaled independently of other samples so that its norm equals one. Each row constitutes a **feature vector** containing the values of every variable for that row. Hence, with this scaling method, we rescale the feature vector.
The norm of a vector is a measure of its magnitude or length in a given space and it can be determined by using the Manhattan (*l1*) or the Euclidean (*l2*) distance. The Manhattan distance is given by the sum of the absolute components of the vector:
![<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow><mrow><mrow><mi>l</mi><mn>1</mn><mfenced open="(" close=")"><mi>x</mi></mfenced><mo>=</mo><mo>|</mo><msub><mi>x</mi><mn>1</mn></msub><mo>|</mo><mo>+</mo><mfenced open="|" close="|"><msub><mi>x</mi><mn>2</mn></msub></mfenced><mo>+</mo><mo>…</mo><mo>..</mo><mo>+</mo><mo>|</mo><msub><mi>x</mi><mi>n</mi></msub><mo>|</mo></mrow></mrow></mrow></math>](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/ca4fb9b988d34cbf97ce214a1df3bf84~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=lL62H5ESAerKoZ%2FKv5aVyOcnPs4%3D)
The Euclidean distance is given by the square root of the square sum of the component of the vector:
![<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow><mrow><mi>l</mi><mn>2</mn><mfenced open="(" close=")"><mi>x</mi></mfenced><mo>=</mo><msqrt><mrow><msubsup><mi>x</mi><mn>1</mn><mn>2</mn></msubsup><mo>+</mo><msubsup><mi>x</mi><mn>2</mn><mn>2</mn></msubsup><mo>+</mo><mo>…</mo><mo>+</mo><msubsup><mi>x</mi><mi>n</mi><mn>2</mn></msubsup></mrow></msqrt></mrow></mrow></math>](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c0e57e341f2a495bb72486d3aa67c2e7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=bFkV%2BJQ6SWp1qj1vjA5lj%2BZsYVs%3D)
Here, ![<math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mrow><msub><mi>x</mi><mn>1</mn></msub><mo>,</mo><msub><mi>x</mi><mn>2</mn></msub><mo>,</mo></mrow></mrow></math>](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/973da39c1ef34a85a9beac189e9c698c~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=qNR4DYlFRlC5D7KCa1dE2c5uiNw%3D)and ![<math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><msub><mi>x</mi><mi>n</mi></msub></mrow></math>](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/a60531bc9d314a0897d43601f98f7e07~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=9LJqkQH72aOCkXOKLY8qrbkOK%2FI%3D)are the values of variables *1*, *2*, and *n* for each observation. Scaling to unit norm consists of dividing each feature vector’s value by either *l1* or *l2*, so that after the scaling, the norm of the feature vector is *1*. To be clear, we divide each of ![<math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mrow><msub><mi>x</mi><mn>1</mn></msub><mo>,</mo><msub><mi>x</mi><mn>2</mn></msub><mo>,</mo></mrow></mrow></math>](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/1c4382849f85448aab377a01eb572e9a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=EgOzJ0aiDOx1%2FN65cyiL6pKdTqg%3D)and ![<math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mrow><msub><mi>x</mi><mi>n</mi></msub><mo>,</mo></mrow></mrow></math>](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6a7d66efe5024458bcd37c4b440209d5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=SUm8fX9ghjpib3uLajpcNx%2Bj9%2FM%3D)by *l1* or *l2*.
This scaling procedure changes the variables’ distribution, as illustrated in the following figure:
![Figure 7.8 – Distribution of a normal and skewed variable before and after scaling each observation’s feature vector to its norm](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/e54ef675dfb446c58338d6364f2179e7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772170292&x-signature=OE1OgNQyjYAKxC7RcniwyMyS%2Fro%3D)

Figure 7.8 – Distribution of a normal and skewed variable before and after scaling each observation’s feature vector to its norm
Note
This scaling technique scales each observation and not each variable. The scaling methods that we discussed so far in this chapter aimed at shifting and resetting the scale of the variables’ distribution. When we scale to the unit length, however, we normalize each observation individually, contemplating their values across all features.
Scaling to the unit norm can be used when utilizing kernels to quantify similarity for text classification and clustering. In this recipe, we will scale each observation’s feature vector to a unit length of `1` using scikit-learn.
How to do it...
To begin, we’ll import the required packages, load the dataset, and prepare the train and test sets:

1.  Let’s import the required Python packages, classes, and functions:

    ```

    import numpy as np

    import pandas as pd

    from sklearn.datasets import fetch_california_housing

    from sklearn.model_selection import train_test_split

    from sklearn.preprocessing import Normalizer

    ```py

     2.  Let’s load the California housing dataset into a `pandas` DataFrame:

    ```

    X, y = fetch_california_housing(

    return_X_y=True, as_frame=True)

    X.drop(labels=[

    "纬度", "经度"], axis=1, inplace=True)

    ```py

     3.  Let’s divide the data into train and test sets:

    ```

    X_train, X_test, y_train, y_test = train_test_split(

    X, y, test_size=0.3, random_state=0)

    ```py

     4.  Let’s set up the scikit-learn library’s `Normalizer()` transformer to scale each observation to the Manhattan distance or `l1`:

    ```

    scaler = Normalizer(norm='l1')

    ```py

Note
To normalize to the Euclidean distance, you need to set the norm to `l2` using `scaler =` `Normalizer(norm='l2')`.

1.  Let’s transform the train and test sets – that is, we’ll divide each observation’s feature vector by its norm:

    ```

    X_train_scaled = scaler.fit_transform(X_train)

    X_test_scaled = scaler.transform(X_test)

    ```py

    We can calculate the length (that is, the Manhattan distance of each observation’s feature vector) using `linalg()` from NumPy.

     2.  Let’s calculate the norm (Manhattan distance) before scaling the variables:

    ```

    np.round(np.linalg.norm(X_train, ord=1, axis=1), 1)

    ```py

    As expected, the norm of each observation varies:

    ```

    array([ 255.3,  889.1, 1421.7, ...,  744.6, 1099.5,

    1048.9])

    ```py

     3.  Let’s now calculate the norm after the scaling:

    ```

    np.round(np.linalg.norm(

    X_train_scaled, ord=1, axis=1), 1)

    ```py

Note
You need to set `ord=1` for the Manhattan distance and `ord=2` for the Euclidean distance as arguments of NumPy’s `linalg()`function, depending on whether you scaled the features to the `l1` or `l2` norm.
We see that the Manhattan distance of each feature vector is `1` after scaling:

array([1., 1., 1., ..., 1., 1., 1.])


 Based on the scikit-learn library’s documentation, this scaling method can be useful when using a quadratic form such as the dot-product or any other kernel to quantify the similarity of a pair of samples.
How it works...
In this recipe, we scaled the observations from the California housing dataset to their feature vector unit norm by utilizing the Manhattan or Euclidean distance. To scale the feature vectors, we created an instance of `Normalizer()` from scikit-learn and set the norm to `l1` for the Manhattan distance. For the Euclidean distance, we set the norm to `l2`. Then, we applied the `fit()` method, although there were no parameters to be learned, as this normalization procedure depends exclusively on the values of the features for each observation. Finally, with the `transform()` method, the scaler divided each observation’s feature vector by its norm. This returned a NumPy array with the scaled dataset. After the scaling, we used NumPy’s `linalg.norm` function to calculate the norm (`l1` and `l2`) of each vector to confirm that after the transformation, it was `1`.

第八章:创建新特征

向数据集中添加新特征可以帮助机器学习模型学习数据中的模式和重要细节。例如,在金融领域,可支配收入,即任何一个月的总收入减去获得债务,可能比仅仅的收入或获得债务对信用风险更为相关。同样,一个人在金融产品中的总获得债务,如车贷、房贷和信用卡,可能比单独考虑的任何债务对估计信用风险更为重要。在这些例子中,我们使用领域知识来构建新变量,这些变量是通过添加或减去现有特征创建的。

在某些情况下,一个变量可能没有与目标变量呈线性或单调关系,但多项式组合可能存在。例如,如果我们的变量与目标变量呈二次关系,<math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mrow><mi>y</mi><mo>=</mo><msup><mi>x</mi><mn>2</mn></msup></mrow></mrow></math>,我们可以通过平方原始变量将其转换为线性关系。我们还可以通过使用样条或决策树来转换预测变量,帮助线性模型更好地理解变量和目标之间的关系。

通过构建额外的特征来训练更简单的模型,例如线性或逻辑回归,其优势在于特征和模型都保持可解释性。我们可以向管理层、客户和监管机构解释驱动模型输出的原因,为我们的机器学习流程增加一层透明度。此外,简单的模型往往训练速度更快,部署和维护也更容易。

在本章中,我们将通过变换或结合变量使用数学函数、样条和决策树来创建新特征。

本章将涵盖以下食谱:

  • 使用数学函数组合特征

  • 将特征与参考变量进行比较

  • 执行多项式展开

  • 使用决策树组合特征

  • 从周期性变量创建周期特征

  • 创建样条特征

技术要求

在本章中,我们将使用 pandasnumpymatplotlibscikit-learnfeature-engine 这些 Python 库。

使用数学函数组合特征

通过结合现有变量和数学及统计函数可以创建新特征。以金融行业为例,我们可以通过汇总个人在单个金融产品中的债务(如车贷、房贷或信用卡债务)来计算一个人的总债务:

总债务 = 车贷债务 + 信用卡债务 + 房贷债务

我们还可以使用其他统计操作推导出其他有洞察力的特征。例如,我们可以确定客户在金融产品中的最大债务或用户在网站上的平均停留时间:

最大债务 = max(车贷余额, 信用卡余额, 按揭余额)

网站平均停留时间 = mean(主页停留时间, 关于页面停留时间, FAQ 页面停留时间)

在原则上,我们可以使用任何数学或统计运算来创建新的特征,例如乘积、平均值、标准差,或者最大或最小值。在这个食谱中,我们将使用 pandasfeature-engine 来实现这些数学运算。

注意

虽然,在食谱中,我们可以向您展示如何使用数学函数组合特征,但我们无法公正地展示在决定应用哪个函数时领域知识的运用,因为每个领域都有所不同。所以,我们将这部分留给你。

准备工作

在这个食谱中,我们将使用来自 scikit-learn 的乳腺癌数据集。特征是通过乳腺细胞的数字化图像计算得出的,描述了细胞核的平滑度、凹陷度、对称性和紧凑度等特征。每一行包含关于组织样本中细胞核形态的信息。目标变量表示组织样本是否对应于癌细胞。目标是根据细胞核的形态预测组织样本属于良性还是恶性的乳腺细胞。

为了熟悉数据集,请在 Jupyter 笔记本或 Python 控制台中运行以下命令:

from sklearn.datasets import load_breast_cancer
data = load_breast_cancer()
print(data.DESCR)

上述代码块应该打印出数据集的描述及其变量的解释。

如何做到这一点...

在这个食谱中,我们将通过使用多个数学运算来组合变量来创建新的特征:

  1. 让我们先加载必要的库、类和数据:

    import pandas as pd
    from feature_engine.creation import MathFeatures
    from sklearn.datasets import load_breast_cancer
    
  2. 接下来,将乳腺癌数据集加载到 pandas DataFrame 中:

    data = load_breast_cancer()
    df = pd.DataFrame(data.data,
        columns=data.feature_names)
    

    在以下代码行中,我们将通过使用多个数学运算来组合变量创建新的特征。

  3. 让我们先创建一个包含我们想要组合的特征子集的列表:

    features = [
        «mean smoothness",
        «mean compactness",
        «mean concavity",
        «mean concave points",
        «mean symmetry",
    ]
    

    步骤 3 中的特征代表了图像中细胞核的平均特征。获取所有检查特征的均值可能是有用的。

  4. 让我们计算特征的均值并显示结果特征:

    df["mean_features"] = df[features].mean(axis=1)
    df["mean_features"].head()
    

    下面的输出显示了 步骤 3 中特征的均值:

    0    0.21702
    1    0.10033
    2    0.16034
    3    0.20654
    4    0.14326
    Name: mean_features, dtype: float64
    
  5. 同样,为了捕捉细胞核的一般变异性,让我们确定平均特征的标准差,然后显示结果特征:

    df["std_features"] = df[features].std(axis=1)
    df["std_features"].head()
    

    下面的输出显示了 步骤 3 中特征的标准差:

    0    0.080321
    1    0.045671
    2    0.042333
    3    0.078097
    4    0.044402
    Name: std_features, dtype: float64
    

注意

当我们根据领域知识构建新的特征时,我们确切地知道我们想要如何组合变量。我们也可以通过多个运算组合特征,然后评估它们是否具有预测性,例如使用特征选择算法或从机器学习模型中推导特征重要性。

  1. 让我们创建一个包含我们想要使用的数学函数的列表来组合特征:

    math_func = [
        "sum", "prod", "mean", "std", "max", "min"]
    
  2. 现在,让我们应用 步骤 6 中的函数来组合 步骤 3 中的特征,将结果变量捕获到一个新的 DataFrame 中:

    df_t = df[features].agg(math_func, axis="columns")
    

    如果我们执行 df_t.head(),我们将看到包含新创建特征的 DataFrame:

图 8.1 – 包含新创建特征的 DataFrame

图 8.1 – 包含新创建特征的 DataFrame

注意

pandasagg 函数可以应用多个函数来组合特征。它可以接受一个包含函数名称的字符串列表,就像我们在 步骤 7 中做的那样;一个包含 NumPy 函数(如 np.log)的列表;以及你创建的 Python 函数。

我们可以使用 feature-engine 自动创建与使用 pandas 创建的相同特征。

  1. 让我们使用输出特征的名称创建一个列表:

    new_feature_names = [
        "sum_f", "prod_f", "mean_f",
        „std_f", „max_f", „min_f"]
    
  2. 让我们设置 MathFeatures() 来将 步骤 6 中的函数应用于 步骤 3 中的特征,使用 步骤 8 中的字符串来命名新特征:

    create = MathFeatures(
        variables=features,
        func=math_func,
        new_variables_names=new_feature_names,
    )
    
  3. 让我们将新特征添加到原始 DataFrame 中,将结果捕获到一个新变量中:

    df_t = create.fit_transform(df)
    

    我们可以通过执行 df_t[features + new_feature_names].head() 来显示输入和输出特征:

图 8.2 – 包含输入特征和新创建变量的 DataFrame

图 8.2 – 包含输入特征和新创建变量的 DataFrame

虽然 pandasagg 函数返回一个包含操作结果的 DataFrame,但 feature-engine 会更进一步,通过将新特征连接到原始 DataFrame 上。

它是如何工作的...

pandas 有许多内置操作,可以将数学和统计计算应用于一组变量。为了在数学上组合特征,我们首先创建了一个包含我们想要组合的特征名称的列表。然后,我们使用 pandasmean()std() 函数确定了这些特征的平均值和标准差。我们还可以应用 sum()prod()max()min() 方法中的任何一个,这些方法分别返回这些特征的总和、乘积、最大值和最小值。为了在列上执行这些操作,我们在方法中添加了 axis=1 参数。

使用 pandas 的 agg() 函数,我们可以同时应用多个数学函数。它接受一个字符串列表作为参数,对应于要应用的功能和函数应该应用的 axis,可以是 1(列)或 0(行)。因此,pandas 的 agg() 函数返回一个应用数学函数到特征组的 pandas DataFrame。

最后,我们通过结合变量和使用 feature-engine 创建了相同的特征。我们使用了 MathFeatures() 转换器,它接受要组合的特征和要应用的功能作为输入;它还提供了指示结果特征名称的选项。当我们使用 fit() 时,转换器没有学习参数,而是检查变量确实是数值的。transform() 方法触发了底层使用 pandas.agg,应用数学函数来创建新的变量。

参见

要了解更多关于 pandas 支持的数学运算,请访问 pandas.pydata.org/pandas-docs/stable/reference/frame.html#computations-descriptive-stats

要了解更多关于 pandas aggregate 的信息,请查看 pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.aggregate.html

比较特征与参考变量

在之前的菜谱 使用数学函数组合特征 中,我们通过将数学或统计函数(如总和或平均值)应用于一组变量来创建新特征。然而,一些数学运算(如减法或除法)是在特征之间进行的。这些操作对于推导比率(如 负债收入比)很有用:

负债收入比 = 总负债 / 总收入

这些操作也有助于计算差异,例如 可支配收入

可支配收入 = 收入 - 总负债

在这个菜谱中,我们将学习如何通过使用 pandasfeature-engine 对变量进行减法或除法来创建新特征。

注意

在这个菜谱中,我们将向您展示如何通过减法和除法创建特征。我们希望这些与金融部门相关的例子能对如何使用领域知识来决定要组合哪些特征以及如何进行一些启发。

如何做到这一点...

让我们先加载必要的 Python 库和来自 scikit-learn 的乳腺癌数据集:

  1. 加载必要的库、类和数据:

    import pandas as pd
    from feature_engine.creation import RelativeFeatures
    from sklearn.datasets import load_breast_cancer
    
  2. 将乳腺癌数据集加载到 pandas DataFrame 中:

    data = load_breast_cancer()
    df = pd.DataFrame(data.data,
        columns=data.feature_names)
    

    在乳腺癌数据集中,一些特征捕捉了乳腺细胞细胞核的最差和平均特征。例如,对于每个图像(即每行),我们都有观察到的所有核的最差紧密度和所有核的平均紧密度。一个捕捉最差值和平均值之间差异的特征可以预测恶性。

  3. 让我们捕捉两个特征之间的差异,即细胞核的 最差紧密度平均紧密度,并将它们存储在一个新变量中并显示其值:

    df["difference"] = df["worst compactness"].sub(
        df["mean compactness"])
    df["difference"].head()
    

    在以下输出中,我们可以看到这些特征值之间的差异:

    0    0.38800
    1    0.10796
    2    0.26460
    3    0.58240
    4    0.07220
    Name: difference, dtype: float64
    

注意

我们可以通过执行 df["difference"] = df["worst compactness"] - (``df["mean compactness"]) 来进行相同的计算。

同样,细胞核最坏和平均特征之间的比率可能表明恶性。

  1. 让我们创建一个新特征,该特征是核的最坏和平均半径之间的比率,然后显示其值:

    df["quotient"] = df["worst radius"].div(
        df["mean radius"])
    df["quotient"].head()
    

    在以下输出中,我们可以看到特征之间的比率值:

    0    1.410784
    1    1.214876
    2    1.197054
    3    1.305604
    4    1.110892
    Name: quotient, dtype: float64
    

注意

我们可以通过执行一个替代命令来计算比率,df["quotient"] = df["worst radius"] / (``df["mean radius"])

我们还可以捕获每个核形态特征与核的平均半径或平均面积之间的比率。让我们首先将这些变量的子集捕获到列表中。

  1. 让我们列出分子中的特征:

    features = [
        "mean smoothness",
        «mean compactness",
        "mean concavity",
        "mean symmetry"
    ]
    
  2. 让我们列出分母中的特征:

    reference = ["mean radius", "mean area"]
    

注意

我们可以通过在 pandas 中将 步骤 5 中的特征除以 步骤 6 中的某个特征来创建特征,通过执行 df[features].div(df["mean radius"])。对于减法,我们将执行 df[features].sub(df["mean radius"])

  1. 让我们设置 feature-engine 库的 RelativeFeatures(),使其从 步骤 5 的每个特征中减去或除以 步骤 6 的特征:

    creator = RelativeFeatures(
        variables=features,
        reference=reference,
        func=["sub", "div"],
    )
    

注意

步骤 5步骤 6 中减去特征在生物学上没有意义,但我们将这样做以演示 RelativeFeatures() 转换器的使用。

  1. 让我们将新特征添加到 DataFrame 中,并将结果捕获在一个新变量中:

    df_t = creator.fit_transform(df)
    
  2. 让我们将新特征的名称捕获到一个列表中:

    all_feat = creator.feature_names_in_
    new_features = [
        f for f in df_t.columns if f not in all_feat]
    

注意

feature_names_in_scikit-learnfeature-engine 转换器中的一个常见属性,它存储用于拟合转换器的 DataFrame 中的变量名称。换句话说,它存储了输入特征的名称。当使用 transform() 时,转换器会检查新输入数据集中的特征是否与训练期间使用的特征匹配。在 步骤 9 中,我们利用这个属性来查找在转换后添加到数据中的额外变量。

如果我们执行 print(new_features),我们将看到一个包含由 ReferenceFeatures() 创建的特征名称的列表。请注意,这些特征包含数学方程式左侧和右侧的变量,以及应用于它们的函数以创建新特征:

['mean smoothness_sub_mean radius',
'mean compactness_sub_mean radius',
'mean concavity_sub_mean radius',
'mean symmetry_sub_mean radius',
'mean smoothness_sub_mean area',
'mean compactness_sub_mean area',
'mean concavity_sub_mean area',
'mean symmetry_sub_mean area',
'mean smoothness_div_mean radius',
'mean compactness_div_mean radius',
'mean concavity_div_mean radius',
'mean symmetry_div_mean radius',
'mean smoothness_div_mean area',
'mean compactness_div_mean area',
'mean concavity_div_mean area',
'mean symmetry_div_mean area']

最后,我们可以通过执行 df_t[new_features].head() 来显示结果变量的前五行:

图 8.3 – 包含新创建特征的 DataFrame

图 8.3 – 包含新创建特征的 DataFrame

feature-engine 将新特征作为原始 DataFrame 右侧的列添加,并自动将这些特征的变量名添加到其中。通过这样做,feature-engine 自动化了我们本应使用 pandas 做的大量手动工作。

它是如何工作的...

pandas有许多内置操作来比较一个特征或一组特征与一个参考变量。在这个菜谱中,我们使用了 pandas 的sub()div()函数来确定两个变量或一组变量与一个参考特征之间的差异或比率。

要从一个变量中减去另一个变量,我们对第一个变量应用了sub()函数到一个pandas序列上,将第二个变量的pandas序列作为参数传递给sub()函数。这个操作返回了一个第三个pandas序列,包含第一个和第二个变量之间的差值。要除以另一个变量,我们使用了div()函数,它的工作方式与sub()相同——即,它将左侧的变量除以div()函数作为参数传递的变量。

然后,我们通过使用Feature-engineReferenceFeatures()函数,自动将几个变量与两个参考变量通过减法或除法组合起来。ReferenceFeatures()转换器接受要组合的变量、参考变量以及用于组合它们的函数。当使用fit()时,转换器不会学习参数,而是检查变量是否为数值。执行transform()会将新特征添加到 DataFrame 中。

注意

ReferenceFeatures()还可以为与第二组参考变量相关的一组变量添加、乘法、取模或求幂。您可以在其文档中了解更多信息:feature-engine.readthedocs.io/en/latest/api_doc/creation/RelativeFeatures.html

参见

要了解更多关于pandas支持的二进制操作,请访问pandas.pydata.org/pandas-docs/stable/reference/frame.html#binary-operator-functions

执行多项式展开

简单模型,如线性回归和逻辑回归,如果我们向它们提供正确的特征,可以捕捉到复杂的模式。有时,我们可以通过将数据集中的变量与自身或其他变量组合来创建强大的特征。例如,在下面的图中,我们可以看到目标y与变量x有二次关系,如图左侧面板所示,线性模型无法准确捕捉这种关系:

图 8.4 – 一个线性模型拟合预测目标 y,从特征 x,x 与目标有二次关系,在平方 x 之前和之后。在左侧面板:模型使用原始变量提供较差的拟合;在右侧面板,模型基于原始变量的平方提供更好的拟合

图 8.4 – 一个线性模型拟合以预测目标,y,从特征,x,该特征与目标之间存在二次关系,在平方 x 之前和之后。在左侧面板:模型通过使用原始变量提供较差的拟合;在右侧面板,模型基于原始变量的平方提供更好的拟合

此线性模型在平方x之前和之后都与目标存在二次关系。然而,如果我们平方x,换句话说,如果我们创建特征的二次多项式,线性模型可以准确地从x的平方预测目标y,正如我们在右侧面板中看到的那样。

另一个经典例子是,一个简单的特征可以使一个简单的模型,如逻辑回归,理解数据中的潜在关系,这就是XOR情况。在以下图表的左侧面板中,我们看到目标类别是如何分布在x1x2的值上的(类别用不同的颜色阴影突出显示):

图 8.5 – XOR 关系的插图以及如何通过组合特征实现完整的类别分离

图 8.5 – XOR 关系的插图以及如何通过组合特征实现完整的类别分离

如果两个特征都是正的,或者两个特征都是负的,那么类别是 1,但如果特征具有不同的符号,那么类别是 0(左侧面板)。逻辑回归将无法从每个单独的特征中识别出这种模式,因为我们可以在中间面板中看到,特征值之间存在显著的类别重叠 – 在这种情况下,x1。然而,将 x1 乘以 x2 创建了一个特征,这使得逻辑回归能够准确地预测类别,因为 x3,正如我们在右侧面板中看到的那样,允许类别被清楚地分离。

使用类似的逻辑,相同或不同变量的多项式组合可以返回新的变量,这些变量传达了额外的信息并捕捉了特征交互,从而为线性模型提供了有用的输入。在大型数据集中,分析每个可能的变量组合并不总是可能的。但我们可以使用例如scikit-learn自动创建多个多项式变量,并让模型决定哪些变量是有用的。在本菜谱中,我们将学习如何使用 scikit-learn 通过多项式组合创建多个特征。

准备就绪

多项式展开用于自动化新特征的创建,捕捉特征交互,以及原始变量与目标之间的潜在非线性关系。要创建多项式特征,我们需要确定哪些特征要组合以及使用哪个多项式度数。

注意

在确定要组合的特征或多项式组合的次数并不容易,记住高次多项式将导致大量新特征的生成,可能会导致过拟合。一般来说,我们保持次数较低,最多为 2 或 3。

scikit-learn中的PolynomialFeatures()转换器会自动创建特征的多项式组合,其次数小于或等于用户指定的次数。

为了轻松跟进这个配方,让我们首先了解当使用PolynomialFeatures()创建三个变量的二次和三次多项式组合时的输出。

三个变量(abc)的二次多项式组合将返回以下新特征:

1, a, b, c, ab, ac, bc, a2, b2, c2

从前面的特征来看,abc是原始变量;abacbc是这些特征的乘积;而a2b2c2是原始特征的平方值。PolynomialFeatures()还会返回一个偏置项1,在创建特征时我们可能不会包含它。

注意

生成特征——abac,和bc——被称为交互2 度的特征交互。度数反映了组合的变量数量。结果组合了最多两个变量,因为我们指定了二次多项式为允许的最大组合。

三个变量(abc)的三次多项式组合将返回以下新特征:

1, a, b, c, ab, ac, bc, abc, a2b, a2c, b2a, b2c, c2a, c2b, a3, b3, c3

在返回的特征中,除了二次多项式组合返回的特征外,我们现在还有特征自身的三次组合(a3b3,和c3),每个特征与第二个特征线性组合的平方值(a2ba2cb2ab2cc2a,和c2b),以及三个特征的乘积(abc)。注意我们包含了所有可能的 1 度、2 度和 3 度交互以及偏置项1

现在我们已经了解了scikit-learn实现的多项式展开的输出,让我们进入配方。

如何做到这一点...

在这个配方中,我们将使用一个玩具数据集通过多项式展开来创建特征,以便熟悉生成的变量。使用真实数据集的多项式展开创建特征与我们在本配方中将要讨论的相同:

  1. 让我们导入所需的库、类和数据:

    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    from sklearn import set_config
    from sklearn.preprocessing import PolynomialFeatures
    
  2. 让我们设置scikit-learn库的set_output API 全局,以便所有转换器在transform()方法的输出结果中返回一个 DataFrame:

    set_config(transform_output="pandas")
    
  3. 让我们创建一个包含一个变量的 DataFrame,其值为 1 到 10:

    df = pd.DataFrame(np.linspace(
        0, 10, 11), columns=["var"])
    
  4. 让我们设置PolynomialFeatures()以创建单变量的所有可能组合,直到三次多项式,并从结果中排除偏置项——也就是说,我们将排除值1

    poly = PolynomialFeatures(
        degree=3,
        interaction_only=False,
        include_bias=False)
    
  5. 现在,让我们创建多项式组合:

    dft = poly.fit_transform(df)
    

    如果我们执行 dft,我们将看到一个包含原始特征的 DataFrame,其后是其值的平方,然后是其值的立方:

图 8.6 – 单个变量的三次多项式展开的 DataFrame

图 8.6 – 单个变量的三次多项式展开的 DataFrame

如果 PolynomialFeatures() 返回一个 NumPy 数组而不是 DataFrame,并且你想获得数组中特征的名称,你可以通过执行 poly.get_feature_names_out() 来实现,它返回 array(['var', 'var²', 'var³'], dtype=object)

  1. 现在,让我们将新的特征值与原始变量进行绘图:

    dft = pd.DataFrame(
        dft, columns=poly.get_feature_names_out())
    plt.plot(df["var"], dft)
    plt.legend(dft.columns)
    plt.xlabel("original variable")
    plt.ylabel("new variables")
    plt.show()
    

    在以下图中,我们可以看到多项式特征与原始变量之间的关系:

图 8.7 – 多项式展开产生的特征与原始变量之间的关系

图 8.7 – 多项式展开产生的特征与原始变量之间的关系

  1. 让我们在玩具数据集中添加两个额外的变量,其值从 1 到 10:

    df["col"] = np.linspace(0, 5, 11)
    df["feat"] = np.linspace(0, 5, 11)
    
  2. 接下来,让我们将数据集中的三个特征与多项式展开到二次方,但这次,我们只返回由至少两个不同变量组合产生的特征,即交互特征:

    poly = PolynomialFeatures(
        degree=2, interaction_only=True,
        include_bias=False)
    dft = poly.fit_transform(df)
    

    如果我们执行 dft,我们将看到多项式展开产生的特征,这些特征包含原始特征,以及三个变量的所有可能组合,但没有二次项,因为我们设置转换器只返回特征之间的交互:

图 8.8 – 使用多项式展开创建特征的结果,仅保留变量之间的交互

图 8.8 – 使用多项式展开创建特征的结果,仅保留变量之间的交互

注意

尝试创建特征的第三次方多项式组合,只返回交互或所有可能特征,以更好地理解 PolynomialFeatures() 的输出。

通过这样,我们已经学会了如何通过将现有变量与自身或其他特征结合来创建新特征。使用真实数据集通过多项式展开创建特征在本质上是一致的。

如果你只想组合特征的一个子集,你可以通过使用ColumnTransformer()来选择要组合的特征,就像我们在本食谱后面的“还有更多...”部分中将要展示的那样,或者通过使用来自feature-engineSklearnTransformerWrapper(),正如你在附带的 GitHub 仓库中可以看到的那样:github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch08-creation/Recipe3-PolynomialExpansion.ipynb

它是如何工作的...

在这个食谱中,我们通过使用特征与其自身或三个变量之间的多项式组合来创建特征。为了创建这些多项式特征,我们使用了scikit-learn中的PolynomialFeatures()。默认情况下,PolynomialFeatures()生成一个新的特征矩阵,该矩阵包含数据中所有特征的所有多项式组合,其度数小于或等于用户指定的degree。通过将degree设置为3,我们创建了所有可能的三次或更小的多项式组合。为了保留特征与其自身的组合,我们将interaction_only参数设置为False。为了避免返回偏差项,我们将include_bias参数设置为False

注意

interaction_only参数设置为True仅返回交互项 – 即由两个或更多变量的组合产生的变量。

fit()方法根据指定的参数确定了所有可能的特征组合。在这个阶段,转换器没有执行实际的数学计算。transform()方法使用特征执行数学计算以创建新的变量。通过get_feature_names()方法,我们可以识别展开的项 – 即每个新特征是如何计算的。

步骤 2中,我们将scikit-learn库的set_output API 的pandas DataFrame 设置为transform()方法的结果。scikit-learn 转换器默认返回NumPy数组。新的set_output API 允许我们将结果容器的类型更改为pandaspolars DataFrame。每次设置转换器时,我们都可以单独设置输出 – 例如,使用poly = PolynomialFeatures().set_output(transform="pandas")。或者,就像在这个食谱中做的那样,我们可以设置全局配置,然后每次设置新的转换器时,它将返回一个pandas DataFrame。

还有更多...

让我们在乳腺癌数据集的变量子集上执行多项式展开来创建特征:

  1. 首先,导入必要的库、类和数据:

    import pandas as pd
    from sklearn.datasets import load_breast_cancer
    from sklearn.compose import ColumnTransformer
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import PolynomialFeatures
    
  2. 然后,加载数据并将其分为训练集和测试集:

    data = load_breast_cancer()
    df = pd.DataFrame(data.data,
        columns=data.feature_names)
    X_train, X_test, y_train, y_test = train_test_split(
        df, data.target, test_size=0.3, random_state=0
    )
    
  3. 创建一个要组合的特征列表:

    features = [
        "mean smoothness",
        "mean compactness",
        "mean concavity"]
    
  4. 设置PolynomialFeatures()以创建所有可能的三次或更小的组合:

    poly = PolynomialFeatures(
        degree=3,
        interaction_only=False,
        include_bias=False)
    
  5. 设置列转换器以仅从步骤 3中指定的列创建特征:

    ct = ColumnTransformer([("poly", poly, features)])
    
  6. 创建多项式特征:

    train_t = ct.fit_transform(X_train)
    test_t = ct.transform(X_test)
    

就这样。通过执行ct.get_feature_names_out(),我们获得了新特征的名称。

注意

ColumnTransformer()poly这个词添加到结果变量中,这是我们在步骤 5中给ColumnTransformer()步骤取的名字。我不是特别喜欢这种行为,因为它使得数据分析变得更难,因为你需要跟踪变量名的变化。为了避免变量名变化,你可以使用feature-engineSklearnTransformerWrapper()

将特征与决策树相结合

在 2009 年知识发现与数据挖掘(KDD)竞赛的获胜方案中,作者通过决策树将两个或多个变量组合起来创建新特征。在检查变量时,他们注意到一些特征与目标有很高的互信息,但相关性较低,这表明与目标的关系不是线性的。虽然这些特征在基于树的算法中使用时是预测性的,但线性模型无法利用它们。因此,为了在线性模型中使用这些特征,他们用训练在单个特征或两个或三个变量组合上的决策树输出的特征替换了这些特征,从而返回了与目标具有单调关系的新的特征。

简而言之,将特征与决策树相结合对于创建与目标具有单调关系的特征是有用的,这对于使用线性模型进行准确预测是有用的。该过程包括使用特征子集训练决策树——通常是每次一个、两个或三个——然后使用树的预测作为新的特征。

注意

你可以在这篇文章中找到关于此过程和 2009 年 KDD 数据竞赛整体获胜解决方案的更多详细信息:proceedings.mlr.press/v7/niculescu09/niculescu09.pdf

好消息是,我们可以使用feature-engine自动化使用树的特性创建,在这个菜谱中,我们将学习如何做到这一点。

如何操作...

在这个菜谱中,我们将使用加利福尼亚住房数据集将特征与决策树相结合:

  1. 首先,我们导入pandas以及所需的函数、类和数据集:

    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    from sklearn.datasets import fetch_california_housing
    from sklearn.model_selection import train_test_split
    from feature_engine.creation, import DecisionTreeFeatures
    
  2. 让我们将加利福尼亚住房数据集加载到pandas DataFrame 中,并删除LatitudeLongitude变量:

    X, y = fetch_california_housing(
        return_X_y=True, as_frame=True)
    X.drop(labels=[
        "Latitude", "Longitude"], axis=1, inplace=True)
    
  3. 将数据集分为训练集和测试集:

    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.3, random_state=0)
    
  4. 检查特征与目标之间的皮尔逊相关系数,这是线性关系的度量:

    for var in X_train.columns:
        pearson = np.corrcoef(X_train[var], y_train)[0, 1]
        pearson = np.round(pearson, 2)
        print(
            f"corr {var} vs target: {pearson}")
    

    在以下输出中,我们可以看到,除了MedInc之外,大多数变量与目标之间没有显示出强烈的线性关系;相关系数小于 0.5:

    corr MedInc vs target: 0.69
    corr HouseAge vs target: 0.1
    corr AveRooms vs target: 0.16
    corr AveBedrms vs target: -0.05
    corr Population vs target: -0.03
    feature-engine library’s DecisionTreeFeatures() selects the best tree by using cross-validation.
    
  5. 创建一个超参数网格以优化每个决策树:

    param_grid = {"max_depth": [2, 3, 4, None]}
    

    feature-engine库的DecisionTreeFeatures()允许我们添加由在单个或多个特征上训练的决策树预测出的特征。我们可以以多种方式指导转换器组合特征。我们将从创建两个变量之间的所有可能组合开始。

  6. 列出我们想要用作输入的两个功能:

    variables = ["AveRooms", "AveBedrms"]
    
  7. DecisionTreeFeatures()设置为创建来自步骤 6中特征的所有可能组合:

    dtf = DecisionTreeFeatures(
        variables=variables,
        features_to_combine=None,
        cv=5,
        param_grid=param_grid,
        scoring="neg_mean_squared_error",
        regression=True,
    )
    

注意

我们将regression设置为True,因为在这个数据集中的目标是连续的。如果你有一个二元目标或正在执行分类,请将其设置为False。确保选择一个适合你模型的评估指标(scoring)。

  1. 拟合转换器,以便它在输入特征上训练决策树:

    dtf.fit(X_train, y_train)
    
  2. 如果你想知道哪些特征被用于训练决策树,你可以像这样检查它们:

    dtf.input_features_
    

    在以下输出中,我们可以看到DecisionTreeFeatures()已经训练了三个决策树——两个使用单个特征AveRoomsAveBedrms,一个使用这两个特征:

    ['AveRooms', 'AveBedrms', ['AveRooms', 'AveBedrms']]
    

注意

DecisionTreeFeatures()还存储决策树。你可以通过执行dtf.estimators_来检查它们。

  1. 现在,将这些特征添加到训练和测试集中:

    train_t = dtf.transform(X_train)
    test_t = dtf.transform(X_test)
    
  2. 列出新特征的名称(转换器将单词tree附加到特征名称上):

    tree_features = [
        var for var in test_t.columns if "tree" in var ]
    
  3. 最后,显示添加到测试集中的特征:

    test_t[tree_features].head()
    

    在以下输出中,我们可以看到由步骤 8中训练的决策树产生的新特征的前五行:

    图 8.9 – 包含从决策树中派生出的特征的测试集的一部分

图 8.9 – 包含从决策树中派生出的特征的测试集的一部分

  1. 为了检查这种转换的力量,计算新特征与目标之间的皮尔逊相关系数:

    for var in tree_features:
        pearson = np.corrcoef(test_t[var], y_test)[0, 1]
        pearson = np.round(pearson, 2)
        print(
            f"corr {var} vs target: {pearson}")
    

    在以下输出中,我们可以看到新变量与目标之间的相关性大于原始特征显示的相关性(将这些值与步骤 4中的值进行比较):

    corr tree(AveRooms) vs target: 0.37
    corr tree(AveBedrms) vs target: 0.12
    corr tree(['AveRooms', 'AveBedrms']) vs target: 0.47
    

    如果你想要组合特定的特征而不是获取变量之间的所有可能组合,你可以通过指定元组中的输入特征来实现。

  2. 创建一个包含我们想要用作决策树输入的不同特征的元组元组:

    features = (('Population'), ('Population','AveOccup'),
        ('Population', 'AveOccup', 'HouseAge'))
    
  3. 现在,我们需要将这些元组传递给DecisionTreeFeatures()features_to_combine参数:

    dtf = DecisionTreeFeatures(
        variables=None,
        features_to_combine=features,
        cv=5,
        param_grid=param_grid,
        scoring="neg_mean_squared_error"
    )
    dtf.fit(X_train, y_train)
    
  4. 我们在上一步骤中拟合了转换器,因此我们可以继续将特征添加到训练和测试集中:

    train_t = dtf.transform(X_train)
    test_t = dtf.transform(X_test)
    
  5. 显示新功能:

    tree_features = [
        var for var in test_t.columns if "tree" in var]
    test_t[tree_features].head()
    

    在以下输出中,我们可以看到来自测试集中决策树预测的新特征:

    图 8.10 – 包含从决策树中派生出的特征的测试集的一部分

图 8.10 – 包含从决策树中提取的特征的测试集的一部分

为了总结这个菜谱,我们将比较使用原始特征训练的 Lasso 线性回归模型和使用从决策树中提取的特征训练的模型的性能。

  1. scikit-learn导入Lassocross_validate函数:

    from sklearn.linear_model import Lasso
    from sklearn.model_selection import cross_validate
    
  2. 设置 Lasso 回归模型:

    lasso = Lasso(random_state=0, alpha=0.0001)
    
  3. 使用交叉验证和原始数据训练和评估模型,然后打印出结果的r-squared:

    cv_results = cross_validate(lasso, X_train, y_train,
        cv=3)
    mean = cv_results['test_score'].mean()
    std = cv_results['test_score'].std()
    print(f"Results: {mean} +/- {std}")
    

    在下面的输出中,我们可以看到使用原始特征训练的 Lasso 回归模型的r-square 值:

    Results: 0.5480403481478856 +/- 0.004214649109293269
    
  4. 最后,使用从决策树中提取的特征训练 Lasso 回归模型,并使用交叉验证进行评估:

    variables = ["AveRooms", "AveBedrms", "Population"]
    train_t = train_t.drop(variables, axis=1)
    cv_results = cross_validate(lasso, train_t, y_train,
        cv=3)
    mean = cv_results['test_score'].mean()
    std = cv_results['test_score'].std()
    print(f"Results: {mean} +/- {std}")
    

    在下面的输出中,我们可以看到基于树衍生特征训练的 Lasso 回归模型的性能更好;其r-square 值大于步骤 20中的值:

    Results: 0.5800993721099441 +/- 0.002845475651622909
    

我希望我已经给你展示了结合决策树和特征以及如何使用feature-engine做到这一点的力量。

它是如何工作的...

在这个菜谱中,我们基于在一个或多个变量上训练的决策树的预测创建了新的特征。我们使用Feature-engine中的DecisionTreeFeatures()来自动化使用交叉验证和超参数优化训练决策树的过程。

DecisionTreeFeatures()在底层使用网格搜索来训练决策树。因此,你可以传递一个超参数网格来优化树,或者转换器将仅优化深度,这在任何情况下都是决策树中最重要的参数。你还可以通过scoring参数更改你想要优化的度量,并通过cv参数更改你想要使用的交叉验证方案。

DecisionTreeFeatures()最令人兴奋的特性是其推断特征组合以创建树衍生特征的能力,这通过features_to_combine参数进行调节。如果你向这个参数传递一个整数——比如说,例如3DecisionTreeFeatures()将创建所有可能的 1、2 和 3 个特征的组合,并使用这些组合来训练决策树。你也可以传递一个整数的列表——比如说,[2,3]——在这种情况下,DecisionTreeFeatures()将创建所有可能的 2 和 3 个特征的组合。你还可以通过传递元组中的特征组合来指定你想要组合的特征以及如何组合,就像我们在步骤 14中所做的那样。

使用fit()DecisionTreeFeatures()找到特征组合并训练决策树。使用transform()DecisionTreeFeatures()将决策树产生的特征添加到 DataFrame 中。

注意

如果你正在训练回归或多类分类,新特征将是连续目标的预测或类别。如果你正在训练二元分类模型,新特征将来自类别 1 的概率。

在添加新特征后,我们通过分析皮尔逊相关系数来比较它们与目标之间的关系,该系数返回线性关联的度量。我们发现从树中派生的特征具有更大的相关系数。

参见

如果你想了解更多关于互信息是什么以及如何计算它的信息,请查看这篇文章:www.blog.trainindata.com/mutual-information-with-python/.

从周期性变量创建周期性特征

一些特征是周期性的——例如,一天中的小时,一年中的月份,一周中的天数。它们都从一个特定的值(比如说,一月)开始,上升到另一个特定的值(比如说,十二月),然后从头开始。一些特征是数字的,比如小时,而一些可以用数字表示,比如月份,其值为 1 到 12。然而,这种数字表示并没有捕捉到变量的周期性或循环性质。例如,十二月(12)比六月(6)更接近一月(1);然而,这种关系并没有被特征的数值表示所捕捉。但如果我们用正弦和余弦函数,这两种自然周期函数来转换这些变量,我们就可以改变它。

使用正弦和余弦函数对周期性特征进行编码允许线性模型利用特征的周期性并减少其建模误差。在本食谱中,我们将从捕获时间周期性的周期性变量中创建新特征。

准备工作

三角函数,如正弦和余弦,是周期性的,其值在每个 2π周期内循环于-1 和 1 之间,如下所示:

图 8.11 – 正弦和余弦函数

图 8.11 – 正弦和余弦函数

我们可以通过在将变量值归一化到 0 和 2π之间后应用三角变换来捕捉周期性变量的周期性。

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow><mrow><mi>sin</mi><mfenced open="(" close=")"><mi>x</mi></mfenced><mo>=</mo><mi>sin</mi><mrow><mrow><mo>(</mo><mn>2</mn><mi>π</mi><mfrac><mi>X</mi><msub><mi>X</mi><mrow><mi>m</mi><mi>a</mi><mi>x</mi></mrow></msub></mfrac><mo>)</mo></mrow></mrow></mrow></mrow></math>

将变量的值除以其最大值将使其在 0 和 1 之间归一化(假设最小值为 0),然后乘以 2π将变量重新缩放到 0 和 2π之间。

我们应该使用正弦函数吗?还是应该使用余弦函数?问题是,我们需要两者并用以无歧义地编码变量的所有值。由于正弦和余弦函数在 0 和 1 之间循环,它们将对于多个 x 值取 0。例如,0 的正弦值为 0,π的正弦值也为 0。所以,如果我们只用正弦函数来编码变量,我们就无法再区分 0 和π的值了。然而,由于正弦和余弦函数相位不同,0 的余弦值为 1,而π的余弦值为-1。因此,通过使用两个函数来编码变量,我们现在能够区分 0 和 1,正弦函数和余弦函数分别以(0,1)和(0,-1)作为值。

如何做到这一点...

在这个菜谱中,我们将首先使用正弦和余弦将玩具 DataFrame 中的hour变量进行变换,以了解新的变量表示。然后,我们将使用feature-engine自动化从多个周期性变量中创建特征:

  1. 首先导入必要的库:

    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    
  2. 创建一个包含一个变量 – hour – 且值在 0 到 23 之间的玩具 DataFrame:

    df = pd.DataFrame([i for i in range(24)],
        columns=["hour"])
    
  3. 接下来,在将变量值归一化到 0 和 2π之间后,使用正弦和余弦变换创建两个特征:

    df["hour_sin"] = np.sin(
        df["hour"] / df["hour"].max() * 2 * np.pi)
    df["hour_cos"] = np.cos(
        df["hour"] / df["hour"].max() * 2 * np.pi)
    

    如果我们执行df.head(),我们将看到原始特征和新特征:

图 8.12 – 包含小时变量和通过正弦和余弦变换获得的新特征的 DataFrame

图 8.12 – 包含小时变量和通过正弦和余弦变换获得的新特征的 DataFrame

  1. 在小时和其正弦变换值之间制作散点图:

    plt.scatter(df["hour"], df["hour_sin"])
    plt.ylabel("Sine of hour")
    plt.xlabel("Hour")
    plt.title("Sine transformation")
    

    在下面的图中,我们可以看到小时值的范围在-1 和 1 之间,就像变换后的正弦函数一样:

图 8.13 – 小时与其正弦变换值的散点图

图 8.13 – 小时与其正弦变换值的散点图

  1. 现在,在小时和其余弦变换之间制作散点图:

    plt.scatter(df["hour"], df["hour_cos"])
    plt.ylabel("Cosine of hour")
    plt.xlabel("Hour")
    plt.title("Cosine transformation")
    

    在下面的图中,我们可以看到小时值的范围在-1 和 1 之间,就像变换后的余弦函数一样:

图 8.14 – 小时与其余弦变换值的散点图

图 8.14 – 小时与其余弦变换值的散点图

最后,我们可以重建小时的周期性,现在它被两个新的特征所捕捉。

  1. 绘制正弦值与小时余弦值的对比图,并使用颜色图叠加小时的原值:

    fig, ax = plt.subplots(figsize=(7, 5))
    sp = ax.scatter(
        df["hour_sin"], df["hour_cos"], c=df["hour"])
    ax.set(
        xlabel="sin(hour)",
        ylabel="cos(hour)",
    )
    _ = fig.colorbar(sp)
    

    在下面的图中,我们可以看到小时的两个三角变换如何反映了小时的周期性,在一张让我们想起钟表的图中:

图 8.15 – 小时的三角变换散点图

图 8.15 – 小时的三角变换散点图

注意

这个图表的代码实现和思路来源于 scikit-learn 的文档:scikit-learn.org/stable/auto_examples/applications/plot_cyclical_feature_engineering.html#trigonometric-features

现在我们已经了解了变换的性质和效果,让我们使用正弦和余弦变换自动从多个变量中创建新特征。我们将使用feature-engine库的CyclicalFeatures()

  1. 导入CyclicalFeatures()

    from feature_engine.creation import CyclicalFeatures
    
  2. 让我们创建一个包含hourmonthweek变量的玩具 DataFrame,这些变量的值分别在 0 到 23、1 到 12 和 0 到 6 之间:

    df = pd.DataFrame()
    df["hour"] = pd.Series([i for i in range(24)])
    df["month"] = pd.Series([i for i in range(1, 13)]*2)
    df["week"] = pd.Series([i for i in range(7)]*4)
    

    如果我们执行df.head(),我们将看到玩具 DataFrame 的前五行:

图 8.16 – 具有三个周期特征的玩具 DataFrame

图 8.16 – 具有三个周期特征的玩具 DataFrame

  1. 设置转换器以从这些变量中创建正弦和余弦特征:

    cyclic = CyclicalFeatures(
        variables=None,
        drop_original=False,
    )
    

注意

通过将variables设置为NoneCyclicalFeatures()将从所有数值变量中创建三角特征。要创建变量子集的三角特征,我们可以将变量名列表传递给variables参数。在创建周期特征后,我们可以使用drop_original参数保留或删除原始变量。

  1. 最后,将特征添加到 DataFrame 中,并将结果捕获在新变量中:

    dft = cyclic.fit_transform(df)
    

    如果我们执行dft.head(),我们将看到原始特征和新特征:

图 8.17 – 具有周期特征以及通过正弦和余弦函数创建的特征的 DataFrame

图 8.17 – 具有周期特征以及通过正弦和余弦函数创建的特征的 DataFrame

就这样 – 我们通过使用正弦和余弦变换自动从多个变量中创建特征,并将它们直接添加到原始 DataFrame 中。

它是如何工作的…

在这个菜谱中,我们使用正弦和余弦函数从变量的归一化值中获取值来编码周期特征。首先,我们将变量值归一化到 0 和 2π之间。为此,我们用pandas.max()获取变量最大值,将变量值除以变量最大值,以将变量缩放到 0 和 1 之间。然后,我们使用numpy.pi将这些值乘以 2π。最后,我们使用np.sinnp.cos分别应用正弦和余弦变换。

为了自动化对多个变量的此过程,我们使用了Feature-engine库的CyclicalFeatures()。通过fit(),转换器学习了每个变量的最大值,通过transform(),它将正弦和余弦变换产生的特征添加到 DataFrame 中。

注意

理论上,为了应用正弦和余弦变换,我们需要将原始变量缩放到 0 到 1 之间。如果最小值是 0,则除以变量的最大值只会得到这种缩放。scikit-learn 的文档和Feature-engine当前的实现将变量除以其最大值(或任意周期),并没有过多关注变量是否从 0 开始。在实践中,如果你将小时特征除以 23 或 24,或者将月份特征除以 12 或 11,你不会在结果变量中看到很大的差异。目前正在讨论是否应该更新 Feature-engine 的实现,因此默认行为可能会在本书出版时发生变化。有关更多详细信息,请参阅文档。

创建样条特征

线性模型期望预测变量和目标之间存在线性关系。然而,如果我们首先转换特征,我们可以使用线性模型来模拟非线性效应。在“执行多项式展开”的食谱中,我们看到了如何通过创建多项式函数的特征来揭示线性模式。在本食谱中,我们将讨论样条的使用。

样条用于在数学上再现灵活的形状。它们由分段低次多项式函数组成。要创建样条,我们必须在x的几个值上放置节点。这些节点表示函数片段的连接点。然后,我们在两个连续节点之间拟合低次多项式。

有几种样条类型,例如平滑样条、回归样条和 B 样条。scikit-learn 支持使用 B 样条来创建特征。根据多项式度和节点数来拟合样条值,对于特定变量的过程超出了本食谱的范围。有关更多详细信息,请参阅本食谱的“也见”部分中的资源。在本食谱中,我们将了解样条是什么以及我们如何使用它们来提高线性模型的表现。

准备工作

让我们了解样条是什么。在下面的图中,左侧我们可以看到一个一阶样条。它由两个线性片段组成 – 一个从 2.5 到 5,另一个从 5 到 7.5。有三个节点 – 2.5,5 和 7.5。在 2.5 和 7.5 之间的区间外,样条取值为 0。这是样条的特征;它们只在某些值之间为非负。在图的右侧面板中,我们可以看到三个一阶样条。我们可以通过引入更多的节点来构建我们想要的任意数量的样条:

图 8.18 – 一阶样条

图 8.18 – 一阶样条

在下面的图中,左侧我们可以看到一个二次样条,也称为二阶样条。它基于四个相邻的节点 – 0,2.5,5 和 7.5。在图的右侧,我们可以看到几个二阶样条:

图 8.19 – 二次样条

图 8.19 – 二次样条

我们可以使用样条来建模非线性函数,我们将在下一节中学习如何做到这一点。

如何做到这一点…

在这个菜谱中,我们将使用样条来建模正弦函数。一旦我们了解样条是什么以及我们如何通过线性模型拟合非线性关系,我们将在实际数据集中使用样条进行回归:

注意

将样条建模正弦函数的想法来自 scikit-learn 的文档:scikit-learn.org/stable/auto_examples/linear_model/plot_polynomial_interpolation.html

  1. 让我们先导入必要的库和类:

    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    from sklearn.linear_model import Ridge
    from sklearn.preprocessing import SplineTransformer
    
  2. 创建一个包含-1 和 11 之间 20 个值的训练集X,以及目标变量y,它是X的正弦值:

    X = np.linspace(-1, 11, 20)
    y = np.sin(X)
    
  3. 绘制Xy之间的关系图:

    plt.plot(X, y)
    plt.ylabel("y")
    plt.xlabel("X")
    

    在下面的图中,我们可以看到X的正弦函数:

图 8.20 – 预测变量和目标变量之间的关系,其中 y = sine(x)

图 8.20 – 预测变量和目标变量之间的关系,其中 y = sine(x)

  1. 通过使用岭回归预测y来自X,并获取模型的预测结果:

    linmod = Ridge(random_state=10)
    linmod.fit(X.reshape(-1, 1), y)
    pred = linmod.predict(X.reshape(-1, 1))
    
  2. 现在,绘制Xy之间的关系图,并叠加预测结果:

    plt.plot(X, y)
    plt.plot(X, pred)
    plt.ylabel("y")
    plt.xlabel("X")
    plt.legend(
        ["y", "linear"],
        bbox_to_anchor=(1, 1),
        loc="upper left")
    

    在下面的图中,我们可以看到线性模型对Xy之间的非线性关系拟合得非常差:

图 8.21 – X 和 y 之间的线性拟合

图 8.21 – X 和 y 之间的线性拟合

  1. 现在,设置SplineTransformer()以从X中获取样条特征,通过使用X值内的三次多项式和五个等距结点:

    spl = SplineTransformer(degree=3, n_knots=5)
    
  2. 获取样条特征,并将 NumPy 数组转换为pandas数据框,添加样条基函数的名称:

    X_t = spl.fit_transform(X.reshape(-1, 1))
    X_df = pd.DataFrame(
        X_t,
        columns=spl.get_feature_names_out(["var"])
    )
    

    通过执行X_df.head(),我们可以看到样条特征:

图 8.22 – 包含样条的数据框

图 8.22 – 包含样条的数据框

注意

SplineTransformer()返回一个特征矩阵,由n_splines = n_knots + degree – 1组成。

  1. 现在,将样条与X的值进行绘图:

    plt.plot(X, X_t)
    plt.legend(
        spl.get_feature_names_out(["var"]),
        bbox_to_anchor=(1, 1),
        loc="upper left")
    plt.xlabel("X")
    plt.ylabel("Splines values")
    plt.title("Splines")
    plt.show()
    

    在下面的图中,我们可以看到不同样条与预测变量X的值之间的关系:

图 8.23 – 样条与预测变量 X 的值绘制的图

图 8.23 – 样条与预测变量 X 的值绘制的图

  1. 现在,通过使用从X获取的样条特征,利用岭回归拟合线性模型以预测y,然后获取模型的预测结果:

    linmod = Ridge(random_state=10)
    linmod.fit(X_t, y)
    pred = linmod.predict(X_t)
    
  2. 现在,绘制Xy之间的关系图,并叠加预测结果:

    plt.plot(X, y)
    plt.plot(X, pred)
    plt.ylabel("y")
    plt.xlabel("X")
    plt.legend(
        ["y", "splines"],
        bbox_to_anchor=(1, 1),
        loc="upper left")
    

    在下面的图中,我们可以看到通过利用样条特征作为输入,岭回归可以更好地预测y的形状:

图 8.24 – 基于样条覆盖 X 和 y 之间真实关系的线性模型的预测

图 8.24 – 基于样条覆盖 X 和 y 之间真实关系的线性模型的预测

注意

增加结点的数量或多项式的度数会增加样条曲线的灵活性。尝试从更高次的多项式创建样条,看看岭回归预测如何变化。

现在我们已经了解了样条特征是什么以及我们如何使用它们来预测非线性效应,让我们在一个真实的数据集上尝试一下。

  1. scikit-learn导入一些额外的类和函数:

    from sklearn.datasets import fetch_california_housing
    from sklearn.compose import ColumnTransformer
    from sklearn.model_selection import cross_validate
    
  2. 加载加利福尼亚住房数据集并删除两个变量,我们不会用它们进行建模:

    X, y = fetch_california_housing(
        return_X_y=True, as_frame=True)
    X.drop(["Latitude", "Longitude"], axis=1,
        inplace=True)
    
  3. 首先,我们将拟合一个岭回归来根据现有变量预测房价,通过使用交叉验证,然后获取模型的性能以设置基准:

    linmod = Ridge(random_state=10)
    cv = cross_validate(linmod, X, y)
    mean_, std_ = np.mean(
        cv[«test_score"]), np.std(cv["test_score"])
    print(f"Model score: {mean_} +- {std_}")
    

    在下面的输出中,我们可以看到模型性能,其中数值是 R 平方:

    SplineTransformer() to obtain spline features from four variables by utilizing third-degree polynomials and 50 knots, and then fit the pipeline to the data:
    
    

    spl = SplineTransformer(degree=3, n_knots=50)

    ct = ColumnTransformer(

    [("splines", spl, [

    "平均房间数"、"平均卧室数"、"人口",

    "平均占用人数"

    )],

    remainder="passthrough",

    )

    ct.fit(X, y)

注意

记住,我们需要使用ColumnTransformer()来从数据的一组变量中获取特征。通过remainder=passthrough,我们确保那些不作为样条模板的变量——即MedIncHouseAge——也被返回到结果 DataFrame 中。要检查这一步产生的特征,请执行ct.get_feature_names_out()

  1. 现在,拟合一个岭回归来根据MedIncHouseAge和样条特征预测房价,使用交叉验证,然后获取模型的性能:

    cv = cross_validate(linmod, ct.transform(X), y)
    mean_, std_ = np.mean(
        cv[«test_score"]), np.std(cv["test_score"])
    print(f"Model score: {mean_} +- {std_}")
    

    在下面的输出中,我们可以看到模型性能,其中数值是 R 平方:

    Model score: 0.5553526813919297 +- 0.02244513992785257
    

如我们所见,通过使用样条代替一些原始变量,我们可以提高线性回归模型的性能。

它是如何工作的…

在这个菜谱中,我们基于样条创建了新的特征。首先,我们使用一个玩具变量,其值从-1 到 11,然后我们从真实数据集中获取样条。在这两种情况下,过程是相同的——我们使用了scikit-learn中的SplineTransformer()SplineTransformer()转换器接受多项式的degree属性和结点的数量(n_knots)作为输入,并返回更好地拟合数据的样条。结点默认放置在X的等距值上,但通过knots参数,我们可以选择将它们均匀分布到X的十分位数上,或者我们可以传递一个包含应用作结点的X的特定值的数组。

注意

节点的数量、间距和位置由用户任意设置,这些是影响样条曲线形状的最主要参数。当在回归模型中使用样条曲线时,我们可以通过交叉验证的随机搜索来优化这些参数。

使用 fit() 函数,转换器计算样条曲线的节点。使用 transform() 函数,它返回 B 样条曲线的数组。转换器返回 n_splines=n_knots + degree – 1

记住,像大多数 scikit-learn 转换器一样,SplineTransformer() 现在也有选项返回 pandas 和 polars DataFrames,除了 NumPy 数组之外,这种行为可以通过 set_output() 方法进行修改。

最后,我们使用 ColumnTransformer() 从特征子集中导出样条曲线。因为我们把 remainder 设置为 passthrough,所以 ColumnTransformer() 将未用于获取样条曲线的特征连接到结果矩阵中。通过这种方式,我们使用样条曲线、MedIncHouseAge 变量拟合了岭回归,并成功提高了线性模型的表现。

参见

要了解更多关于 B 样条背后的数学知识,请查看以下文章: