Python-特征工程秘籍-全-

Python 特征工程秘籍(全)

原文:annas-archive.org/md5/3be0ee95713353c7de9e7e13bc64211b

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:前言

Python Feature Engineering Cookbook,涵盖了表格数据特征工程的几乎所有方面,包括缺失数据插补、分类编码、变量转换、离散化、缩放和异常值的处理。 它还讨论了如何从日期和时间、文本、时间序列和 关系数据集中提取特征。

本书将通过展示如何使用开源 Python 库来加速特征工程过程,通过多个实用的、动手的食谱来减轻特征工程的痛苦。 在本书中,您将利用 <st c="632">pandas</st> <st c="643">scikit-learn</st>来转换和创建新的变量。此外,您还将学习利用四大开源特征工程库的力量——Feature-engine、Category Encoders、Featuretools,以及 tsfresh。

您还将发现第二版中没有的额外食谱。 这些包括时间序列中的缺失数据插补、使用决策树创建新特征,以及使用中位数绝对偏差突出显示异常值。 更重要的是,我们提供了指导方针,帮助您根据您的模型和数据特征决定使用哪些转换。 您将确切知道每个特征转换是什么、为什么以及如何实现。

本书面向对象

本书面向机器学习和数据科学的学生和专业人员,以及参与部署机器学习模型的软件工程师,他们希望提高自己在数据转换和特征创建方面的技能,以改善模型训练。 本书旨在为所有对特征工程感兴趣或正在从事特征工程的人提供清晰的指导,说明应该做什么,如何做,以及为什么这样做很重要。 本资源超越了基础知识,提供了实用的见解和详细的解释,帮助您有效地掌握特征工程。

本书涵盖内容

第一章, 缺失数据插补,探讨了用合适的估计值替换数值、分类和时间序列数据中缺失值的技术。 它涵盖了单次和多次插补方法,并展示了如何使用 scikit-learn 和 Feature-engine 来简化插补过程。

第二章, 编码分类变量,涵盖了将分类变量转换为数值特征的方法。 它从常见的技巧,如独热编码和顺序编码开始,然后探讨了高基数和线性模型的适应性。 本章还讨论了特定领域的编码方法,如证据权重,并展示了如何使用目标编码等方法对高基数变量进行编码,确保你了解如何规范化过程以 避免过拟合

第三章, 转换数值变量,讨论了何时以及为什么需要转换变量以用于机器学习模型。 然后,它展示了不同的变量转换函数,并强调每个函数最适合哪种类型的变量。 到本章结束时,你将了解何时需要转换你的变量,以及为什么你应用对数或平方根等函数。

第四章, 执行变量离散化,介绍了离散化的概念,并强调了其在机器学习中的应用。 本章探讨了各种离散化方法,详细说明了它们的优缺点。 它涵盖了基本的等宽和等频离散化过程,以及使用决策树和 k-均值进行离散化。 最后,它将离散化与编码相结合,以返回与 目标单调的变量。

第五章, 处理异常值,介绍了识别异常值及其特征的方法。 然后,它讨论了移除异常值或调整其值以适应可接受边界的技巧,利用 pandas 和 Feature-engine。

第六章, *从日期和时间变量中提取特征 *变量 *,描述了如何从日期和时间变量中创建特征。 它涵盖了如何从特征中提取日期和时间组件,以及如何组合日期时间变量以及如何处理不同的 时区

第七章 执行特征缩放,涵盖了将变量置于相似尺度的方法。 它讨论了标准化、如何缩放到最大和最小值以及如何执行更稳健的变量缩放形式。 你还将找到关于根据你的模型 和变量选择哪种方法的指南。

第八章 创建新功能,介绍了将现有变量组合起来生成新特征的多种方法。 它展示了如何根据领域知识使用数学运算来组合特征。 然后,它讨论了如何通过正弦、余弦和样条曲线的使用来转换特征。 最后,它展示了通过决策树从一个或多个变量中创建特征的价值。

第九章 使用 Featuretools 从关系数据中提取特征,介绍了关系数据集,然后解释了我们可以如何利用 Featuretools 在不同的数据聚合级别创建特征。 你将学习如何自动从数值和分类变量、日期时间以及文本中创建数十个特征。

第十章 使用 tsfresh 从时间序列数据创建特征,讨论了如何自动从时间序列数据中创建数百个特征,用于监督分类或回归。 你将利用 tsfresh 的强大功能,自动从你的 时间序列中创建和选择相关特征。

第十一章 从文本变量中提取特征,探讨了有效的方法来清理和从短文本片段中提取特征,用于监督学习模型。 本章涵盖了计数单词、句子和字符以及测量词汇多样性的技术。 此外,它指导你通过文本清理过程,并演示了如何通过计数单词来构建特征矩阵。

为了最大限度地利用这本书

本书提供了实用工具和技术,以简化您的特征工程流程,让您能够提高代码质量并简化流程。 本书探讨了使用 Python 转换和创建特征的方法,以有效地训练机器学习模型。 因此,熟悉机器学习和 Python 编程将有助于您理解和应用本书中提出的 概念。

以下库版本中测试了这些食谱: **

  • <st c="6438">category-encoders == 2.6.3</st>

  • <st c="6465">Feature-engine == 1.8.0</st>

  • <st c="6489">featuretools == 1.31.0</st>

  • <st c="6512">matplotlib==3.8.3</st>

  • <st c="6530">nltk=3.8.1</st>

  • <st c="6541">numpy==1.26.4</st>

  • <st c="6555">pandas==2.2.1</st>

  • <st c="6569">scikit-learn==1.5.0</st>

  • <st c="6589">scipy==1.12.0</st>

  • <st c="6603">seaborn==0.13.2</st>

  • <st c="6619">tsfresh==0.20.0</st>

本书涵盖的软件/硬件 操作系统要求
Python 3.9 或更高版本 Windows、macOS、 或 Linux

*请注意,Python 库的较早或较新版本可能会阻止代码运行。如果您使用的是较新版本,请确保检查其文档以获取任何最近的更新、参数名称更改, 或弃用。

如果您正在使用本书的数字版,我们建议您亲自输入代码或通过 GitHub 仓库(下一节中提供链接)访问代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。 这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,网址为 https://github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他代码包,这些代码包来自我们丰富的书籍和视频目录,可在以下网址找到 https://github.com/PacktPublishing/。查看它们!

使用的约定

本书中使用了多种文本约定。 **

<st c="7736">文本中的代码</st>:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。 以下是一个示例:“我们使用了 <st c="7931">year</st>, <st c="7937">month</st>, 和 <st c="7948">quarter</st> 来分别捕获 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})

任何命令行输入或输出都按以下方式编写

 pip install yellowbrick

提示或重要注意事项

看起来像这样。

章节

在本书中,您将找到一些频繁出现的标题(准备就绪, 如何操作..., 工作原理..., 还有更多...,和 另请参阅)。

为了清楚地说明如何完成食谱,请按以下方式使用这些部分

准备就绪

本节告诉您在食谱中可以期待什么,并描述了如何设置任何软件或任何为食谱所需的初步设置。

如何操作…

本节包含遵循食谱所需的步骤

工作原理…

本节通常包含对上一节发生情况的详细说明

还有更多…

本节包含有关制作食谱的附加信息,以使您对其有更深入的了解

另请参阅

本节提供了有关食谱的其他有用信息的链接

取得联系

我们的读者反馈总是受欢迎的

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并给我们发送电子邮件 customercare@packtpub.com

勘误表:尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。 如果你在这本书中发现了错误,我们非常感谢你向我们报告。 请访问 www.packtpub.com/support/errata,选择你的书籍,点击 勘误提交表 链接,并输入 详细信息。

盗版:如果你在互联网上遇到任何形式的我们作品的非法副本,我们非常感谢你提供位置地址或网站名称。 请通过 copyright@packt.com 与我们联系,并提供 材料的链接。

如果你对成为作者感兴趣:如果你在某个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请 访问 authors.packtpub.com

分享你的想法

一旦你阅读了 Python 特征工程食谱,我们很乐意听听你的想法! 点击此处直接进入该书的亚马逊评论页面 并分享 你的反馈。

你的评论对我们和科技社区非常重要,并将帮助我们确保我们提供的是 高质量的内容。

下载这本书的免费 PDF 副本

感谢购买 这本书!

你喜欢在旅途中阅读,但又无法携带你的印刷 书籍到处走吗?

你的电子书购买是否与你的选择设备不兼容?

别担心,现在每购买一本 Packt 书籍,你都可以免费获得该书的 DRM 免费 PDF 版本。 无需付费。

在任何地方、任何设备上阅读。 从你最喜欢的技术书籍中直接搜索、复制和粘贴代码到你的应用程序中。

优惠远不止于此,你还可以获得独家折扣、时事通讯和每日免费内容的 邮箱访问。

遵循以下简单步骤来获得 以下好处:

  1. 扫描二维码或访问以下 链接

https://packt.link/free-ebook/978-1-83588-358-7

  1. 提交您的购买证明 证明

  2. 这就完了! 我们将直接将您的免费 PDF 和其他福利发送到您的 电子邮件

第二章:填充缺失数据

缺失数据——即某些观测值的值缺失——是大多数数据源中不可避免的问题。一些机器学习模型的实现可以开箱即用处理缺失数据。为了训练其他模型,我们必须移除带有缺失数据的观测值或将它们转换为允许的值。

用统计估计替换缺失数据的行为称为填充。任何填充技术的目标都是生成一个完整的数据集。有多种填充方法。我们根据数据是否随机缺失、缺失值的比例以及我们打算使用的机器学习模型来选择使用哪种方法。在本章中,我们将讨论几种填充方法。

本章将涵盖以下食谱:

  • 移除带有缺失数据的观测值

  • 执行均值或中位数填充

  • 填充分类变量

  • 用任意数字替换缺失值

  • 为填充寻找极端值

  • 标记填充值

  • 实施前向和后向填充

  • 执行插值

  • 通过链式方程进行多元填充

  • 使用最近邻估计缺失数据

技术要求

在本章中,我们将使用 Python 库 Matplotlib、pandas、NumPy、scikit-learn 和 Feature-engine。如果你需要安装 Python,免费的 Anaconda Python 发行版(https://www.anaconda.com/)包括大多数数值计算库。

<st c="1470">feature-engine</st>可以使用pip安装,如下所示:

 pip install feature-engine

如果你使用 Anaconda,你可以使用conda安装<st c="1588">feature-engine</st>

 conda install -c conda-forge feature_engine

注意

本章中的食谱是在出版时使用的 Python 库的最新版本中创建的。您可以在随附的 GitHub 仓库中的 <st c="1819">requirements.txt</st> 文件中检查版本, 网址为 https://github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/requirements.txt

我们将使用信用审批 数据集,该数据集来自UCI 机器学习仓库 https://archive.ics.uci.edu/),许可协议为 CC BY 4.0 创意共享: https://creativecommons.org/licenses/by/4.0/legalcode。您可以在以下链接找到数据集: http://archive.ics.uci.edu/dataset/27/credit+approval

我已下载并修改了如下笔记本中的数据:此笔记本

我们还将使用位于 Facebook 的 Prophet GitHub 仓库中的航空乘客 数据集(https://github.com/facebook/prophet/blob/main/examples/example_air_passengers.csv),许可协议为 MIT: https://github.com/facebook/prophet/blob/main/LICENSE

我已修改了如下笔记本中的数据:此笔记本

你可以在附带的 GitHub 存储库中找到修改后的数据集的副本: https://github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch01-missing-data-imputation/

删除具有缺失数据的观察值

完整案例分析 (CCA**),也称为案例列表删除,包括丢弃具有缺失数据的观察值。 CCA 可以应用于分类 和数值变量。 使用 CCA,我们可以在插补后保留变量的分布,前提是数据缺失是随机的,并且仅在少量观察值中。 然而,如果数据在许多变量中缺失,CCA 可能会导致大量数据集的删除。

仅在少量观察值缺失且你有充分的理由相信它们对你的模型不重要时才使用 CCA。

如何操作...

让我们首先进行一些导入并加载数据集:

  1. 让我们导入 <st c="3956">pandas</st><st c="3964">matplotlib</st>,以及来自 scikit-learn 的训练/测试分割函数

     import matplotlib.pyplot as plt
    import pandas as pd
    from sklearn.model_selection import train_test_split
    
  2. 让我们加载并显示在《技术要求》部分中描述的数据集:

     data = pd.read_csv("credit_approval_uci.csv")
    data.head()
    

    在以下图像中,我们看到了数据的前 5 行

图 1.1 – 数据集的前 5 行

图 1.1 – 数据集的前 5 行

  1. 让我们像通常准备训练机器学习模型数据那样进行操作: 通过将数据分为训练集和测试集:

     X_train, X_test, y_train, y_test = train_test_split(
        data.drop("target", axis=1),
        data["target"],
        test_size=0.30,
        random_state=42,
    )
    
  2. 现在让我们绘制一个条形图,显示训练集和测试集中每个变量的缺失数据比例:

     fig, axes = plt.subplots(
        2, 1, figsize=(15, 10), squeeze=False)
    X_train.isnull().mean().plot(
        kind='bar', color='grey', ax=axes[0, 0], title="train")
    X_test.isnull().mean().plot(
        kind='bar', color='black', ax=axes[1, 0], title="test")
    axes[0, 0].set_ylabel('Fraction of NAN')
    axes[1, 0].set_ylabel('Fraction of NAN')
    plt.show()
    

    前面的代码块返回以下条形图 ,显示了训练集(顶部)和测试集(底部)中每个变量的缺失数据比例:

图 1.2 – 每个变量的缺失数据比例

图 1.2 – 每个变量的缺失数据比例

  1. 现在,我们将删除任何变量中存在缺失值的观测值:

     train_cca = X_train.dropna()
    test_cca = X_test.dropna()
    

注意

pandas 的 <st c="5916">dropna()</st>默认删除任何缺失值的观测值。 我们可以像这样删除变量子集中具有缺失数据的观测值: <st c="6060">data.dropna(subset=["A3", "A4"])</st>

  1. 让我们打印并比较原始和完整 案例数据集的大小:

     print(f"Total observations: {len(X_train)}")
    print(f"Observations without NAN: {len(train_cca)}")
    

    我们从训练集中删除了超过 200 个具有缺失数据的观测值,如下面的输出所示:

    <st c="6381">Total observations: 483</st>
    <st c="6405">Observations without NAN: 264</st>
    
  2. 从训练集和测试集中删除观测值后,我们需要对齐 目标变量:

     y_train_cca = y_train.loc[train_cca.index]
    y_test_cca = y_test.loc[test_cca.index]
    

    现在,数据集和目标变量包含没有 缺失数据的行。

  3. 要使用 <st c="6746">feature-engine</st>删除具有缺失数据的观测值,让我们导入所需的变压器:

     from feature_engine.imputation import DropMissingData
    
  4. 让我们设置填充器以自动找到具有 缺失数据的变量:

     cca = DropMissingData(variables=None, missing_only=True)
    
  5. 让我们拟合变压器,以便它找到具有 缺失数据的变量:

     cca.fit(X_train)
    
  6. 让我们检查变压器发现的具有 NAN 的变量:

     cca.variables_
    

    之前的命令返回了具有 缺失数据的变量名称:

    <st c="7241">['A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'A10', 'A14']</st>
    
  7. 让我们删除训练集和 测试集中具有缺失数据的行:

     train_cca = cca.transform(X_train)
    test_cca = cca.transform(X_test)
    

    使用 <st c="7454">train_cca.isnull().sum()</st> 来证实完整 案例数据集中没有缺失数据。

  8. <st c="7551">DropMissingData</st> 可以在从训练集中删除缺失数据后自动调整目标:

     train_c, y_train_c = cca.transform_x_y( X_train, y_train)
    test_c, y_test_c = cca.transform_x_y(X_test, y_test)
    

之前的代码从训练集和测试集中删除了包含 <st c="7802">nan</st> 的行,然后重新对齐了 目标变量。

注意

要从变量子集中删除具有缺失数据的观测值,请使用 <st c="7956">DropMissingData(variables=['A3', 'A4'])</st>。要删除至少 5%的变量中包含 <st c="8017">nan</st> 的行,请使用 <st c="8058">DropMissingData(threshold=0.95)</st>

工作原理...

在这个菜谱中, 我们绘制了每个变量中缺失数据的比例,然后删除了所有具有 缺失值的观测值。

我们使用了 <st c="8246">pandas</st> <st c="8252">isnull()</st> <st c="8266">mean()</st> 方法来确定每个变量中缺失观测值的比例。 <st c="8355">isnull()</st> 方法为每个变量创建了一个布尔向量,其中包含 <st c="8414">True</st> <st c="8423">False</st> 值,表示值是否缺失。 <st c="8480">mean()</st> 方法取这些值的平均值,并返回缺失数据的比例。

我们使用了 <st c="8580">pandas</st> <st c="8586">plot.bar()</st> 来创建每个变量的缺失数据分数的条形图。 图 1**.2中,我们看到了训练集和测试集中每个变量的 <st c="8703">nan</st> 分数。

为了删除任何变量中存在缺失值的观测值,我们使用了 pandas 的 <st c="8827">dropna()</st>,从而获得一个完整的 案例数据集。

最后,我们使用 Feature-engine 的 <st c="8936">DropMissingData()</st>删除缺失数据。此填充器在调用 <st c="9074">fit()</st> 方法时自动识别并存储了训练集中缺失数据的变量。 使用 <st c="9097">transform()</st> 方法,填充器删除了那些变量中的 <st c="9154">nan</st> 观测值。 使用 <st c="9184">transform_x_y()</st>,填充器从数据集中删除了包含 <st c="9231">nan</st> 的行,然后重新对齐了 目标变量。

另请参阅

如果您想在管道中使用 <st c="9322">DropMissingData()</st> 与其他 Feature-engine 或 scikit-learn 转换器一起,请查看 Feature-engine 的 <st c="9450">Pipeline</st>: https://Feature-engine.trainindata.com/en/latest/user_guide/pipeline/Pipeline.html。此管道在删除行后可以将目标与训练集和测试集对齐。

执行均值或中位数填充

均值或中位数插补 包括用变量的均值或中位数值替换缺失数据。 为了避免数据泄露,我们使用训练集来确定均值或中位数,然后使用这些值来插补训练集和测试集,以及所有 未来的数据。

Scikit-learn 和 Feature-engine 从训练集中学习均值或中位数,并将这些参数存储在 箱外供将来使用。

在这个菜谱中,我们将使用 <st c="10132">pandas</st><st c="10140">scikit</st>-<st c="10148">learn</st>,和 <st c="10155">feature-engine</st>来执行均值和中位数插补。

注意

如果变量呈正态分布,则使用均值插补;否则使用中位数插补。 如果缺失数据比例很高,均值和中位数插补可能会扭曲变量的分布。

如何做...

让我们开始 这个菜谱:

  1. 首先,我们将导入 <st c="10446">pandas</st> 以及从 <st c="10496">scikit-learn</st> <st c="10514">feature-engine</st>中所需的函数和类:

     import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.impute import SimpleImputer
    from sklearn.compose import ColumnTransformer
    from feature_engine.imputation import MeanMedianImputer
    
  2. 让我们加载我们在 技术 要求 部分中准备的数据集:

     data = pd.read_csv("credit_approval_uci.csv")
    
  3. 让我们将数据分割成训练集和测试集及其 相应的目标:

     X_train, X_test, y_train, y_test = train_test_split(
        data.drop("target", axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  4. 让我们通过排除 对象类型的变量来创建一个包含数值变量的列表:

     numeric_vars = X_train.select_dtypes(
        exclude="O").columns.to_list()
    

    如果您执行 <st c="11251">numeric_vars</st>,您将看到数值变量的名称: <st c="11316">['A2', 'A3', 'A8', 'A11', '</st>``<st c="11343">A14', 'A15']</st>

  5. 让我们将变量的中位数存储在 一个字典中:

     median_values = X_train[
        numeric_vars].median().to_dict()
    

提示

注意我们如何使用训练集来计算中位数。 我们将使用这些值来替换训练集和测试集中的缺失数据。 要计算均值,请使用 pandas <st c="11645">mean()</st> 而不是 <st c="11663">median()</st>

如果您执行 <st c="11688">median_values</st>,您将看到一个包含每个变量的中位数的字典: <st c="11765">{'A2': 28.835, 'A3': 2.75, 'A8': 1.0, 'A11': 0.0, 'A14': 160.0, '</st>``<st c="11830">A15': 6.0}.</st>

  1. 让我们用中值替换缺失数据

     X_train_t = X_train.fillna(value=median_values)
    X_test_t = X_test.fillna(value=median_values)
    

    如果您在填充后执行 <st c="11996">X_train_t[numeric_vars].isnull().sum()</st> ,数值变量中的缺失值数量应该是 0

注意

<st c="12130">pandas</st> <st c="12137">fillna()</st> 默认返回一个包含填充值的新的数据集。 要替换原始 DataFrame 中的缺失数据,将 <st c="12260">inplace</st> 参数设置为 <st c="12281">True</st> <st c="12288">X_train.fillna(value=median_values, inplace</st><st c="12331">=True)</st>

现在,让我们使用 <st c="12396">scikit-learn</st>用中值填充缺失值

  1. 让我们设置填充器 以用中值替换缺失数据

     imputer = SimpleImputer(strategy="median")
    

注意

要执行均值填充,将 <st c="12556">SimpleImputer()</st> 设置为以下格式: <st c="12584">imputer =</st> <st c="12593">SimpleImputer(strategy = "</st>``<st c="12620">mean")</st>

  1. 我们通过使用 <st c="12692">ColumnTransformer()</st>来限制填充仅限于数值变量:

     ct = ColumnTransformer(
        [("imputer", imputer, numeric_vars)],
        remainder="passthrough",
        force_int_remainder_cols=False,
    ).set_output(transform="pandas")
    

注意

Scikit-learn 可以根据我们设置的转换输出返回 <st c="12895">numpy</st> 数组, <st c="12909">pandas</st> 数据框,或 <st c="12931">极坐标</st> 框。 默认情况下,它返回 <st c="13018">numpy</st> 数组。

  1. 让我们将填充器 拟合到训练集,以便它学习中值 值:

     ct.fit(X_train)
    
  2. 让我们查看学到的 中值:

     ct.named_transformers_.imputer.statistics_
    

    前一个命令返回每个变量的中值

    array([ 28.835,   2.75,   1.,   0., 160.,   6.])
    
  3. 让我们用中值替换缺失值

     X_train_t = ct.transform(X_train)
    X_test_t = ct.transform(X_test)
    
  4. 让我们显示生成的 训练集:

     print(X_train_t.head())
    

    以下图像显示了生成的 DataFrame:

图 1.3 – 填充后的训练集。被填充的变量由填充器前缀标记;未转换的变量显示前缀余数

图 1.3 – 填充后的训练集。 被填充的变量由填充器前缀标记;未转换的变量显示前缀余数

最后,让我们使用 <st c="14295">feature-engine</st>. 进行中位数插补。

  1. 让我们设置插补器,以用中位数替换数值变量中的缺失数据:

     imputer = MeanMedianImputer(
        imputation_method="median",
        variables=numeric_vars,
    )
    

注意

要执行均值插补,将 <st c="14523">imputation_method</st> 改为 <st c="14544">"mean"</st>。默认情况下,<st c="14563">MeanMedianImputer()</st> 将插补 DataFrame 中的所有数值变量,忽略分类变量。 使用 <st c="14677">variables</st> 参数将插补限制为数值变量的子集。

  1. 将插补器拟合,以便它学习中位数值:

     imputer.fit(X_train)
    
  2. 检查学习到的中位数:

     imputer.imputer_dict_
    

    前一个命令返回字典中的中位数值:

    {<st c="14951">'A2': 28.835, 'A3': 2.75, 'A8': 1.0, 'A11': 0.0, 'A14': 160.0, 'A15': 6.0}</st>
    
  3. 最后,让我们用中位数替换缺失值:

     X_train = imputer.transform(X_train)
    X_test = imputer.transform(X_test)
    

<st c="15156">Feature-engine</st><st c="15174">MeanMedianImputer()</st> 返回一个 <st c="15204">DataFrame</st>。您可以使用 <st c="15292">X_train[numeric</st><st c="15307">_vars].isnull().mean()</st>` 检查插补变量是否不包含缺失值。

如何工作...

在这个配方中,我们使用 <st c="15429">pandas</st>scikit-learn<st c="15449"><st c="15451">feature-engine</st>` 将缺失数据替换为变量的中位数值。

我们使用 scikit-learn 的 <st c="15540">train_test_split()</st> 函数将数据集分为训练集和测试集。 该函数接受预测变量、目标、测试集中要保留的观测值比例以及用于可重复性的 <st c="15687">random_state</st> 值` 作为参数。 它返回一个包含原始观测值 70% 的训练集和一个包含原始观测值 30% 的测试集。 70:30 的分割是随机进行的。

要使用pandas填充缺失数据,在第 5 步中,我们创建了一个字典,其中数值变量名称作为键,它们的平均值作为值。这些平均值是从训练集中学习的,以避免数据泄露。为了替换缺失数据,我们应用了pandasfillna()到训练和测试集,传递包含每个变量的中值的字典作为参数。

要使用scikit-learn用中位数替换缺失值,我们使用了SimpleImputer(),其strategy设置为"median"。为了限制填充仅限于数值变量,我们使用了ColumnTransformer()。将remainder参数设置为passthrough,我们使ColumnTransformer()返回训练集中看到的所有变量在转换输出中;填充的变量随后是那些未转换的变量。

注意

ColumnTransformer()会更改输出中变量的名称。转换后的变量显示前缀imputer,而未更改的变量显示前缀remainder

在第 8 步中,我们将列转换器的输出设置为pandas以获得一个 DataFrame 作为结果。默认情况下,ColumnTransformer()返回numpy数组。

注意

从版本 1.4.0 开始,scikit-learn转换器可以返回numpy数组、pandas DataFrame 或polar frames 作为transform()方法的输出结果。

使用fit()SimpleImputer()学习训练集中每个数值变量的中位数并将它们存储在其statistics_属性中。使用transform(),它用中位数替换了缺失值。

要使用 Feature-engine 将缺失值替换为中位数,我们使用了 <st c="17448">MeanMedianImputer()</st> ,并将 <st c="17477">imputation_method</st> 设置为 <st c="17502">median</st>。要限制插补变量的子集,我们将变量名列表传递给 <st c="17609">variables</st> 参数。 使用 <st c="17635">fit()</st>,转换器学习并存储了每个 变量 的中位数值,存储在其 <st c="17723">imputer_dict_</st> 属性中的字典中。 使用 <st c="17761">transform()</st>,它替换了缺失值,返回一个 pandas DataFrame。

插补分类变量

我们通常用最频繁的类别或一个特定的字符串来插补分类变量。 为了避免数据泄露,我们从训练集中找到频繁的类别。 然后,我们使用这些值来插补训练、测试和未来的数据集。 <st c="18119">scikit-learn</st> <st c="18136">feature-engine</st> 会自动找到并存储用于插补的频繁类别。

在这个配方中,我们将用最频繁的类别 或一个 任意字符串替换分类变量中的缺失数据。

如何做到这一点...

首先,让我们进行一些导入并准备 数据:

  1. 让我们导入 <st c="18444">pandas</st> 以及从 <st c="18495">scikit-learn</st> <st c="18512">feature-engine</st>中所需的函数和类:

     import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.impute import SimpleImputer
    from sklearn.compose import ColumnTransformer
    from feature_engine.imputation import CategoricalImputer
    
  2. 让我们加载我们在 技术 要求 部分中准备的数据集:

     data = pd.read_csv("credit_approval_uci.csv")
    
  3. 让我们将数据集分割成训练集和测试集及其 相应的目标:

     X_train, X_test, y_train, y_test = train_test_split(
        data.drop("target", axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  4. 让我们将分类变量 存储在一个列表中:

     categorical_vars = X_train.select_dtypes(
        include="O").columns.to_list()
    
  5. 让我们将变量的最频繁类别存储在一个 字典中:

     frequent_values = X_train[
        categorical_vars].mode().iloc[0].to_dict()
    
  6. 让我们用频繁的类别 替换缺失值:

     X_train_t = X_train.fillna(value=frequent_values)
    X_test_t = X_test.fillna(value=frequent_values)
    

注意

<st c="19503">fillna()</st> 默认返回一个包含插补值的新的 DataFrame。 我们可以通过执行 <st c="19640">X_train.fillna(value=frequent_valu</st><st c="19674">es, inplace=True)</st>来替换原始 DataFrame 中的缺失数据。

  1. 要用特定字符串替换缺失数据,请创建一个包含分类变量名称作为键和任意字符串作为 值的插补字典:

     imputation_dict = {var:
         "no_data" for var in categorical_vars}
    

    现在,我们可以使用这个字典和第 *6 中的代码来替换缺失数据。

注意

使用 <st c="20020">pandas</st> <st c="20026">value_counts()</st> 我们可以看到插补添加的字符串。 例如,尝试执行 以下命令: <st c="20117">X_train["A1"].value_counts()</st>

现在,让我们使用 <st c="20218">scikit-learn</st>来用最频繁的类别插补缺失值:

  1. 让我们设置插补器以找到每个变量的最频繁类别:

     imputer = SimpleImputer(strategy='most_frequent')
    

注意

<st c="20360">SimpleImputer()</st> 将为数值和分类变量学习众数。 但在实践中,众数插补仅用于分类 变量。

  1. 让我们将插补限制在 分类变量

     ct = ColumnTransformer(
        [("imputer",imputer, categorical_vars)],
        remainder="passthrough"
        ).set_output(transform="pandas")
    

注意

要用字符串而不是最频繁的类别来插补缺失数据,请按以下方式设置 <st c="20784">SimpleImputer()</st> <st c="20812">imputer =</st> <st c="20822">SimpleImputer(strategy="constant",</st> <st c="20857">fill_value="missing")</st>.

  1. 将插补器拟合到训练集,以便它学习最频繁的值:

     ct.fit(X_train)
    
  2. 让我们看看插补器学到的最频繁值:

     ct.named_transformers_.imputer.statistics_
    

    上一条命令返回每个变量的最频繁值

    <st c="21153">array(['b', 'u', 'g', 'c', 'v', 't', 'f', 'f', 'g'], dtype=object)</st>
    
  3. 最后,让我们用频繁的类别替换缺失值:

     X_train_t = ct.transform(X_train)
    X_test_t = ct.transform(X_test)
    

    确保通过 执行 <st c="21414">X_train_t.head()</st>.来检查生成的 DataFrames。

注意

The <st c="21441">ColumnTransformer()</st> 更改了变量的名称。 被插补的变量显示前缀 <st c="21535">imputer</st> ,而未转换的变量显示前缀 <st c="21579">remainder</st>

最后,让我们使用 <st c="21641">feature-engine</st>来插补缺失值s

  1. 让我们设置插补器 以用最频繁的值替换分类变量中的缺失数据:

     imputer = CategoricalImputer(
        imputation_method="frequent",
        variables=categorical_vars,
    )
    

注意

<st c="21871">variables</st> 参数设置为 <st c="21898">None</st><st c="21904">CategoricalImputer()</st> 将自动填充训练集中找到的所有分类变量。 使用此参数将填充限制为分类变量的子集,如步骤 13所示。

  1. 将填充器拟合到训练集,以便它学习最频繁的类别:

     imputer.fit(X_train)
    

注意

要使用特定字符串填充分类变量,将 <st c="22273">imputation_method</st> 设置为 <st c="22294">missing</st>,并将 <st c="22306">fill_value</st> 设置为所需的字符串。

  1. 让我们检查一下学习到的类别

     imputer.imputer_dict_
    

    我们可以在以下输出中看到包含最频繁值的字典:

    <st c="22482">{'A1': 'b',</st>
     <st c="22494">'A4': 'u',</st>
     <st c="22505">'A5': 'g',</st>
     <st c="22516">'A6': 'c',</st>
     <st c="22527">'A7': 'v',</st>
     <st c="22538">'A9': 't',</st>
     <st c="22549">'A10': 'f',</st>
     <st c="22561">'A12': 'f',</st>
     <st c="22573">'A13': 'g'}</st>
    
  2. 最后,让我们用频繁类别替换缺失值:

     X_train_t = imputer.transform(X_train)
    X_test_t = imputer.transform(X_test)
    

    如果你想要使用 <st c="22819">CategoricalImputer()</st>》用字符串或最频繁的值填充数值变量,将 <st c="22849">ignore_format</st> 参数设置为 <st c="22876">True</st>

<st c="22881">CategoricalImputer()</st> 返回 一个 pandas DataFrame 作为 结果。

工作原理...

在这个菜谱中,我们使用最频繁的类别或任意字符串替换了分类变量中的缺失值。我们使用了 <st c="23093">pandas</st><st c="23099">scikit-learn</st><st c="23101">feature-engine</st>

步骤 5中,我们使用变量名作为键,频繁类别作为值创建了一个字典。为了捕获频繁类别,我们使用了 pandas 的 <st c="23292">mode()</st>,并且为了返回一个字典,我们使用了 pandas 的 <st c="23343">to_dict()</st>。为了替换缺失数据,我们使用了 <st c="23391">pandas</st><st c="23397">fillna()</st>,传递包含变量及其频繁类别的字典作为参数。 在一个变量中可能存在多个众数,这就是为什么我们确保通过使用 <st c="23612">.iloc[0]</st>. 只捕获其中之一的原因。

为了使用scikit-learn替换缺失值,我们使用了SimpleImputer(),其strategy设置为most_frequent。为了将插补限制为分类变量,我们使用了ColumnTransformer()。将remainder设置为passthrough后,ColumnTransformer()将返回训练集中所有变量的结果,作为transform()方法的输出。

注意

ColumnTransformer()更改输出中变量的名称。转换后的变量显示前缀imputer,而未更改的变量显示前缀remainder

使用fit()函数,SimpleImputer()学习变量最频繁的类别并将它们存储在其statistics_属性中。使用transform()函数,它用学习到的参数替换了缺失数据。

SimpleImputer()ColumnTransformer()默认返回 NumPy 数组。我们可以通过set_output()参数更改此行为。

为了使用feature-engine替换缺失值,我们使用了CategoricalImputer(),其imputation_method设置为frequent。使用fit(),转换器学习并存储了最频繁的类别在其imputer_dict_属性中的字典中。使用transform(),它用学习到的参数替换了缺失值。

<st c="24814">SimpleImputer()</st>不同, <st c="24831">CategoricalImputer()</st> 将仅插补分类变量,除非明确通过设置 <st c="24946">ignore_format</st> 参数为 <st c="24973">True</st>来告知不要这样做。 此外,使用 <st c="24997">feature-engine</st> 转换器,我们可以通过转换器本身限制转换到变量子集。 对于 <st c="25122">scikit-learn</st> 转换器,我们需要额外的 <st c="25172">ColumnTransformer()</st> 类来将转换应用于变量子集。

用任意数替换缺失值

我们可以用任意值替换缺失数据。 常用的值有 <st c="25385">999</st>, <st c="25390">9999</st>, 或 <st c="25399">-1</st> 对于正态分布。 这种方法用于数值变量。 对于分类变量,等效的方法是用任意字符串替换缺失数据,如插补分类 变量 配方中所述。

当用任意数替换缺失值时,我们需要小心不要选择接近均值、中位数或分布的任何其他常见值的数值。

注意

当数据不是随机缺失时,我们会使用任意数值插补,使用非线性模型,或者当缺失数据的百分比较高时。 这种插补技术会扭曲原始 变量分布。

在这个配方中,我们将使用 <st c="26106">pandas</st><st c="26111">s</st>, <st c="26115">scikit-learn</st>, 和 <st c="26129">feature-engine</st>来用任意数插补缺失数据。

如何操作...

让我们首先导入必要的工具和加载数据:

  1. 导入 <st c="26239">pandas</st> 和所需的函数 和类:

     import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.impute import SimpleImputer
    from feature_engine.imputation import ArbitraryNumberImputer
    
  2. 让我们加载在技术 要求 部分中描述的数据集:

     data = pd.read_csv("credit_approval_uci.csv")
    
  3. 让我们将 数据分为训练集和 测试集:

     X_train, X_test, y_train, y_test = train_test_split(
        data.drop("target", axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    

    我们将选择大于分布最大值的任意值。

  4. 让我们找到四个 数值变量的最大值:

     X_train[['A2','A3', 'A8', 'A11']].max()
    

    之前的 命令返回以下输出:

    <st c="26994">A2     76.750</st>
    <st c="27004">A3     26.335</st>
    <st c="27014">A8     28.500</st>
    <st c="27024">A11    67.000</st>
    <st c="27061">99</st> for the imputation because it is bigger than the maximum values of the numerical variables in *<st c="27158">step 4</st>*.
    
  5. 让我们复制原始的 DataFrame:

     X_train_t = X_train.copy()
    X_test_t = X_test.copy()
    
  6. 现在,我们将缺失值替换为 <st c="27299">99</st>

     X_train_t[["A2", "A3", "A8", "A11"]] = X_train_t[[
        "A2", "A3", "A8", "A11"]].fillna(99)
    X_test_t[["A2", "A3", "A8", "A11"]] = X_test_t[[
        "A2", "A3", "A8", "A11"]].fillna(99)
    

注意

要使用 <st c="27546">pandas</st> <st c="27552">fillna()</st>函数填充不同变量不同的值,可以使用如下字典: <st c="27591">imputation_dict = {"A2": -1, "A3": -</st><st c="27627">1, "A8": 999, "</st>``<st c="27643">A11": 9999}</st>.

现在,我们将使用 <st c="27682">scikit-learn</st>来填充缺失值,使用任意数:

  1. 让我们设置 <st c="27748">填充器</st> 以替换缺失值为 <st c="27782">99</st>

     imputer = SimpleImputer(strategy='constant', fill_value=99)
    

注意

如果你的数据集包含分类变量, <st c="27905">SimpleImputer()</st> 将在任何缺失值的情况下,也将 <st c="27930">99</st> 添加到这些变量中。

  1. 让我们将 <st c="27997">填充器</st> 拟合到包含要填充变量的训练集切片中

     vars = ["A2", "A3", "A8", "A11"]
    imputer.fit(X_train[vars])
    
  2. 在所需变量中将缺失值替换为 <st c="28161">99</st>

     X_train_t[vars] = imputer.transform(X_train[vars])
    X_test_t[vars] = imputer.transform(X_test[vars])
    

    继续执行 <st c="28349">X_test_t[["A2", "A3", "</st>``<st c="28372">A8", "A11"]].isnull().sum()</st>来检查缺失值的数量。

    为了完成,让我们使用填充缺失值 功能 <st c="28448">f</st><st c="28449">eature-engine</st>.

  3. 让我们设置 <st c="28481">填充器</st> 以替换 <st c="28499">缺失值</st> <st c="28520">99</st> 在 4 个特定变量中:

     imputer = ArbitraryNumberImputer(
        arbitrary_number=99,
        variables=["A2", "A3", "A8", "A11"],
    )
    

<st c="28641">注意</st>

<st c="28646">ArbitraryNumberImputer()</st> 将自动选择训练集中的所有数值变量进行填充,如果我们设置 <st c="28768">variables</st> 参数 <st c="28791">None</st>

  1. 最后,让我们将缺失值替换为 <st c="28839">99</st>

     X_train = imputer.fit_transform(X_train)
    X_test = imputer.transform(X_test)
    

注意

为了使用不同的数字来填充不同的变量,请按照以下方式设置 <st c="28991">ArbitraryNumberImputer()</st> <st c="29028">ArbitraryNumberImputer(imputater_dict = {"A2": -1, "A3": -1, "A8": 999, "</st>``<st c="29101">A11": 9999})</st>

我们现在已经使用三个不同的开源库将缺失数据替换为任意数字。

它是如何工作的...

在这个配方中,我们使用 <st c="29332">pandas</st> <st c="29340">sc</st><st c="29342">ikit-learn</st>,和 <st c="29355">featur</st><st c="29365">e-engine</st>替换了数值变量中的缺失值。

为了确定使用哪个任意值,我们使用 pandas 的 <st c="29493">max()</st>检查了四个数值变量的最大值。我们选择了 <st c="29509">99</st> ,因为它大于所选变量的最大值。 步骤 5中,我们使用了 <st c="29605">pandas</st> <st c="29611">fillna()</st> 来替换缺失数据。

要使用 <st c="29682">scikit-learn</st>替换缺失值,我们使用了 <st c="29708">SimpleImputer()</st>,将 <st c="29734">策略</st> 设置为 <st c="29750">constant</st>,并在 <st c="29784">fill_value</st> 参数中指定了 <st c="29774">99</st> 接下来,我们将填充器拟合到训练集中需要填充数值变量的一个子集。 最后,我们使用 <st c="29937">transform()</st>替换了缺失值。

要使用 <st c="29987">feature-engine</st> 替换缺失值,我们使用了 <st c="30010">ArbitraryValueImputer()</st>,指定了值 <st c="30056">99</st> 以及要填充的变量作为参数。 接下来,我们将 <st c="30123">fit_transform()</st> 方法应用于替换训练集中的缺失数据,并使用 <st c="30195">transform()</st> 方法替换测试集中的缺失数据。

为填充确定极端值

用变量分布末尾的值(极端值)替换缺失值,就像用任意值替换它们一样,但不是手动设置任意值,而是从变量分布的末尾自动选择这些值。

我们可以用比变量中大多数值大或小的值来替换缺失数据。 为了选择一个比平均值大的值,我们可以使用平均值加上标准差的因子。 或者,我们可以将其设置为 75 分位数 + 四分位数间距 × 1.5。 四分位数间距 代表 四分位数范围 ,是第 75 分位数和第 25 分位数之间的差。 为了用比剩余值小的值替换缺失数据,我们可以使用平均值减去标准差的因子,或者第 25 分位数 – 四分位数间距 × 1.5。

注意

尾部插补可能会扭曲原始变量的分布,因此可能不适合 线性模型。

在这个菜谱中,我们将使用 <st c="31314">pandas</st> <st c="31325">feature-engine</st>来实现尾部或极端值插补。

如何做...

为了开始这个 菜谱,让我们导入必要的工具 并加载 数据:

  1. 让我们导入 <st c="31444">pandas</st> 和所需的功能 和类:

     import pandas as pd
    from sklearn.model_selection import train_test_split
    from feature_engine.imputation import EndTailImputer
    
  2. 让我们加载我们在 技术 要求 部分中描述的数据集:

     data = pd.read_csv("credit_approval_uci.csv")
    
  3. 让我们将数值变量捕获到一个列表中,排除 目标:

     numeric_vars = [
        var for var in data.select_dtypes(
            exclude="O").columns.to_list()
        if var !="target"
    ]
    
  4. 让我们将数据分为训练集和测试集,只保留 数值变量:

     X_train, X_test, y_train, y_test = train_test_split(
        data[numeric_vars],
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  5. 我们现在将确定 四分位数间距:

     IQR = X_train.quantile(0.75) - X_train.quantile(0.25)
    

    我们可以通过执行 <st c="32245">IQR</st> <st c="32252">print(IQR)</st>来可视化 四分位数间距值:

    <st c="32264">A2      16.4200</st>
    <st c="32274">A3</st> <st c="32278">6.5825</st>
    <st c="32284">A8       2.8350</st>
    <st c="32294">A11      3.0000</st>
    <st c="32305">A14    212.0000</st>
    <st c="32318">A15    450.0000</st>
    <st c="32331">dtype: float64</st>
    
  6. 让我们创建一个包含变量名称和 插补值的字典:

     imputation_dict = (
        X_train.quantile(0.75) + 1.5 * IQR).to_dict()
    

注意

如果我们使用四分位数范围邻近规则,我们通过将 1.5 倍的四分位数范围(IQR)加到第 75 百分位数来确定插补值。 如果变量呈正态分布,我们可以通过计算平均值加上标准差的因子来计算插补值, <st c="32762">imputation_dict = (X_train.mean() + 3 * X_train.std()).to_dict()</st>.

  1. 最后,让我们替换 缺失的数据:

     X_train_t = X_train.fillna(value=imputation_dict)
    X_test_t = X_test.fillna(value=imputation_dict)
    

注意

我们还可以使用 <st c="33061">value = X_train[var].quantile(0.25) - 1.5 * IQR</st> <st c="33112">value = X_train[var].mean() – 3 * X_train[var].std()</st>来用分布左尾的值替换缺失数据 .

为了完成,让我们使用 feature-engine 来插补缺失值:

  1. 让我们设置 <st c="33240">imputer</st> 来使用 IQR 邻近规则估计分布右侧的值:

     imputer = EndTailImputer(
        imputation_method="iqr",
        tail="right",
        fold=3,
        variables=None,
    )
    

注意

要使用平均值和标准差来计算替换值,设置 <st c="33507">imputation_method="Gaussian"</st>。使用 <st c="33541">left</st> <st c="33549">right</st> <st c="33562">tail</st> 参数中指定在寻找插补值时要考虑的分布的哪一侧。

  1. 让我们将 <st c="33678">EndTailImputer()</st> 拟合到训练集,以便它学习插补的值:

     imputer.fit(X_train)
    
  2. 让我们检查 学习到的值:

     imputer.imputer_dict_
    

    之前的命令 返回一个包含用于插补 每个变量的值 的字典:

    <st c="33927">{'A2': 88.18,</st>
     <st c="33941">'A3': 27.31,</st>
     <st c="33954">'A8': 11.504999999999999,</st>
     <st c="33980">'A11': 12.0,</st>
     <st c="33993">'A14': 908.0,</st>
     <st c="34007">'A15': 1800.0}</st>
    
  3. 最后,让我们替换 缺失的值:

     X_train = imputer.transform(X_train)
    X_test = imputer.transform(X_test)
    

记住,你可以使用 <st c="34219">X_train[[</st><st c="34228">'A2','A3', 'A8', 'A11', '</st>``<st c="34254">A14', 'A15']].isnull().mean()</st>.

它是如何工作的...

在这个菜谱中,我们使用pandasfeature-engine在数值变量的分布末端替换了缺失值

我们根据本食谱引言中描述的公式确定了插补值。 我们使用 pandas <st c="34569">quantile()</st> 来找到特定的分位数值,或者使用 <st c="34617">pandas</st> <st c="34623">mean()</st> <st c="34635">std()</st> 来获取平均值和标准差。 使用 pandas <st c="34690">fillna()</st> 我们替换了缺失值。

使用 <st c="34762">EndTailImputer()</st> <st c="34784">feature-engine</st>替换缺失值,我们将 <st c="34807">distribution</st> 设置为 <st c="34823">iqr</st> 以根据 IQR 邻近规则计算值。 <st c="34889">tail</st> 设置为 <st c="34901">right</st> 后,转换器从分布的右侧找到了插补值。 使用 <st c="34992">fit()</st>,插补器学习并存储了在 <st c="35083">imputer_dict_</st> 属性中的插补值。 使用 <st c="35113">transform()</st>,我们替换了缺失的 值,返回 DataFrames。

标记插补值

在前面的食谱中,我们专注于使用它们值的估计来替换缺失数据。 此外,我们还可以向标记 缺失值的观测值添加缺失指示符。

缺失指示符是一个二元变量,它取值为 <st c="35451">1</st> <st c="35456">True</st> 以指示值是否缺失,否则取值为 <st c="35506">0</st> <st c="35511">False</st> 在同时用缺失指示符标记缺失观测值的同时,用平均值、中位数或最频繁的类别替换缺失观测值是一种常见的做法。 在本食谱中,我们将学习如何使用 <st c="35778">pandas</st> <st c="35786">scikit-learn</st> <st c="35800">和</st> feature-engine`添加缺失指示符。

如何操作...

让我们先进行一些导入并加载数据:

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

     import pandas as pd
    import numpy as np
    from sklearn.model_selection import train_test_split
    from sklearn.impute import SimpleImputer
    from sklearn.compose import ColumnTransformer
    from sklearn.pipeline import Pipeline
    from feature_engine.imputation import(
        AddMissingIndicator, CategoricalImputer,
        MeanMedianImputer
    )
    
  2. 让我们加载并分割 *技术 * *要求 * 部分中描述的 数据集:

     data = pd.read_csv("credit_approval_uci.csv")
    X_train, X_test, y_train, y_test = train_test_split(
        data.drop("target", axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  3. 让我们将变量名捕获到一个 列表中:

     varnames = ["A1", "A3", "A4", "A5", "A6", "A7", "A8"]
    
  4. 让我们为缺失指示器创建名称并将它们存储到一个 列表中:

     indicators = [f"{var}_na" for var in varnames]
    

    如果我们执行 <st c="36761">indicators</st>,我们将看到我们将用于新变量的名称: <st c="36830">['A1_na', 'A3_na', 'A4_na', 'A5_na', 'A6_na', '</st>``<st c="36877">A7_na', 'A8_na']</st>

  5. 让我们复制原始的 DataFrame:

     X_train_t = X_train.copy()
    X_test_t = X_test.copy()
    
  6. 让我们添加 缺失指示器:

     X_train_t[indicators] =X_train[
        varnames].isna().astype(int)
    X_test_t[indicators] = X_test[
        varnames].isna().astype(int)
    

注意

如果您希望指示器具有 <st c="37190">True</st> <st c="37199">False</st> 作为值,而不是 <st c="37226">0</st> <st c="37232">1</st>,请在 <st c="37242">astype(int)</st> 中移除 步骤 6

  1. 让我们检查 结果 DataFrame:

     X_train_t.head()
    

    我们可以在以下图像的 DataFrame 右侧看到新添加的变量: (图像)

图 1.4 – 带有缺失指示器的 DataFrame

图 1.4 – 带有缺失指示器的 DataFrame

现在,让我们使用 Feature-engine 添加缺失指示器。

  1. 设置插补器,为每个具有 缺失数据的变量添加二进制指示器:

     imputer = AddMissingIndicator(
        variables=None, missing_only=True
        )
    
  2. 将插补器拟合到训练集,以便它找到具有 缺失数据的变量:

     imputer.fit(X_train)
    

注意

如果我们执行 <st c="38202">imputer.variables_</st>,我们会找到将添加缺失指示器的变量。

  1. 最后,让我们添加 缺失指示器:

     X_train_t = imputer.transform(X_train)
    X_test_t = imputer.transform(X_test)
    

    到目前为止,我们只是添加了缺失指示器。 但我们变量中仍然有缺失数据。 我们需要用数字来替换它们。 在本教程的剩余部分,我们将结合使用缺失指示器与均值和 众数插补。

  2. 让我们创建一个 管道,将缺失指示器添加到分类和数值变量中,然后使用最频繁的类别对分类变量进行插补,使用均值对数值变量进行插补:

     pipe = Pipeline([
        ("indicators",
            AddMissingIndicator(missing_only=True)),
        ("categorical", CategoricalImputer(
            imputation_method="frequent")),
        ("numerical", MeanMedianImputer()),
    ])
    

注意

<st c="39031">feature-engine</st> <st c="39046">填充器会自动识别数值或类别变量。</st> <st c="39115">因此在这种情况下不需要切片数据或传递变量名作为参数到</st> <st c="39172">这个转换器的参数中。</st>

  1. <st c="39227">让我们添加指示器并填充</st> <st c="39264">缺失值:</st>

     X_train_t = pipe.fit_transform(X_train)
    X_test_t = pipe.transform(X_test)
    

<st c="39353">注意</st>

使用 <st c="39358">Use</st> <st c="39363">X_train_t.isnull().sum()</st> <st c="39387">来验证没有数据缺失。</st> <st c="39434">执行</st> <st c="39442">X_train_t.head()</st> <st c="39458">以查看</st> <st c="39480">结果数据集。</st>

<st c="39499">最后,让我们添加缺失指示器,并利用 scikit-learn 同时用均值和最频繁的类别分别填充数值和类别变量,</st> <st c="39657">实现填充:</st>

  1. <st c="39680">让我们创建一个包含</st> <st c="39713">数值和</st> <st c="39735">类别变量</st> <st c="39735">名称的列表:</st>

     numvars = X_train.select_dtypes(
        exclude="O").columns.to_list()
    catvars = X_train.select_dtypes(
        include="O").columns.to_list()
    
  2. <st c="39885">让我们设置一个管道来执行均值和频繁类别填充,同时标记</st> <st c="39977">缺失数据:</st>

     pipe = ColumnTransformer([
        ("num_imputer", SimpleImputer(
            strategy="mean",
            add_indicator=True),
        numvars),
        ("cat_imputer", SimpleImputer(
            strategy="most_frequent",
            add_indicator=True),
        catvars),
    ]).set_output(transform="pandas")
    
  3. <st c="40218">现在,让我们执行</st> <st c="40240">填充:</st>

     X_train_t = pipe.fit_transform(X_train)
    X_test_t = pipe.transform(X_test)
    

<st c="40329">确保探索</st> <st c="40350">X_train_t.head()</st> <st c="40367">以熟悉</st> <st c="40393">管道的输出。</st>

<st c="40411">它是如何工作的...</st>

<st c="40427">要使用 pandas 添加缺失</st> <st c="40442">指示器</st> <st c="40453">,我们使用了</st> <st c="40476">isna()</st> <st c="40482">,它创建了一个新向量,如果存在缺失值则分配值为</st> <st c="40534">True</st> <st c="40538">,否则为</st> <st c="40571">False</st> <st c="40576">。</st> <st c="40588">我们使用了</st> <st c="40596">astype(int)</st> <st c="40607">将布尔向量转换为具有值</st> <st c="40671">1</st> <st c="40672">和</st> <st c="40676">0</st> <st c="40677">的二进制向量。</st>

<st c="40678">要使用</st> <st c="40711">feature-engine</st> <st c="40725">添加缺失指示器,我们使用了</st> <st c="40735">AddMissingIndicator()</st> <st c="40756">。使用</st> <st c="40763">fit()</st> <st c="40768">转换器找到了具有缺失数据的变量。</st> <st c="40824">使用</st> <st c="40829">transform()</st> <st c="40840">它将缺失指示器添加到训练集和测试集的右侧。</st>

为了依次添加缺失指标然后替换 <st c="40977">nan</st> 值,我们将 Feature-engine 的 <st c="41062">AddMissingIndicator()</st>, <st c="41085">CategoricalImputer()</st>, 和 <st c="41111">MeanMedianImputer()</st> 集成在一个 <st c="41140">pipeline</st>中。<st c="41154">fit()</st> 方法从<st c="41176">pipeline</st> 中使转换器找到包含<st c="41231">nan</st> 的变量,并计算数值变量的平均值和分类变量的众数。 <st c="41328">transform()</st> 方法从<st c="41360">pipeline</st> 中使转换器添加缺失指标,然后用它们的估计值替换缺失值。

注意

Feature-engine 转换返回的 DataFrames 保留了变量的原始名称和顺序。 <st c="41588">Scikit-learn 的ColumnTransformer()`,另一方面,会改变结果数据中变量的名称和顺序。

最后,我们使用<st c="41815">scikit-learn</st>添加缺失指标并用平均值和最频繁的类别替换缺失数据。我们排列了两个<st c="41858">SimpleImputer()</st>实例,第一个用平均值填充数据,第二个用最频繁的类别填充数据。 <st c="41977">在两种情况下,我们将add_indicator <st c="42016">参数设置为True<st c="42034">以添加缺失指标。</st>我们将<st c="42077">SimpleImputer()</st> 包装在<st c="42098">ColumnTransformer()</st> 中,以专门修改数值或分类变量。 <st c="42177">然后我们使用fit() <st c="42199">和transform() <st c="42215">方法从pipeline` 中训练转换器,然后添加指标并替换缺失数据。

当返回 DataFrames 时,ColumnTransformer() 会更改变量的名称及其顺序。 查看通过执行 步骤 15 的结果 <st c="42477">X_train_t.head()</st>。你会看到每个步骤的名称被添加为变量的前缀,以标记每个变量被哪个转换器修改。 然后, <st c="42658">num_imputer__A2</st> 由管道的第一步返回,而 <st c="42728">cat_imputer__A12</st> 由管道的第二步返回

还有更多...

Scikit-learn 有 一个 <st c="42829">MissingIndicator()</st> 转换器,它只是添加缺失指示器。 在文档中查看它: https://scikit-learn.org/stable/modules/generated/sklearn.impute.MissingIndicator.html 并在相关的 GitHub 仓库中找到一个示例 https://github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch01-missing-data-imputation/Recipe-06-Marking-imputed-values.ipynb

实现前向和后向填充

时间序列数据也显示缺失值。 为了在时间序列中插补缺失数据,我们使用特定的方法。 前向填充插补涉及在数据序列中用最接近的前一个非缺失值填充数据集中的缺失值。 换句话说,我们将最后一个观察到的值向前传递到下一个有效值。 后向填充插补涉及用数据序列中跟随它的下一个非缺失值填充缺失值。 换句话说,我们将最后一个有效值向后传递到其前面的 有效值。

在这个菜谱中,我们将使用前向和后向填充替换时间序列中的缺失数据。

如何操作...

让我们首先导入所需的 库和时间 序列数据集:

  1. 让我们导入 <st c="44023">pandas</st> <st c="44034">matplotlib</st>:

     import matplotlib.pyplot as plt
    import pandas as pd
    
  2. 让我们加载我们在 *技术要求 部分中描述的航空乘客数据集,并显示时序的前五行:

     df = pd.read_csv(
        "air_passengers.csv",
        parse_dates=["ds"],
        index_col=["ds"],
    )
    print(df.head())
    

    我们在以下输出中看到时序数据:

     <st c="44389">y</st>
    <st c="44391">ds</st>
    <st c="44393">1949-01-01  112.0</st>
    <st c="44410">1949-02-01  118.0</st>
    <st c="44427">1949-03-01</st> <st c="44438">132.0</st>
    <st c="44444">1949-04-01  129.0</st>
    <st c="44461">1949-05-01  121.0</st>
    

注意

您可以通过执行 df.isnull().mean() 确定缺失数据的百分比。

  1. 让我们绘制时序图以查找任何明显 的数据间隙:

     ax = df.plot(marker=".", figsize=[10, 5], legend=None)
    ax.set_title("Air passengers")
    ax.set_ylabel("Number of passengers")
    ax.set_xlabel("Time")
    

    前面的代码返回以下图表,其中我们看到了数据缺失的时间间隔:

图 1.5 – 显示缺失值的时序数据

图 1.5 – 显示缺失值的时序数据

  1. 让我们通过将任何区间内最后观察到的值携带到下一个有效值来填充缺失数据:

     df_imputed = df.ffill()
    

    您可以通过执行 df_imputed.isnull().sum() 来验证缺失数据的缺失:

  2. 现在让我们绘制完整的数据集,并将用于 插补的值叠加为虚线:

     ax = df_imputed.plot(
        linestyle="-", marker=".", figsize=[10, 5])
    df_imputed[df.isnull()].plot(
        ax=ax, legend=None, marker=".", color="r")
    ax.set_title("Air passengers")
    ax.set_ylabel("Number of passengers")
    ax.set_xlabel("Time")
    

    前面的代码返回以下图表,其中我们看到了用于替换 <st c="45647">nan</st> 的值,并以虚线形式叠加在连续时间序列线之间

图 1.6 – 使用最后观察到的值(虚线)替换缺失值的时序数据

图 1.6 – 使用最后观察到的值(虚线)替换缺失值的时序数据

  1. 或者,我们可以使用 后向填充来填充缺失数据:

     df_imputed = df.bfill()
    

    如果我们绘制插补数据集,并将插补值叠加到我们 在第 5 步 所做的那样,我们将看到以下图表:

图 1.7 – 使用后向填充(虚线)替换缺失值的时序数据

图 1.7 – 使用后向填充(虚线)替换缺失值的时序数据

注意

用于插补的值的高度在 图 1.6 和 1.7 中不同。在 图 1**.6 中,我们向前携带最后一个值,因此高度较低。 图 1**.7 中,我们向后携带下一个值,因此高度 较高。

我们现在已经获得了完整的 数据集,我们可以使用它进行时间序列分析和建模。

它是如何工作的...

<st c="46672">pandas</st> <st c="46679">ffill()</st> 在时间序列中的任何时间间隔中取最后一个观测到的值,并将其传播到下一个观测值。 因此,在 图 1**.6 中,我们看到对应于最后一个观测高度缺失值的高度虚线覆盖。

<st c="46934">pandas</st> <st c="46941">bfill()</st> 在时间序列中的任何时间间隔中取下一个有效值,并将其传播到之前观测到的值。 因此,在 图 1**.7 中,我们看到对应于下一个观测高度缺失值的高度虚线覆盖。

默认情况下, <st c="47223">ffill()</st> <st c="47235">bfill()</st> 将插补所有有效观测值之间的值。 我们可以通过设置限制,使用两种方法中的<st c="47406">limit</st> <st c="47411">参数来限制插补到任何间隔中的最大数据点数。 例如, <st c="47452">ffill(limit=10)</st> 将只替换任何间隔中的前 10 个数据点。

执行插值

我们可以通过在两个非缺失数据点之间进行插值来对时间序列中的缺失数据进行插补。 插值是通过函数估计一个或多个值的方法。 在线性插值中,我们在最后一个观测值和下一个有效点之间拟合一个线性函数。 在样条插值中,我们在最后一个和下一个观测值之间拟合一个低阶多项式。 使用插值的想法是获得对缺失数据的更好估计。 的缺失数据。

在这个菜谱中,我们将在一个时间序列中执行线性插值和样条插值。

如何做...

让我们首先导入所需的库和时间序列数据集。 系列数据集。

  1. 让我们导入 <st c="48215">pandas</st> <st c="48226">matplotlib</st>:

     import matplotlib.pyplot as plt
    import pandas as pd
    
  2. 让我们加载在技术 要求 部分中描述的时间序列数据:

     df = pd.read_csv(
        "air_passengers.csv",
        parse_dates=["ds"],
        index_col=["ds"],
    )
    

注意

您可以通过绘制时间序列来查找数据缺口,就像我们在 步骤 3 中的 实现向前和向后填充 *食谱中所做的那样。

  1. 让我们通过 线性插值来插补缺失数据:

     df_imputed = df.interpolate(method="linear")
    

注意

如果行之间的时间间隔不均匀,则应将 <st c="48743">方法</st> 设置为 <st c="48767">时间</st> 以实现线性拟合。

您可以通过 执行 <st c="48853">df_imputed.isnull().sum()</st>来验证缺失数据的缺失。

  1. 现在让我们绘制完整的数据集,并将用于 插补的值以虚线形式叠加:

     ax = df_imputed.plot(
        linestyle="-", marker=".", figsize=[10, 5])
    df_imputed[df.isnull()].plot(
        ax=ax, legend=None, marker=".", color="r")
    ax.set_title("Air passengers")
    ax.set_ylabel("Number of passengers")
    ax.set_xlabel("Time")
    

    前面的代码返回以下图表,其中我们看到了用于替换 <st c="49297">nan</st> 的值,并以虚线形式绘制在时间序列的连续线之间:

图 1.8 – 时间序列数据,其中缺失值通过在最后和下一个有效数据点之间进行线性插值来替换(虚线)

图 1.8 – 时间序列数据,其中缺失值通过在最后和下一个有效数据点之间进行线性插值来替换(虚线)

  1. 或者,我们可以通过进行样条插值来插补缺失数据。 我们将使用二次多项式:

     df_imputed = df.interpolate(method="spline", order=2)
    

    如果我们绘制插补数据集,并将插补值叠加,就像我们在 步骤 4中所做的那样,我们将看到以下图表:

图 1.9 – 时间序列数据,其中缺失值通过在最后和下一个有效数据点之间拟合二次多项式来替换(虚线)

图 1.9 – 时间序列数据,其中缺失值通过在最后和下一个有效数据点之间拟合二次多项式来替换(虚线)

注意

更改插值中使用的多项式的次数,以查看替换值如何变化。

我们现在已经获得了完整的 数据集,我们可以用于分析和建模。

它是如何工作的...

<st c="50360">pandas</st> <st c="50367">interpolate()</st> 通过使用插值方法填充范围内的缺失值。 当我们设置 <st c="50464">method</st> <st c="50474">linear</st>时, <st c="50482">interpolate()</st> 将所有数据点视为等距,并在存在缺失数据的区间内使用线性拟合连接最后和下一个有效点。

注意

如果你想要执行线性插值,但你的数据点不是等距的,将 <st c="50725">method</st> 设置为 <st c="50735">time</st>

然后我们通过将 <st c="50823">method</st> 设置为 <st c="50833">spline</st> <st c="50844">order</st> 设置为 <st c="50853">2</st>来执行二次多项式样条插值。

<st c="50855">pandas</st> <st c="50862">interpolate()</st> 使用 <st c="50882">scipy.interpolate.interp1d</st> <st c="50913">scipy.interpolate.UnivariateSpline</st> 在内部实现,因此可以实现其他插值 方法。 有关更多详细信息,请查看 pandas 文档 ,网址为 https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.interpolate.html

另请参阅

虽然插值旨在比前向填充和后向填充获得更好的缺失数据估计,但如果时间序列显示出强烈的趋势和季节性,这些估计可能仍然不准确。 为了获得这些类型时间序列中缺失数据的更好估计,请查看在时间序列特征工程课程 中跟随时间序列分解后插值的 https://www.trainindata.com/p/feature-engineering-for-forecasting

通过链式方程执行多元插补

多元插补方法,与单变量插补相反,使用多个变量来估计缺失值。 链式方程多元插补 (MICE) 将数据集中每个具有缺失值的变量建模为剩余变量的函数。 该函数的输出用于替换缺失数据。

MICE 包括以下步骤:

  1. 首先,它对每个有缺失数据的变量执行简单的单变量插补。 例如, 中位数插补。

  2. 接下来,它选择一个特定的变量,比如, <st c="52233">var_1</st>,并将缺失值重新设置为缺失。

  3. 它训练一个模型来预测 <st c="52314">var_1</st> ,使用其他变量作为 输入特征。

  4. 最后,它用模型的输出替换 <st c="52408">var_1</st> 的缺失值。

MICE 重复 步骤 2 4 ,对于剩余的每个变量。

一个插补周期 一旦所有变量都被建模就结束。 MICE 执行多个插补周期,通常是 10 个。 这意味着我们重复 步骤 2 4 ,对于每个变量重复 10 次。 想法是,到周期结束时,我们应该找到了每个变量的缺失数据的最佳可能估计。

注意

多元插补 可以在我们不想扭曲变量分布的情况下,作为单变量插补的有用替代方案。 当我们对获得缺失数据的良好估计感兴趣时,多元插补也非常有用。

在这个配方中,我们将使用 scikit-learn 实现 MICE。

如何做...

为了开始这个配方,让我们导入所需的库并加载数据:

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

     import pandas as pd
    import matplotlib.pyplot as plt
    from sklearn.model_selection import train_test_split
    from sklearn.linear_model import BayesianRidge
    from sklearn.experimental import (
        enable_iterative_imputer
    )
    from sklearn.impute import (
        IterativeImputer,
        SimpleImputer
    )
    
  2. 让我们从描述在 *技术 * *要求 * 部分的数据集中加载一些数值变量:

     variables = [
        "A2", "A3", "A8", "A11", "A14", "A15", "target"]
    data = pd.read_csv(
        "credit_approval_uci.csv",
        usecols=variables)
    
  3. 让我们将数据分为训练集和 测试集:

     X_train, X_test, y_train, y_test = train_test_split(
        data.drop("target", axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  4. 让我们使用贝叶斯回归创建一个 MICE 插补器,指定迭代周期数并设置 <st c="54108">random_state</st> 以实现可重复性:

     imputer = IterativeImputer(
        estimator= BayesianRidge(),
        max_iter=10,
        random_state=0,
    ).set_output(transform="pandas")
    

注意

<st c="54264">IterativeImputer()</st> 包含其他有用的参数。 例如,我们可以使用 <st c="54385">initial_strategy</st> 参数指定第一个插补策略。 我们可以从均值、中位数、众数或任意插补中进行选择。 我们还可以指定我们想要如何遍历变量,是随机遍历还是从缺失值最少的变量遍历到缺失值最多的变量。

  1. 让我们拟合 <st c="54641">IterativeImputer()</st> ,以便它训练 估计器 来预测每个变量中的缺失值:

     imputer.fit(X_train)
    

注意

我们可以使用任何回归模型来估计缺失数据 ,使用 <st c="54833">IterativeImputer()</st>

  1. 最后,让我们在训练集和 测试集中填充缺失值:

     X_train_t = imputer.transform(X_train)
    X_test_t = imputer.transform(X_test)
    

注意

为了证实缺失数据的缺乏,我们可以 执行 <st c="55065">X_train_t.isnull().sum()</st>

为了总结这个方法,让我们使用简单的单变量插补方法来插补变量,并比较其对 变量分布的影响。

  1. 让我们设置 scikit-learn 的 <st c="55267">SimpleImputer()</st> 以执行均值插补,然后转换 数据集:

     imputer_simple = SimpleImputer(
        strategy="mean").set_output(transform="pandas")
    X_train_s = imputer_simple.fit_transform(X_train)
    X_test_s = imputer_simple.transform(X_test)
    
  2. 现在让我们绘制 变量 <st c="55552">A3</st> 在 MICE 插补后的直方图,然后是相同变量在 均值插补后的直方图:

     fig, axes = plt.subplots(
        2, 1, figsize=(10, 10), squeeze=False)
    X_test_t["A3"].hist(
        bins=50, ax=axes[0, 0], color="blue")
    X_test_s["A3"].hist(
        bins=50, ax=axes[1, 0], color="green")
    axes[0, 0].set_ylabel('Number of observations')
    axes[1, 0].set_ylabel('Number of observations')
    axes[0, 0].set_xlabel('A3')
    axes[1, 0].set_xlabel('A3')
    axes[0, 0].set_title('MICE')
    axes[1, 0].set_title('Mean imputation')
    plt.show()
    

    在下面的图中,我们可以看到均值插补扭曲了变量分布,更多的观测值趋向于 平均值:

图 1.10 – 变量 A3 在老鼠插补(顶部)或均值插补(底部)后的直方图,显示了后者导致的变量分布扭曲

图 1.10 – 变量 A3 在老鼠插补(顶部)或均值插补(底部)后的直方图,显示了后者导致的变量分布扭曲

它是如何工作的...

在这个菜谱中,我们使用 <st c="56529">IterativeImputer()</st><st c="56553">scikit-learn</st> 进行多元插补。当我们拟合模型时,<st c="56590">IterativeImputer()</st> 执行了我们在菜谱介绍中描述的步骤。也就是说,它使用均值对所有变量进行插补。然后它选择一个变量,将其缺失值重新设置为缺失。最后,它拟合一个贝叶斯回归器来根据其他变量估计该变量。它对每个变量重复此过程。这是插补的一个周期。我们将它设置为重复此过程 10 次。到这个过程的最后,<st c="57048">IterativeImputer()</st> 训练了一个贝叶斯回归器,用于根据数据集中的其他变量预测每个变量的值。使用 <st c="57188">transform()</st>,它使用这些贝叶斯模型的预测来插补缺失数据。

<st c="57275">Iter</st><st c="57280">ativeImputer()</st> 只能基于数值变量对数值变量中的缺失数据进行插补。如果你想使用分类变量作为输入,你需要先对它们进行编码。然而,请注意,它只会执行回归。因此,它不适合估计离散或分类变量中的缺失数据。

参见

要了解更多关于 MICE 的信息,请查看以下资源:

使用最近邻估计缺失数据

使用 K-Nearest Neighbors (KNN) 进行缺失值填充涉及通过考虑 其最近邻的值来估计数据集中的缺失值,其中数据点之间的相似性基于距离度量,例如欧几里得距离。 它将缺失值分配给最近邻值的平均值,并按 它们的距离进行加权。

考虑以下包含 4 个变量(列)和 11 个观测值(行)的数据集。 我们想要填充第二个变量第五行的暗值。 首先,我们找到该行的 k 个最近邻,其中 k=3 在我们的例子中,并且它们通过矩形框(中间面板)突出显示。 接下来,我们取最接近的邻居显示的平均值,用于 变量 2。

图 1.11 – 显示要填充的值(深色框)、要填充的值的三条最近行(正方形框)以及用于填充的平均值考虑的值的图

图 1.11 – 显示要填充的值(深色框)、要填充的值的三条最近行(正方形框)以及用于填充的平均值考虑的值的图

缺失值填充的值由 (value1 × w1 + value2 × w2 + value3 × w3) / 3 给出,其中 w1、w2 和 w3 与数据到要填充的数据点的距离成比例 的邻居距离到要填充的数据 点的距离

在这个菜谱中,我们将使用 scikit-learn 执行 KNN 缺失值填充

如何做到这一点...

为了进行菜谱,让我们导入所需的库并准备 数据:

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

     import matplotlib.pyplot as plt
    import pandas as pd
    from sklearn.model_selection import train_test_split
    from sklearn.impute import KNNImputer
    
  2. 让我们加载在 技术要求 部分中描述的数据集(仅一些 数值变量):

     variables = [
        "A2", "A3", "A8", "A11", "A14", "A15", "target"]
    data = pd.read_csv(
        "credit_approval_uci.csv",
        usecols=variables,
    )
    
  3. 让我们将数据分为训练集和 测试集:

     X_train, X_test, y_train, y_test = train_test_split(
        data.drop("target", axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  4. 让我们设置缺失值填充器 以用其最近五个邻居的加权平均值替换缺失数据

     imputer = KNNImputer(
        n_neighbors=5, weights="distance",
    ).set_output(transform="pandas")
    

注意

替换值可以计算为 k 近邻的均匀平均值,通过设置权重 均匀 ,或者像我们在食谱中做的那样,计算加权平均值。 权重基于邻居到要插补的观测值的距离。 最近邻的权重更大。

  1. 找到 最近邻:

     imputer.fit(X_train)
    
  2. 用邻居显示的值的加权平均值替换缺失值:

     X_train_t = imputer.transform(X_train)
    X_test_t = imputer.transform(X_test)
    

结果是包含缺失数据已被替换的pandas DataFrame。

它是如何工作的...

在这个方法中,我们用每个观测值的 k 近邻的平均值替换了缺失数据 我们设置了 <st c="60938">KNNImputer()</st> 来根据欧几里得距离找到每个观测值的五个最近邻。 替换值被估计为要插补变量的五个最近邻显示值的加权平均值。 使用 <st c="61179">transform()</st>,插补器计算了替换值并替换了 缺失数据。

第三章:0 2

编码分类变量

<st c="146">业主</st> 变量<st c="185">owner</st><st c="190">非业主</st> 是分类变量,同样,<st c="235">婚姻状况</st> 变量<st c="278">从未结婚</st><st c="293">已婚</st><st c="302">离婚</st><st c="316">丧偶</st>也是分类变量。在一些分类变量中,标签具有内在顺序;例如,在<st c="412">学生成绩</st> 变量中,<st c="452">A</st><st c="455">B</st><st c="458">C</st><st c="465">Fail</st> 是按顺序排列的,其中<st c="488">A</st> 是最高等级,而<st c="518">Fail</st> 是最低等级。 <st c="541">这些被称为城市 <st c="712">变量,其值为伦敦<st c="748">、曼彻斯特<st c="760">、布里斯托尔<st c="769">等。</st>

分类变量的值通常编码为字符串。为了训练大多数机器学习模型,我们需要将这些字符串转换为数字。将字符串替换为数字的行为称为分类编码。在本章中,我们将讨论多种分类编码方法

本章将涵盖以下食谱

  • 通过独热编码创建二进制变量

  • 对频繁类别进行独热编码

  • 用计数或观察值的频率替换类别

  • 序数替换类别

  • 根据目标值进行序数编码

  • 实现目标均值编码

  • 使用证据权重进行编码

  • 对稀有或不频繁类别进行分组

  • 进行二元编码

技术要求

在本章中,我们将使用 <st c="1587">Matplotlib</st>, <st c="1599">pandas</st>, <st c="1607">NumPy</st>, <st c="1614">scikit-learn</st>, <st c="1628">feature-engine</st>,以及 Category Encoders Python 库。 如果你需要安装 Python,免费的 Anaconda Python 发行版(https://www.anaconda.com/)包括了大多数数值 计算库

<st c="1827">feature-engine</st> 可以使用 <st c="1865">pip</st>安装:

 pip install feature-engine

如果你使用 Anaconda,你可以使用 <st c="1935">feature-engine</st> <st c="1955">conda</st>安装:

 conda install -c conda-forge feature_engine

要安装 Category Encoders,请使用以下 <st c="2041">pip</st> 命令:

 pip install category_encoders

我们将使用来自 UCI 机器学习仓库 信用审批* 数据集 (https://archive.ics.uci.edu/),该数据集受 CC BY 4.0 创意共享许可协议保护: https://creativecommons.org/licenses/by/4.0/legalcode。你可以在以下链接找到数据集: http://archive.ics.uci.edu/dataset/27/credit+approval

我下载并修改了如本 笔记本 https://github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch02-categorical-encoding/credit-approval-dataset.ipynb](https://github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch02-categorical-encoding/credit-approval-dataset.ipynb)所示的数据。

你可以在附带的 GitHub 仓库中找到修改后的数据集: https://github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch02-categorical-encoding/

注意

在编码分类变量之前,你可能想要填充它们的缺失数据。查看 第一章中关于分类变量填充方法的内容 *填充 缺失 数据

通过独热编码创建二进制变量

<st c="3223">1</st> 如果类别存在,否则 <st c="3256">0</st>

以下 表格显示了 <st c="3337">Smoker</st> 变量的一热编码表示,其类别为 <st c="3376">Smoker</st> <st c="3387">Non-Smoker</st>

Figure 2.1 – One-hot encoded representation of the Smoker variable

图 2.1 – Smoker 变量的一热编码表示

如图 图 2.1**.1所示,从 <st c="3575">Smoker</st> 变量中,我们可以导出一个用于 <st c="3628">Smoker</st>的二进制变量,该变量显示吸烟者的值为 <st c="3661">1</st> ,或者对于不吸烟者,导出一个 <st c="3703">Non-Smoker</st>的二进制变量,该变量取值为 <st c="3740">1</st>

对于具有 <st c="3778">Color</st> 值的分类变量 <st c="3824">red</st> <st c="3829">blue</st>,和 <st c="3839">green</st>,我们可以创建三个变量,分别称为 <st c="3883">red</st> <st c="3888">blue</st>,和 <st c="3898">green</st>。这些变量将在观察值对应相应颜色时赋予值为 <st c="3949">1</st> ,如果不对应,则赋予值为 <st c="4011">0</st>

具有 k 个唯一类别的分类变量可以使用 k-1 个二进制变量进行编码。 对于 <st c="4124">Smoker</st> k 2 ,因为它包含两个标签(<st c="4166">Smoker</st> <st c="4178">Non-Smoker</st>),所以我们只需要一个二进制变量(k - 1 = 1)来捕捉所有信息。 对于 <st c="4282">Color</st> 变量,它有 3 个类别(k = 3 <st c="4331">red</st> <st c="4336">blue</st>,和 <st c="4346">green</st>),我们需要 2 个(k - 1 = 2)二进制 变量来捕捉所有信息,以便以下情况发生:

  • 如果观察结果是红色,它将被 <st c="4511">红色</st> 变量捕获(<st c="4525">红色</st> = <st c="4532">1</st>, <st c="4535">蓝色</st> = <st c="4542">0</st>

  • 如果观察结果是蓝色,它将被 <st c="4599">蓝色</st> 变量捕获(<st c="4614">红色</st> = <st c="4621">0</st>, <st c="4624">蓝色</st> = <st c="4631">1</st>

  • 如果观察结果是绿色,它将被 <st c="4704">红色</st> <st c="4712">蓝色</st> (<st c="4718">红色</st> = <st c="4724">0</st>, <st c="4727">蓝色</st> = <st c="4734">0</st>)

编码 k-1 二进制变量非常适合线性模型。 在某些情况下,我们可能更喜欢使用 k 二进制变量来编码分类变量:

  • 在训练决策树时,由于它们不会同时评估整个特征空间

  • 当递归选择特征时

  • 当确定变量中每个类别的 重要性时

在这个菜谱中,我们将比较 <st c="5188">pandas</st> <st c="5196">scikit-learn</st> <st c="5210">featur</st><st c="5220">e-engine</st>的 one-hot 编码实现。

如何做到这一点...

首先,让我们做一些导入并准备数据:

  1. 导入 <st c="5312">pandas</st> <st c="5327">train_test_split</st> 函数 <st c="5358">scikit-learn</st>

     import pandas as pd
    from sklearn.model_selection import train_test_split
    
  2. 让我们加载信用 批准数据集:

     data = pd.read_csv("credit_approval_uci.csv")
    
  3. 让我们将数据分为训练集和 测试集:

     X_train, X_test, y_train, y_test = train_test_split(
        data.drop(labels=["target"], axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  4. 让我们 检查 <st c="5765">A4</st> 变量的 唯一类别:

     X_train["A4"].unique()
    

    我们可以在以下输出中看到 <st c="5833">A4</st> 的独特值:

    <st c="5923">A4</st> into *<st c="5930">k-1</st>* binary variables using <st c="5958">pandas</st> and then inspect the first five rows of the resulting DataFrame:
    
    

    dummies = pd.get_dummies(

    X_train["A4"], drop_first=True)
    

    dummies.head()

    
    

注意

使用<st c="6113">pandas</st> <st c="6122">get_dummies()</st>,我们可以通过<st c="6193">dummy_na</st> 参数忽略或编码缺失数据。 通过设置<st c="6224">dummy_na=True</st>,缺失数据将被编码到一个新的二进制变量中。 要将变量编码为 k 个虚拟变量,请使用<st c="6337">drop_first=False</st>

在这里,我们可以看到 步骤 5的输出,其中每个标签现在都是一个 二进制变量:

 <st c="6444">Missing        l        u        y</st>
<st c="6458">596     False</st> <st c="6468">False   True  False</st>
<st c="6485">303     False  False   True  False</st>
<st c="6512">204     False  False</st> <st c="6529">False   True</st>
<st c="6539">351     False  False  False   True</st>
<st c="6566">118     False  False   True  False</st>
  1. 现在,让我们 将所有分类变量编码为 k-1 二进制值:

     X_train_enc = pd.get_dummies(X_train, drop_first=True)
    X_test_enc = pd.get_dummies(X_test, drop_first=True)
    

注意

<st c="6773">pandas</st> <st c="6783">get_dummies()</st>默认情况下将编码对象、字符串或类别类型的所有变量。 要编码变量的子集,请将变量名列表传递给<st c="6953">columns</st> 参数。

  1. 让我们检查一下生成的 DataFrame 的前五行: resulting DataFrame:

     X_train_enc.head()
    

注意

当编码多个变量时,<st c="7097">get_dummies()</st> 会捕获变量名 – 比如,<st c="7145">A1</st> – 并在类别名称前加上下划线来标识生成的 二进制变量。

我们可以在以下输出中看到二进制变量: following output:

图 2.2 – 一个转换后的 DataFrame,显示了数值变量后跟分类变量的独热编码表示

图 2.2 – 一个转换后的 DataFrame,显示了数值变量后跟分类变量的独热编码表示

注意

<st c="8116">pandas</st> <st c="8126">get_dummies()</st>将为 DataFrame 中看到的每个类别创建一个二进制变量。 因此,如果训练集中比测试集中有更多类别,<st c="8281">get_dummies()</st> 将在转换后的训练集中返回比转换后的测试集更多的列,反之亦然。 为了避免这种情况,最好使用<st c="8462">scikit-learn</st> <st c="8478">feature-engine</st>进行独热编码。

让我们使用<st c="8526">scikit-learn</st> 来进行独热编码。

  1. 让我们 <st c="8600">scikit-learn</st>导入编码器 <st c="8577">ColumnTransformer</st>

     from sklearn.preprocessing import OneHotEncoder
    from sklearn.compose import ColumnTransformer
    
  2. 让我们创建一个包含分类变量名称的列表:

     cat_vars = X_train.select_dtypes(
        include="O").columns.to_list()
    
  3. 让我们设置编码器以创建 k-1 个二进制变量:

     encoder = OneHotEncoder(drop="first",
        sparse_output=False)
    

注意

要将变量编码为 *k * 个虚拟变量,将 <st c="9004">drop</st> 参数设置为 <st c="9022">None</st>。要将仅二进制变量编码为 k-1,将 <st c="9078">drop</st> 参数设置为 <st c="9096">if_binary</st>。后者是有用的,因为将二进制变量编码为 k 个虚拟变量 是多余的。

  1. 让我们 将编码限制在 分类变量

     ct = ColumnTransformer(
        [("encoder", encoder, cat_vars)],
        remainder="passthrough",
        force_int_remainder_cols=False,
    ).set_output(transform="pandas")
    
  2. 让我们拟合编码器,使其识别要编码的类别:

     ct.fit(X_train)
    
  3. 让我们检查将用二进制变量表示的类别:

     ct.named_transformers_["encoder"].categories_
    

    该转换器将为以下类别添加二进制变量:

图 2.3 – 将编码为二进制变量的类别数组(每个变量一个数组)

图 2.3 – 将编码为二进制变量的类别数组(每个变量一个数组)

注意

<st c="10244">scikit-learn</st> <st c="10261">OneHotEncoder()</st> 将仅编码从训练集中学习到的类别。 如果测试集中有新的类别,我们可以指示编码器忽略它们、返回错误或将它们替换为不常见的类别,通过将 <st c="10503">handle_unknown</st> 参数设置为 <st c="10531">ignore</st><st c="10539">error</st><st c="10546">或</st> infrequent_if_exists`

  1. 让我们 编码分类变量:

     X_train_enc = ct.transform(X_train)
    X_test_enc = ct.transform(X_test)
    

    确保通过执行 <st c="10727">X_test_enc.head()</st> 来检查结果。

  2. 为了熟悉输出,让我们打印结果 DataFrame 的变量名称:

     ct.get_feature_names_out()
    

    在以下图像中,我们看到转换后的 DataFrame 中的变量名称:

图 2.4 – 转换后的 DataFrame 中的变量名称数组

图 2.4 – 结果 DataFrame 中变量的名称数组

注意

<st c="11779">ColumnTransformer()</st> 在转换过程中更改变量的名称和顺序。 如果变量已被编码,它将附加<st c="11919">encoder</st> 前缀;如果变量未被修改,它将附加<st c="11991">remainder</st> 前缀。

为了总结这个配方,让我们使用<st c="12063">featu</st><st c="12068">re-engine</st>`进行独热编码:

  1. 让我们 导入 编码器 来自 <st c="12115">f</st>``<st c="12116">eature-engine</st>:

     from feature_engine.encoding import OneHotEncoder
    
  2. 让我们设置编码器,使其返回*<st c="12227">k-1</st>* 二元变量:

     ohe_enc = OneHotEncoder(drop_last=True)
    

注意

<st c="12293">feature-engine</st>OneHotEncoder()<st c="12327">默认情况下编码所有分类变量。</st> <st c="12374">要编码变量的子集,请传递变量名称列表:</st>OneHotEncoder(variables=["A1", "A4"]<st c="12482">)。</st> <st c="12486">要编码数值变量,将ignore_format <st c="12538">参数设置为True` 或将变量转换为对象。

  1. 让我们将编码器拟合到训练集,以便它学习要编码的类别和变量: 要编码的:

     ohe_enc.fit(X_train)
    

注意

要将二元变量编码为*<st c="12746">k-1</st>*,并将其他分类变量编码为*<st c="12788">k</st> 虚变量,将<st c="12807">drop_last_binary</st> 参数设置为<st c="12837">True</st>

  1. 让我们探索将要 编码的变量:

     ohe_enc.variables_
    

    Transformer 找到了并存储了对象或分类类型的变量,如下面的输出所示: 以下输出:

    ['A1', 'A4', 'A5', 'A6', 'A7', 'A9', 'A10', 'A12', 'A13']
    
  2. 让我们 探索将创建虚变量的类别:

     ohe_enc.encoder_dict_
    

    以下 字典包含将在每个变量中编码的类别:

     <st c="13269">{'A1': ['a', 'b'],</st>
     <st c="13288">'A4': ['u', 'y', 'Missing'],</st>
     <st c="13317">'A5': ['g', 'p', 'Missing'],</st>
     <st c="13346">'A6': ['c', 'q', 'w', 'ff', 'm', 'i', 'e', 'cc', 'x', 'd', 'k', 'j', 'Missing', 'aa'],</st>
     <st c="13433">'A7': ['v', 'ff', 'h', 'dd', 'z', 'bb', 'j', 'Missing', 'n'],</st>
     <st c="13495">'A9': ['t'],</st>
     <st c="13508">'A10': ['t'],</st>
     <st c="13522">'A12': ['t'],</st>
     <st c="13536">'A13': ['g', 's']}</st>
    
  3. 让我们在训练集和测试集中编码分类变量:

     X_train_enc = ohe_enc.transform(X_train)
    X_test_enc = ohe_enc.transform(X_test)
    

    如果我们执行 <st c="13713">X_train_enc.head()</st>,我们将看到以下 DataFrame:

图 2.5 – 带有数值变量和分类变量独热编码表示的转换后的 DataFrame

图 2.5 – 转换后的 DataFrame,数值变量之后跟着分类变量的独热编码表示

注意 如何 <st c="14325">A4</st> 分类变量 被替换为 <st c="14368">A4_u</st><st c="14374">A4_y</st>,以及 等等。

注意

我们可以通过执行 <st c="14468">ohe_enc.get</st><st c="14489">_feature_names_out()</st>来获取转换后的数据集中所有变量的名称。

它的工作原理...

在这个菜谱中,我们使用 <st c="14607">pandas</st><st c="14615">scikit-learn</st>,和 <st c="14629">feature-engine</st>对分类变量进行了独热编码。

<st c="14648">pandas</st> <st c="14658">get_dummies()</st> 将分类变量替换为表示每个类别的二进制变量集。 当用于整个数据集时,它返回数值变量,然后是每个变量中看到的每个类别的独热编码表示,变量类型为对象、字符串, 或分类。

注意

<st c="14976">pandas</st> 将为数据集中看到的每个类别返回二进制变量。 在实际应用中,为了避免数据泄露并预测部署情况,我们只想为训练集中看到的类别返回虚拟变量。 因此,使用 <st c="15228">scikit-learn</st> <st c="15245">feature-engine</st>更安全。

<st c="15260">OneHotEncoder()</st> <st c="15282">scikit-learn</st> <st c="15298">feature-engine</st> 学习了我们应用 <st c="15418">fit()</st>时应该用二进制变量表示的类别。使用 <st c="15430">transform()</st><st c="15443">scikit-learn</st> 仅返回二进制变量,而 <st c="15500">feature-engine</st> 返回数值变量,然后是分类变量的独热编码表示。

<st c="15619">scikit-learn</st> <st c="15632">’s</st> <st c="15636">OneHotEncoder()</st> <st c="15651">默认情况下编码所有变量。</st> <st c="15686">要限制编码为</st> <st c="15714">分类变量,我们</st> <st c="15740">使用了</st> <st c="15745">ColumnTransformer()</st> <st c="15764">。我们将</st> <st c="15787">transform()</st> <st c="15798">的输出</st> <st c="15802">设置为</st> <st c="15808">pandas</st> <st c="15841">,以获得结果数据作为</st> <st c="15841">DataFrame。</st>

<st c="15853">注意</st>

<st c="15858">One-hot 编码适用于线性模型。</st> <st c="15907">它还扩展了特征空间。</st> <st c="15942">如果你的数据集包含许多分类变量或高度基数变量,你可以通过仅编码最频繁的类别来限制二进制变量的数量。</st> <st c="16119">你可以使用</st> <st c="16159">scikit-learn</st> <st c="16171">和</st> <st c="16176">feature-engine</st> <st c="16190">自动完成此操作,正如我们在</st> <st c="16213">Performing one-hot encoding of freq</st><st c="16248">uent</st> <st c="16254">categories</st> <st c="16264">recipe.</st> <st c="16213">执行频繁类别 one-hot 编码</st> <st c="16254">recipe.</st> <st c="16264">配方</st> <st c="16213">中描述的。</st>

<st c="16272">还有更多...</st>

<st c="16288">我们还可以使用 Category Encoders Python</st> <st c="16361">库</st> <st c="16370">https://</st><st c="16378">contrib.scikit-learn.org/category_encoders/onehot.html</st> <st c="16433">。</st>

<st c="16434">为了限制二进制变量的数量,我们可以选择要编码的类别和要忽略的类别;查看以下</st> <st c="16579">文章</st> <st c="16588">https://www.blog.trainindata.com/one-hot-encoding-</st><st c="16638">categorical-variables</st><st c="16660">/</st><st c="16662">。</st>

<st c="16663">执行频繁类别 one-hot 编码</st>

<st c="16714">One-hot 编码</st> <st c="16732">使用二进制变量表示每个变量的类别。</st> <st c="16791">因此,高度基数变量或具有多个分类特征的数据集的 one-hot 编码可以显著扩展特征空间。</st> <st c="16935">这反过来可能会增加使用机器学习模型的计算成本或降低其性能。</st> <st c="17053">为了减少二进制变量的数量,我们可以对最频繁的类别执行 one-hot 编码。</st> <st c="17160">对顶级类别进行 one-hot 编码</st> <st c="17177">相当于将剩余的、较少出现的类别视为一个单一的、独特的类别。</st>

在这个示例中,我们将使用 <st c="17291">pandas</st><st c="17380">Scikit-l</st><st c="17396">earn</st><st c="17403">和</st> <st c="17407">feature-engine</st> <st c="17421"> 来实现最流行类别的 one-hot 编码。

如何操作...

首先,让我们导入 必要的 Python 库并准备好 数据集:

  1. 导入所需的 Python 库、函数、 和类:

     import pandas as pd
    import numpy as np
    from sklearn.model_selection import train_test_split
    
  2. 让我们加载 Credit Approval 数据集并将其分为训练集和 测试集:

     data = pd.read_csv("credit_approval_uci.csv")
    X_train, X_test, y_train, y_test = train_test_split(
        data.drop(labels=["target"], axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    

注意

需要在训练集中确定最频繁的类别。 这是为了避免 数据泄露。

  1. 让我们检查 <st c="18084">A6</st> 变量的唯一类别:

     X_train["A6"].unique()
    

    以下输出显示了 <st c="18141">A6</st> 的唯一值:

    <st c="18343">A6</st>, sort them in decreasing order, and then display the five most frequent categories:
    
    

    X_train["A6"].value_counts().sort_values(

    ascending=False).head(5)
    
    
    <st c="18496">We can</st> <st c="18503">see the five most frequent categories and the number of observations per category in the</st> <st c="18593">following output:</st>
    
    

    A6

    c 93

    q 56

    w 48

    i 41

    ff 38

    使用 步骤 4 中的代码在一个列表推导式中将 <st c="18717">A6</st> 放入一个列表中:

     top_5 = [x for x in X_train[ <st c="18816">" A6"].value_counts().sort_values(</st> **<st c="18851">ascending=False).head(5).index
    ]</st>**
    
    
    

***** 让我们为每个顶级类别向训练集和 测试集的副本添加一个二元变量:

```py
 X_train_enc = X_train.copy()
X_test_enc = X_test.copy()
for label in top_5:
    X_train_enc[f"A6_{label}"] = np.where(
        X_train["A6"] == label, 1, 0)
    X_test_enc[f"A6_{label}"] = np.where(
        X_test["A6"] == label, 1, 0)
```

+   <st c="19178">让我们显示</st> <st c="19192">原始和编码变量</st> `<st c="19203">A6</st>`<st c="19249">,在</st> <st c="19258">训练集</st>中的</st> `<st c="19201">前 10</st>` <st c="19209">行:</st>

```py
 X_train_enc[["A6"] + [f"A6_{
    label}" for label in top_5]].head(10)
```

<st c="19335">在</st> *<st c="19353">步骤 7</st>*<st c="19359">的输出中,我们可以看到</st> `<st c="19376">A6</st>` <st c="19378">变量,后面跟着</st> <st c="19405">二元变量:</st>

```py
 <st c="19422">A6  A6_c  A6_q</st> <st c="19436">A6_w  A6_i  A6_ff</st>
<st c="19451">596   c      1      0      0      0        0</st>
<st c="19467">303   q</st> <st c="19474">0      1      0      0        0</st>
<st c="19483">204   w      0      0      1      0</st> <st c="19498">0</st>
<st c="19499">351  ff      0      0      0      0        1</st>
<st c="19515">118   m      0</st> <st c="19524">0      0      0        0</st>
<st c="19531">247   q      0      1      0      0        0</st>
<st c="19547">652</st> <st c="19551">i      0      0      0      1        0</st>
<st c="19563">513   e      0      0</st> <st c="19574">0      0        0</st>
<st c="19579">230  cc      0      0      0      0        0</st>
<st c="19677">scikit-learn</st>.
```

+   <st c="19690">让我们导入</st> <st c="19704">编码器:</st>

```py
 from sklearn.preprocessing import OneHotEncoder
```

+   <st c="19764">让我们设置</st> <st c="19775">编码器以编码至少</st> `<st c="19829">39</st>` <st c="19831">个观察值中显示的类别,并将编码的类别数量限制为</st> `<st c="19890">5</st>`<st c="19894">:</st>

```py
 encoder = OneHotEncoder(
    min_frequency=39,
    max_categories=5,
    sparse_output=False,
).set_output(transform="pandas")
```

+   <st c="20010">最后,让我们</st> <st c="20025">将转换器拟合到两个高基数变量,然后转换</st> <st c="20100">数据:</st>

```py
 X_train_enc = encoder.fit_transform(X_train[
    ['A6', 'A7']])
X_test_enc = encoder.transform(X_test[['A6', 'A7']])
```

<st c="20222">如果你执行</st> `<st c="20238">X_train_enc.head()</st>` <st c="20256">,你会看到</st> <st c="20272">生成的 DataFrame:</st>****

****图 2.6 – 包含至少 39 个观察值的类别二元变量以及表示所有剩余类别的额外二元变量的转换 DataFrame

图 2.6 – 包含至少 39 个观察值的类别二元变量以及表示所有剩余类别的额外二元变量的转换 DataFrame

为了总结这个方法,让我们使用 <st c="20843">feature-engine</st>. 来编码最频繁的类别

  1. 让我们设置 单热编码器,以对 <st c="20943">A6</st> <st c="20950">A7</st> 变量的前五个最频繁类别进行编码:

     From feature_engine.encoding import OneHotEncoder
    ohe_enc = OneHotEncoder(
        top_categories=5,
        variables=["A6", "A7"]
    )
    

注意

要编码的频繁类别数量由用户任意确定。

  1. 让我们将编码器拟合到训练集,以便它学习并存储 <st c="21271">A6</st> <st c="21278">A7</st>的最频繁类别:

     ohe_enc.fit(X_train)
    
  2. 最后,让我们在训练集和测试集中对 <st c="21326">A6</st> <st c="21333">A7</st> 进行编码:

     X_train_enc = ohe_enc.transform(X_train)
    X_test_enc = ohe_enc.transform(X_test)
    

    您可以通过执行 <st c="21524">X_train_enc.head()</st>来查看转换后的 DataFrame 中的新二进制变量。您还可以通过执行 <st c="21623">ohe_enc.encoder_dict_</st>来找到编码器学习到的前五个类别。

它是如何工作的...

在这个菜谱的第一部分,我们处理了 <st c="21715">A6</st> 分类变量。 我们使用 <st c="21780">pandas</st> <st c="21789">unique()</st>检查其独特的类别。接下来,我们使用 <st c="21862">pandas</st> <st c="21871">value_counts()</st>计算每个类别的观测数,它返回了一个以类别为索引、观测数为值的 <st c="21904">pandas</st> 系列。 接下来,我们使用 <st c="22096">pandas</st> <st c="22104">sort_values()</st>将类别从观测数最多的到最少的进行排序。然后,我们使用 <st c="22191">pandas</st> <st c="22200">head()</st>将系列缩减到最流行的五个类别。我们使用列表推导式来捕获最频繁类别的名称。 之后,我们对每个类别进行循环,并使用 NumPy 的 <st c="22365">where()</st>创建二进制变量,如果观测显示该类别,则放置值为 <st c="22424">1</st> ,否则为 <st c="22469">0</st>

<st c="22481">我们在</st> <st c="22506">scikit-learn</st> <st c="22521">和</st> <st c="22527">scikit-learn</st> <st c="22539">中的</st> <st c="22544">feature-engine</st> <st c="22558">的</st> <st c="22566">通过单热编码创建二元变量</st> <st c="22616">食谱中讨论了如何使用</st> <st c="22625">这里,我将只强调编码最频繁类别所需的参数。</st>

<st c="22714">使用</st> <st c="22724">scikit-learn</st> <st c="22750">来编码频繁类别,我们将</st> <st c="22775">min_frequency</st> <st c="22788">参数设置为</st> <st c="22802">39</st> <st c="22804">。因此,在少于</st> <st c="22843">39</st> <st c="22845">个观测值中显示的类别将被组合成一个额外的二元变量</st> <st c="22907">,称为</st> <st c="22914">infrequent_sklearn</st> <st c="22932">。</st>

使用<st c="22933">特征工程</st> <st c="22969">feature-engine</st> <st c="22983">,我们将</st> <st c="22996">top_categories</st> <st c="23010">参数设置为</st> <st c="23024">5</st> <st c="23025">。因此,该转换器只为 5 个最频繁的类别创建了二元变量。</st> <st c="23116">较少出现的类别将在所有</st> <st c="23147">二元变量中显示为</st> <st c="23154">0</st> <st c="23155">。</st>

<st c="23184">还有更多...</st>

<st c="23200">这个食谱基于 2009 年知识发现与数据挖掘杯赛(</st> <st c="23253">知识发现与数据挖掘</st> <st c="23282">(</st> <st c="23283">KDD</st> <st c="23286">) <st c="23282">)的获奖解决方案,</st> <st c="23306">Winning the KDD Cup Orange Challenge with Ensemble Selection</st> <st c="23366">(http://proceedings.mlr.press/v7/niculescu09/niculescu09.pdf),其中作者将单热编码限制为每个变量的 10 个最频繁类别。</st>

<st c="23527">用计数或观测值的频率替换类别</st>

与“计数”或“观测值的计数或频率”编码相比,我们用该类别的计数或观测值中显示该类别的比例来替换类别。 也就是说,如果有 10 个观测值中的 100 个显示 蓝色 类别对于 颜色 变量,我们在进行计数编码时将用 蓝色 替换为 10 ,或者在进行频率编码时用 0.1 这些编码方法在类别频率与目标之间存在关系时很有用。 例如,在销售中,产品的频率可能表明 其受欢迎程度。`

<st c="24143">注意</st>

如果两个不同的类别在相同的观测数中存在,它们将被相同的值替换,这可能会导致 <st c="24284">信息丢失</st>

在这个菜谱中,我们将使用 <st c="24370">pandas</st> <st c="24376">和</st> feature-engine 执行计数和频率编码。

如何做到这一点...

我们将使用 <st c="24455">pandas</st> 对一个变量进行编码,然后我们将使用 <st c="24498">feature-engine</st>来自动化这个过程:

  1. 让我们从 <st c="24536">进口</st>开始:

     import pandas as pd
    from sklearn.model_selection import train_test_split
    from feature_engine.encoding import CountFrequencyEncoder
    
  2. 让我们加载信用批准数据集并将其分为训练集和 <st c="24748">测试集</st>

     data = pd.read_csv("credit_approval_uci.csv")
    X_train, X_test, y_train, y_test = train_test_split(
        data.drop(labels=["target"], axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  3. 让我们 <st c="24951">使用计数或观测频率”来捕获</st> A7<st c="25050">变量</st>的每个类别的观测数,并在一个字典中:

     counts = X_train["A7"].value_counts().to_dict()
    

注意

要找到频率而不是计数, <st c="25161">执行</st> <st c="25169">X_train["A7"].value_counts(normalize=True).to_dict()</st>

如果我们执行 <st c="25237">print(counts)</st>,我们将看到 <st c="25304">A7</st>的每个类别的观测计数:

<st c="25308">{'v': 277, 'h': 101, 'ff': 41, 'bb': 39, 'z': 7, 'dd': 5, 'j': 5, 'Missing': 4, 'n': 3, 'o': 1}</st>
  1. 让我们用 <st c="25436">A7</st> 中的计数来替换 <st c="25472">数据集</st>中的类别:

     X_train_enc = X_train.copy()
    X_test_enc = X_test.copy()
    X_train_enc["A7"] = X_train_enc["A7"].map(counts)
    X_test_enc["A7"] = X_test_enc["A7"].map(counts)
    

    继续执行 <st c="25680">X_train_enc.head()</st> 来验证类别已经被计数所替换:

    要将此过程应用于多个变量,我们可以 <st c="25822">使用</st> <st c="25826">feature-engine</st>

  2. 让我们设置编码器,使其使用观测计数来编码所有分类变量:

     count_enc = CountFrequencyEncoder(
        encoding_method="count", variables=None,
    )
    

注意

<st c="26026">CountFrequencyEncoder()</st> <st c="26050">将自动找到并编码训练集中的所有分类变量。</st> 要仅编码变量子集,请将变量名列表传递给 variables 参数。 <st c="26233">要使用频率进行编码,请使用</st> <st c="26271">encoding_method="frequency"</st> `

  1. 让我们将编码器拟合到训练集,以便它存储每个变量每个类别的观测数:

     count_enc.fit(X_train)
    
  2. 编码器自动找到了分类变量。 <st c="26497">让我们检查</st> `它们:

     count_enc.variables_
    

    <st c="26539">之前的命令返回了训练集中分类变量的名称:</st>

    <st c="26625">['A1', 'A4', 'A5', 'A6', 'A7', 'A9', 'A10', 'A12', 'A13']</st>
    
  3. <st c="26683">让我们按变量打印每个类别的观测数:</st>

    <st c="26748">count_enc.encoder_dict_</st>
    

    <st c="26772">之前的属性存储了将要用于替换类别的映射:</st>

图 2.7 – 包含每个变量的每个类别的观测数字典;这些值将用于编码分类变量

<st c="27366">图 2.7 – 包含每个变量的每个类别的观测数字典;这些值将用于编码分类变量</st>

  1. <st c="27522">最后,让我们用“每个类别的观测数或频率”替换训练集和测试集中的类别:</st>

     X_train_enc = count_enc.transform(X_train)
    X_test_enc = count_enc.transform(X_test)
    

<st c="27726">通过执行 X_train_enc.head()<st c="27779"> 来查看结果。编码器返回了 pandas 数据框,其中分类变量的字符串被替换为观测数的计数,使得变量准备好在机器学习模型中使用。</st>

<st c="27970">工作原理...</st>

<st c="27986">在这个配方中,我们使用 pandasfeature-engine 将类别替换为观测数。</st>

使用 <st c="28089">pandas</st><st c="28105">value_counts()</st>,我们确定了变量 <st c="28182">A7</st> 的每个类别的观测数,并且使用 <st c="28204">pandas</st><st c="28213">to_dict()</st>,我们将这些值捕获在一个“每个类别的观测数或频率”的字典中,其中每个键是一个唯一的类别,每个值是该类别的观测数。 使用 <st c="28412">pandas</st><st c="28421">map()</st> 和这个字典,我们在训练集和测试集中将类别替换为观测数。

<st c="28541">注意</st>

<st c="28546">编码的观测数应该从训练集中获得,以避免数据泄露。</st> 注意,测试集中的新类别将没有相应的映射,因此将被替换为 nan<st c="28763">。为了避免这种情况,请使用 feature-engine<st c="28799">。或者,您可以将 nan<st c="28839">替换为0<st c="28846">。</st>

使用 <st c="28879">feature-engine</st> 进行计数编码时,我们使用了 <st c="28903">CountFrequencyEncoder()</st> 并将 <st c="28935">encoding_method</st> 设置为 <st c="28954">'count'</st>。我们将 <st c="28975">variables</st> 参数设置为 <st c="29001">None</st>,以便编码器自动找到数据集中的所有类别变量。使用 <st c="29097">fit()</st>,转换器找到了类别变量,并将每个类别的观测计数存储在 <st c="29206">encoder_dict_</st> 属性中。使用 <st c="29236">transform()</st>,转换器将类别替换为计数,返回一个 <st c="29318">pandas</st> <st c="29324">DataFrame</st>

注意

如果测试集中有在训练集中不存在的类别,编码器默认会引发错误。您可以使其忽略它们,在这种情况下,它们将显示为 <st c="29528">nan</st>,或者将它们编码为 <st c="29551">0</st>

参见

您还可以使用 Python 库 Category Encoders 执行计数和频率编码:contrib.scikit-learn.org/category_encoders/count.html

要查看计数编码的一些有用应用,请参阅这篇文章:https://

替换类别为序数

序数编码包括将类别替换为从 1k 的数字(或 0k-1,具体取决于实现),其中 k 是变量的不同类别数量。这些数字是任意分配的。序数编码更适合非线性机器学习模型,这些模型可以通过任意分配的数字来寻找与目标相关的模式。

在这个菜谱中,我们将使用 <st c="30339">pandas</st><st c="30348">scikit-learn</st><st c="30360">and</st> <st c="30366">feature-engine</st> 进行序数编码。

如何做到这一点...

首先,让我们导入并准备 数据集:

  1. 导入 <st c="30459">pandas</st> 和数据 分割函数:

     import pandas as pd
    from sklearn.model_selection import train_test_split
    
  2. 让我们加载 Credit Approval 数据集并将其分为训练集和 测试集:

     data = pd.read_csv("credit_approval_uci.csv")
    X_train, X_test, y_train, y_test = train_test_split(
        data.drop(labels=["target"], axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  3. 为了 编码 <st c="30847">A7</st> 变量,让我们创建一个类别到整数的字典对:

     ordinal_mapping = {k: i for i, k in enumerate(
        X_train["A7"].unique(), 0)
    }
    

    如果我们执行 <st c="31004">print(ordinal_mapping)</st>,我们将看到将替换每个类别的数字:

    <st c="31083">{'v': 0, 'ff': 1, 'h': 2, 'dd': 3, 'z': 4, 'bb': 5, 'j': 6, 'Missing': 7, 'n': 8, 'o': 9}</st>
    
  4. 现在,让我们在 DataFrame 的副本中替换类别:

     X_train_enc = X_train.copy()
    X_test_enc = X_test.copy()
    X_train_enc["A7"] = X_train_enc["A7"].map(ordinal_mapping)
    X_test_enc["A7"] = X_test_enc["A7"].map(ordinal_mapping)
    

    执行 <st c="31430">print(X_train["A7"].head())</st> 以查看上一操作的 结果。

    接下来,我们将使用 scikit-learn`执行顺序编码:

  5. 让我们导入所需的 类:

     from sklearn.preprocessing import OrdinalEncoder
    from sklearn.compose import ColumnTransformer
    

注意

不要将 <st c="31712">OrdinalEncoder()</st> <st c="31734">LabelEncoder()</st> <st c="31754">scikit-learn</st>混淆。前者旨在编码预测特征,而后者旨在修改 目标变量。

  1. 让我们设置 编码器:

     enc = OrdinalEncoder()
    
  2. 让我们创建一个包含要编码的分类变量的列表:

     cat_vars = X_train.select_dtypes(include="O").columns.to_list()
    
  3. 让我们将编码限制在 分类变量 上:

     ct = ColumnTransformer(
        [("encoder", enc, cat_vars)],
        remainder="passthrough",
        force_int_remainder_cols=False,
    ).set_output(transform="pandas")
    

注意

请记住将 <st c="32286">remainder</st> 设置为 <st c="32299">"passthrough"</st> ,以便 <st c="32325">ColumnTransformer()</st> 返回未转换的变量。

  1. 让我们将编码器拟合到训练集,以便它创建并存储类别到数字的表示:

     ct.fit(X_train)
    

注意

通过执行 <st c="32534">ct.named_transformers_["encoder"].categories_</st>,您可以可视化每个变量的唯一类别。

  1. 现在,让我们对训练集和 测试集中的分类变量进行编码:

     X_train_enc = ct.transform(X_train)
    X_test_enc = ct.transform(X_test)
    

    执行 <st c="32798">X_train_enc.head()</st> 以检查生成的 DataFrame。

注意

<st c="32859">ColumnTransformer()</st> 将通过在变量名后附加<st c="32925">encoder</st> 来标记编码变量。 未修改的变量显示<st c="33001">remainder</st> 前缀。

现在,让我们使用<st c="33018">。</st> <st c="33032">ordinal encoding</st> <st c="33055">feature-engine</st>

  1. 让我们导入编码器:

     from feature_engine.encoding import OrdinalEncoder
    
  2. 让我们设置编码器,使其在步骤 7中指定的分类变量中用任意整数替换类别:

     enc = OrdinalEncoder(
        encoding_method="arbitrary",
        variables=cat_vars,
    )
    

注意

<st c="33356">feature-engine</st>’s <st c="33375">OrdinalEncoder()</st> 会自动找到并编码所有分类变量,如果变量 参数是<st c="33480">None</st>。或者,它将编码列表中指定的变量。 此外,它可以根据目标均值值分配整数(参见基于目标值进行序数编码 *的配方)。

  1. 让我们将编码器拟合到训练集中,以便它学习并存储类别到整数的映射:

     enc.fit(X_train)
    

<st c="33818">注意</st>

分类到整数的映射存储在<st c="33875">encoder_dict_</st> 属性中,可以通过执行<st c="33922">enc.encoder_dict_</st>来访问。

  1. 最后,让我们将训练集和测试集中的分类变量进行编码:

     X_train_enc = enc.transform(X_train)
    X_test_enc = enc.transform(X_test)
    

<st c="34098">feature-engine</st> 返回<st c="34122">pandas</st> DataFrames,其中原始变量的值被数字替换,使 DataFrame 准备好在机器学习模型中使用。

它是如何工作的...

在这个方法中,我们将类别替换为任意分配的整数

我们使用了 <st c="34372">pandas</st> <st c="34381">unique()</st> 来找到变量A7 的唯一类别。 接下来,我们创建了一个类别到整数的字典,并将其传递给pandas<st c="34516">’</st> map()` ,以将A7 中的字符串替换为整数。

<st c="34572">接下来,我们使用</st> <st c="34617">scikit-learn</st> <st c="34629">的</st> <st c="34633">OrdinalEncoder()</st> <st c="34650">进行了序数编码,并使用</st> <st c="34659">ColumnTransformer()</st> <st c="34678">将编码限制为分类变量。</st> <st c="34730">使用</st> <st c="34735">fit()</st> <st c="34740">,转换器根据训练集中的类别创建了类别到整数的映射。</st> <st c="34841">使用</st> <st c="34846">transform()</st> <st c="34857">,类别被替换为整数。</st> <st c="34903">通过将</st> <st c="34918">remainder</st> <st c="34927">参数设置为</st> <st c="34941">passthrough</st> <st c="34952">,我们使</st> <st c="34962">ColumnTransformer()</st> <st c="34981">将未编码的变量连接到编码特征之后。</st>

使用<st c="35065">feature-engine</st>进行序数编码时,我们使用了<st c="35123">OrdinalEncoder()</st>,表示整数应通过<st c="35209">encoding_method</st>任意分配,并在<st c="35280">variables</st> <st c="35289">参数</st>中传递一个包含要编码变量的列表。使用 <st c="35305">fit()</st>,编码器将整数分配给每个变量的类别,这些类别存储在<st c="35398">encoder_dict_</st> <st c="35411">属性</st>中。这些 <st c="35428">映射</st>随后被<st c="35460">transform()</st> <st c="35471">方法</st>使用,以替换训练集和测试集中的类别,并返回 DataFrames。

<st c="35554">注意</st>

<st c="35559">当测试集中的类别不在训练集中时,它将没有映射到数字。</st> <st c="35667">OrdinalEncoder()</st> <st c="35683">来自</st> <st c="35689">scikit-learn</st> <st c="35701">和</st> <st c="35706">feature-engine</st> <st c="35720">默认情况下会引发错误。</st> <st c="35753">然而,它们可以选择用用户定义的值或- 1 分别替换未看到的类别。`

<st c="35858">scikit-learn</st> <st c="35871">的</st> <st c="35875">OrdinalEncoder()</st> <st c="35891">可以限制编码到具有最小频率的类别。</st> <st c="35964">feature-engine</st> <st c="35978">的</st> <st c="35982">OrdinalEncoder()</st> <st c="35998">可以根据目标平均值`分配数字,正如我们将在下面的食谱中看到的那样。

<st c="36093">还有更多...</st>

您还可以使用来自 Category Encoders 的 <st c="36155">OrdinalEncoder()</st> 执行序数编码。查看这里

基于目标值执行序数编码

在上一个配方中,我们将类别替换为任意分配的整数。我们也可以根据目标值给类别分配整数。为了做到这一点,首先,我们计算每个类别的目标平均值。接下来,我们按从最低到最高的目标平均值对类别进行排序。最后,我们将数字分配给有序的类别,从0 到第一个类别,到k-1 到最后一个类别,其中k 是不同类别的数量。

这种编码方法在分类变量和响应之间创建了一个单调关系,因此使变量更适合用于线性模型。

在这个配方中,我们将使用 <st c="37108">pandas</st> <st c="37119">feature-engine</st>来编码类别,同时遵循目标值。

如何做到这一点...

首先,让我们导入必要的 Python 库并准备好数据集:

  1. 导入所需的 Python 库、函数和类:

     import pandas as pd
    import matplotlib.pyplot as plt
    from sklearn.model_selection import train_test_split
    
  2. 让我们加载 Credit Approval 数据集并将其分为训练集和测试集:

     data = pd.read_csv("credit_approval_uci.csv")
    X_train, X_test, y_train, y_test = train_test_split(
        data.drop(labels=["target"], axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  3. 让我们在 <st c="37715">A7</st>中确定每个类别的平均目标值,然后按从最低到最高的目标值对类别进行排序:

     y_train.groupby(X_train["A7"]).mean().sort_values()
    

    以下是在先前的命令的输出:

    <st c="37914">A7</st>
    <st c="37917">o          0.000000</st>
    <st c="37928">ff         0.146341</st>
    <st c="37940">j          0.200000</st>
    <st c="37951">dd         0.400000</st>
    <st c="37963">v          0.418773</st>
    <st c="37974">bb         0.512821</st>
    <st c="37986">h          0.603960</st>
    <st c="37997">n          0.666667</st>
    <st c="38008">z          0.714286</st>
    <st c="38019">Missing    1.000000</st>
    <st c="38036">Name: target, dtype: float64</st>
    
  4. 现在,让我们重复在步骤 3中的计算,但这次,让我们保留有序的类别名称:

     ordered_labels = y_train.groupby(
        X_train["A7"]).mean().sort_values().index
    

    要显示先前的命令的输出,我们可以执行 <st c="38306">print(ordered_labels)</st> <st c="38330">Index(['o', 'ff', 'j', 'dd', 'v', 'bb', 'h', 'n', 'z', 'Missing'],</st> <st c="38397">dtype='object', name='A7')</st>

  5. 让我们创建一个类别到整数的字典,使用我们在步骤 4中创建的有序列表:

     ordinal_mapping = {
        k: i for i, k in enumerate(ordered_labels, 0)
    }
    

    我们可以通过执行 print(ordinal_mapping)`来可视化前面代码的结果:

    <st c="38847">A7</st> in a copy of the datasets:
    
    

    X_train_enc = X_train.copy()

    X_test_enc = X_test.copy()

    X_train_enc["A7"] = X_train_enc["A7"].map(

    ordinal_mapping)
    

    X_test_enc["A7"] = X_test_enc["A7"].map(

    ordinal_mapping)
    
    
    

注意

如果测试集包含训练集中不存在的类别,前面的代码将 引入 <st c="39164">np.nan</st>

为了可视化这种编码的效果,让我们绘制编码前后 <st c="39267">A7</st> 变量类别与目标之间的关系。

  1. 让我们绘制 <st c="39382">A7</st> 变量每个类别的均值目标响应:

     y_train.groupby(X_train["A7"]).mean().plot()
    plt.title("Relationship between A7 and the target")
    plt.ylabel("Mean of target")
    plt.show()
    

    我们可以从以下图中看到 <st c="39596">A7</st> 类别与目标之间的非单调关系:

图 2.8 – 编码前 A7 类别每类的均值目标值。

图 2.8 – 编码前 A7 类别每类的均值目标值。

  1. 让我们绘制编码变量中每个类别的均值目标值:

     y_train.groupby(X_train_enc["A7"]).mean().plot()
    plt.title("Relationship between A7 and the target")
    plt.ylabel("Mean of target")
    plt.show()
    

    编码变量与目标之间存在单调关系——目标均值目标值越高,分配给的类别 的数字就越高:

图 2.9 – 编码后 A7 类别每类的均值目标值。

图 2.9 – 编码后 A7 类别每类的均值目标值。

现在,让我们 使用 <st c="40393">feature-engine</st>执行有序顺序编码。

  1. 让我们导入 编码器:

     from feature_engine.encoding import OrdinalEncoder
    
  2. 接下来,让我们设置编码器,使其根据目标均值值将整数分配给数据集中所有分类变量:

     ordinal_enc = OrdinalEncoder(
        encoding_method="ordered",
        variables=None)
    

注意

<st c="40699">OrdinalEncoder()</st> 将自动查找并编码所有分类变量。 要限制编码到变量的子集,将它们的名称作为列表传递给 <st c="40864">variables</st> 参数。 要编码数值变量, 设置 <st c="40919">ignore_format=True</st>

  1. 让我们将编码器拟合到训练集,以便它找到类别变量,然后存储类别和整数映射:

     ordinal_enc.fit(X_train, y_train)
    
  2. 最后,让我们在训练集和测试集中将类别替换为数字:

     X_train_enc = ordinal_enc.transform(X_train)
    X_test_enc = ordinal_enc.transform(X_test)
    

注意

你将在encoder_dict_属性中找到替换每个类别的数字。

检查通过执行X_train_enc.head(). 变换的输出。

它是如何工作的...

在这个菜谱中,我们根据目标均值将类别替换为整数。

在这个菜谱的第一部分,我们处理了A7类别变量。 使用pandasgroupby(),我们根据A7的类别对数据进行分组,并使用pandasmean(),我们确定了每个类别的目标均值。 接下来,我们使用pandassort_values()对类别进行排序,从目标均值响应最低的类别到最高的类别。 这个操作的输出是一个pandas序列,类别作为索引,目标均值作为值。 使用pandasindex,我们将排序后的类别存储在一个数组中;然后,使用 Python 字典推导式,我们创建了一个类别到整数的字典。 最后,我们使用这个字典通过pandasmap()将类别替换为整数。

注意

为了避免数据泄露,我们从训练集确定类别到整数的映射。

[使用feature-engine进行编码时,我们使用了OrdinalEncoder(),将encoding_method设置为ordered。我们将参数变量设置为None,以便编码器自动检测数据集中的所有分类变量。 [使用fit(),编码器找到了分类变量,并根据目标均值值将数字分配给它们的类别。 [分类变量的名称和类别到数字对的字典分别存储在variables_encoder_dict_属性中。 [最后,使用transform(),我们在训练集和测试集中将类别替换为数字,返回pandas DataFrame。]

另请参阅

要使用 Category Encoders 实现此菜谱,请访问本书的 GitHub 仓库: https://github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch02-categorical-encoding/Recipe-05-Ordered-ordinal-encoding.ipynb.

实现目标均值编码

**均值编码** 或 **目标编码** 将每个类别映射到目标属性的概率估计值。如果目标是二进制,则数值映射是目标在给定类别值条件下的后验概率。如果目标是连续的,则数值表示是给定类别值的目标的期望值。]

在其最简单形式中,每个类别的数值表示是通过特定类别组的目标变量的平均值给出的。例如,如果我们有一个 <st c="43913">城市</st> 变量,其类别为 <st c="43951">伦敦</st> <st c="43959">曼彻斯特</st> <st c="43975">布里斯托尔</st>,并且我们想要预测违约率(目标取值为 <st c="44052">0</st> <st c="44058">1</st>);如果伦敦的违约率为 30%,我们将 <st c="44086">伦敦</st> 替换为 <st c="44112">0.3</st>;如果曼彻斯特的违约率为 20%,我们将 <st c="44154">曼彻斯特</st> 替换为 <st c="44184">0.2</st>;依此类推。 如果目标是连续的——比如说我们想要预测收入——那么我们将 <st c="44301">伦敦</st> <st c="44309">曼彻斯特</st> <st c="44325">布里斯托尔</st> 替换为每个城市所赚取的平均收入。

在数学术语中,如果目标是二进制,则替换值,S,是这样确定的:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:msubmml:mrowmml:miS</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub>mml:mo=</mml:mo>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi>mml:mo(</mml:mo>mml:miy</mml:mi>mml:mo=</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:mrow></mml:msub>mml:mo/</mml:mo>mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math>

在这里,分子是具有类别值 1 的观测值的数量,对于类别 i ,分母是具有类别值 的观测值的数量 i

如果目标是连续的, S,这是通过以下公式确定的:

Si=∑yini

在这里,分子是类别 i 中观察到的目标值的总和 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math> 是类别 i中的观察总数。

如果每个类别值有足够多的观察值,这些公式提供了对目标估计的良好近似——换句话说,如果 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math> 很大。 然而,在许多数据集中,将存在一些观察值中出现的类别。 在这些情况下,从前述公式推导出的目标估计可能 不可靠。

为了减轻对罕见类别返回的估计值不准确的问题,目标估计值可以确定为两个概率的混合:前述公式返回的概率和基于整个训练集的目标先验概率。 这两个概率通过一个加权因子进行混合,该加权因子是类别组大小的函数:

Si=λni(Y=1)ni+(1−λi)nλN

在这个公式中, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrow<mml:mi mathvariant="normal">λ</mml:mi></mml:mrow></mml:msub></mml:math> 是目标取值为1的总案例数,N是训练集的大小,而𝜆是加权因子。

当类别组较大时,𝜆 趋向于1,因此方程的第一个项被赋予更多的权重。当类别组大小较小时,则𝜆 趋向于0,因此估计主要由方程的第二个项驱动——即目标的前验概率。换句话说,如果组大小较小,知道类别值并不能告诉我们关于目标值的信息。

权重因子,𝜆,在不同的开源实现中确定方式不同。在类别编码器中,𝜆 是组大小,k,以及平滑参数,f,的函数,它控制着前一个方程的第一个项和第二个项之间的转换速率:

λ=11+e−(n−k)/f

在这里,k是我们可以完全信任方程第一个项的最小大小的一半。完全信任 方程的第一个项 的参数由用户任意选择或通过优化选择。f 参数由用户任意选择或通过优化选择。

scikit-learnfeature-engine中,𝜆是一个关于整个数据集和类别内目标方差的函数,并且如下确定:

λ=ni×ts+ni×t

在这里,t 是整个数据集中的目标方差,s 是类别内的目标方差。两种实现是等效的,但了解这些方程很重要,因为它们将帮助你在转换器中设置参数。

注意

均值编码旨在在不扩展特征空间的情况下对高度基数分类变量进行编码。 更多详情,请查看以下文章:Micci-Barreca D. A. 用于分类和预测问题中高基数分类属性的前处理方案. ACM SIGKDD Explorations 通讯,2001。

在这个菜谱中,我们将使用 <st c="47505">scikit-learn</st> <st c="47522">feature-engine</st>来执行均值编码。

如何操作...

让我们从 这个菜谱 开始:

  1. 导入 <st c="47591">pandas</st> 以及数据 `分割函数

     import pandas as pd
    from sklearn.model_selection import train_test_split
    
  2. 让我们加载 Credit Approval 数据集并将其分为训练集和 测试集:

     data = pd.read_csv("credit_approval_uci.csv")
    X_train, X_test, y_train, y_test = train_test_split(
        data.drop(labels=["target"], axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  3. 让我们从<st c="48000">scikit-learn</st>导入转换器

     from sklearn.preprocessing import TargetEncoder
    from sklearn.compose import ColumnTransformer
    
  4. 让我们 创建一个包含分类变量名称的列表:

     cat_vars = X_train.select_dtypes(
        include="O").columns.to_list()
    
  5. 让我们设置编码器,使其使用目标方差来确定权重因子,正如菜谱开头所述:

     enc = TargetEncoder(smooth="auto", random_state=9)
    
  6. 让我们将插补限制在 分类变量:

     ct = ColumnTransformer(
        [("encoder", enc, cat_vars)],
        remainder="passthrough",
    ).set_output(transform="pandas")
    
  7. 让我们拟合编码器并转换 数据集:

     X_train_enc = ct.fit_transform(X_train, y_train)
    X_test_enc = ct.transform(X_test)
    

    通过执行 <st c="48755">X_train_enc.head()</st>` 来查看结果。

注意

<st c="48784">fit_transform()</st> 方法与<st c="48810">scikit-learn</st><st c="48826">TargetEncoder()</st> <st c="48872">fit().transform()</st>应用不等价。使用<st c="48896">fit_transform()</st>,生成的数据集基于交叉验证方案的训练折的局部拟合进行编码。 此功能是故意设计的,以防止机器学习模型过度拟合 训练集。

现在,让我们使用<st c="49176">feature-engine</st>执行目标编码

  1. 让我们导入 编码器:

     from feature_engine.encoding import MeanEncoder
    
  2. 让我们设置目标均值编码器以编码所有分类变量,同时 应用平滑处理:

     mean_enc = MeanEncoder(smoothing="auto",
        variables=None)
    

注意

<st c="49426">MeanEncoder()</st> 默认情况下不应用平滑。 确保您将其设置为 <st c="49502">auto</st> 或整数以控制先验和后验目标估计之间的混合 b

  1. 让我们将变压器拟合到训练集,以便它学习并存储每个类别的平均目标值 每个变量:

     mean_enc.fit(X_train, y_train)
    
  2. 最后,让我们对训练集和 测试集进行编码:

     X_train_enc = mean_enc.transform(X_train)
    X_test_enc = mean_enc.transform(X_test)
    

注意

类别到数字的配对存储在 <st c="49957">encoder_dict_</st> 属性中的字典字典中。 要显示存储的参数,执行 <st c="50017">mean_enc.encoder_dict_.</st>

它是如何工作的…

在这个配方中,我们 使用 <st c="50139">scikit-learn</st> <st c="50156">feature-engine</st>将类别替换为平均目标值。

要使用 <st c="50187">scikit-learn</st>进行编码,我们使用了 <st c="50209">TargetEncoder()</st>,将 <st c="50238">smooth</st> 参数保留为其默认值 <st c="50279">auto</st>。就这样,变压器使用了目标方差来确定概率混合的权重因子。 使用 <st c="50408">fit()</st>,变压器学习了它应该使用的值来替换类别,并且使用 <st c="50499">transform()</st>,它替换了 类别。

请注意,对于 <st c="50554">TargetEncoder()</st>,随后的 <st c="50575">fit()</st> 方法和 <st c="50600">transform()</st> 不返回与 <st c="50650">fit_transform()</st>方法相同的数据集。 后者的想法是在管道中使用 <st c="50776">fit_transform()</st> ,这样机器学习模型就不会过拟合。 然而,这里变得有些混乱,存储在 <st c="50932">encodings_</st> 属性中的映射在 <st c="50972">fit()</st> <st c="50982">fit_transform()</st>之后是相同的,这是故意为之,以便当我们将 <st c="51052">transform()</st> 应用于新数据集时,无论我们是否将 <st c="51139">fit()</st> <st c="51148">fit_transform()</st>应用于训练集,我们都能获得相同的结果。

注意

未看到的类别通过scikit-learn TargetEncoder()进行编码,以目标均值编码。 <st c="51277">feature-engine</st> <st c="51295">MeanEncoder()</st>可以返回错误,用nan替换未看到的类别,或者用目标均值替换。

要使用feature-engine执行目标编码,我们使用了 <st c="51462">MeanEncoder(),</st> <st c="51489">smoothing</st> 参数设置为 <st c="51512">auto</st>。通过 <st c="51523">fit()</st>,转换器找到了并存储了分类变量以及编码每个类别的值。 通过 <st c="51634">transform()</st>,它将类别替换为数字,返回 <st c="51699">pandas</st> 数据框。

还有更多...

如果您想使用pandas 或类别编码器实现目标编码,请查看随附 GitHub 存储库中的笔记本: https://github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch02-categorical-encoding/Recipe-06-Target-mean-encoding.ipynb

当类别分组较小时,有一种替代方法可以返回更准确的目标估计值 每个类别的替换值如下确定:

Si=niY=1+pYxmni+m

这里, ni(Y=1)是类别 i 的目标均值,并且 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math> 是具有类别 i 的观测值的数量。目标先验由 pY 给出,并且 m 是权重因子。 通过这种调整,我们唯一需要设置的参数是权重, m 。如果 m 很大,那么就会给予目标先验概率更多的重视。 这种调整会影响所有类别的目标估计,但对于观测值较少的类别影响更大,因为在这种情况下, m 可能比 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:min</mml:mi></mml:mrow>mml:mrowmml:mii</mml:mi></mml:mrow></mml:msub></mml:math> 在公式分母中的 数量大得多。

注意

这种方法是 Category Encoders 的<st c="52786">TargetEncoder()</st>的一个很好的替代方案,因为,在 Category Encoders 实现的目标编码中,我们需要优化两个参数而不是一个(正如我们在<st c="52934">feature-engine</st> <st c="52948">和</st> <st c="52953">scikit-learn</st> <st c="52965">中做的那样),以控制 <st c="52979">平滑度。</st>

要使用<st c="53046">MEstimateEncoder()</st>实现此编码方法,请访问本书的 GitHub 仓库: https://github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch02-categorical-encoding/Recipe-06-Target-mean-encoding.ipynb

使用证据权重进行编码

证据权重 (WoE) 主要是为了信用和金融行业开发的,以促进变量筛选和探索性 分析,并构建更具有预测性的线性模型来评估 贷款违约的风险。

WoE 是从基本的 优势比 计算得出的:

WoE=log(proportionpositivecasesproportionnegativecases)

在这里,正负分别指目标值的 1 0 每个类别的正例比例是每个类别组正例总和除以训练集中正例总数。 每个类别的负例比例是每个类别组负例总和除以训练集中负观察值的总数。

WoE 具有以下特性:

  • WoE = 0 if p(positive) / p(negative) = 1; 即,如果结果 是随机的

  • WoE > 0 if p(positive) > p(negative)

  • 如果 0 ,则 WoE < p(negative) > p(positive)

这使我们能够直接可视化变量中类别的预测能力:WoE 越高,事件发生的可能性就越大。 如果 WoE 为正,则事件很可能发生。

逻辑回归模型基于 Y预测变量,对二元响应进行建模,假设 X X 的对数几率之间存在线性关系

logpY=1pY=0=b0+b1X1+b2X2+…+bnXn

在这里, log (p(Y=1)/p(Y=0)) 是对数几率。 正如你所见,WOE 将类别编码在对数几率——即逻辑回归的输出——的同一尺度上。

因此,通过使用 WOE,预测变量被准备并编码在同一尺度上,逻辑回归模型中的参数——即系数——可以 直接比较。

在这个菜谱中,我们将使用 <st c="55079">pandas</st> <st c="55090">feature-engine</st>执行 WOE 编码。

如何操作...

让我们首先进行一些导入并准备 数据:

  1. 导入所需的库 和函数:

     import numpy as np
    import pandas as pd
    from sklearn.model_selection import train_test_split
    
  2. 让我们加载 信用批准数据集并将其分为训练集和 测试集:

     data = pd.read_csv("credit_approval_uci.csv")
    X_train, X_test, y_train, y_test = train_test_split(
        data.drop(labels=["target"], axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  3. 让我们求出目标值的逆,以便计算 负案例:

     neg_y_train = pd.Series(
        np.where(y_train == 1, 0, 1),
        index=y_train.index
    )
    
  4. 让我们确定目标变量取值为 <st c="55833">1</st> <st c="55837">0</st>的观测数:

     total_pos = y_train.sum()
    total_neg = neg_y_train.sum()
    
  5. 现在,让我们计算 WOE 公式的分子和分母,这是我们之前在这个菜谱中讨论过的:

     pos = y_train.groupby(
        X_train["A1"]).sum() / total_pos
    neg = neg_y_train.groupby(
        X_train["A1"]).sum() / total_neg
    
  6. 现在,让我们 计算每个类别的 WOE:

     woe = np.log(pos/neg)
    

    我们可以通过执行 print(woe) 来显示类别与 WOE 对的序列:

    <st c="56276">A1</st>
    <st c="56278">Missing    0.203599</st>
    <st c="56295">a          0.092373</st>
    <st c="56306">b         -0.042410</st>
    <st c="56375">A1</st> with the WoE in a copy of the datasets:
    
    

    X_train_enc = X_train.copy()

    X_test_enc = X_test.copy()

    X_train_enc["A1"] = X_train_enc["A1"].map(woe)

    X_test_enc["A1"] = X_test_enc["A1"].map(woe)

    
    <st c="56565">You can inspect the e</st><st c="56587">ncoded variable by</st> <st c="56607">executing</st> `<st c="56617">X_train_enc["A1"].head()</st>`<st c="56641">.</st><st c="56642">Now, let’s perform WoE encoding</st> <st c="56675">using</st> `<st c="56681">feature-engine</st>`<st c="56695">.</st>
    
  7. 让我们导入 编码器:

    <st c="56722">from feature_engine.encoding import WoEEncoder</st>
    
  8. 接下来,让我们设置编码器以编码三个 分类变量:

     woe_enc = WoEEncoder(variables = ["A1", "A9", "A12"])
    

注意

对于罕见类别,可能会发生p(0)=0 p(1)=0,此时除法或对数运算未定义。 为了避免这种情况,可以将不常见的类别按以下 分组罕见或不常见 类别 配方进行分组。

  1. 让我们将 转换器拟合到训练集,以便它学习并存储不同类别的 WoE:

     woe_enc.fit(X_train, y_train)
    

注意

我们可以通过执行 woe_enc.encoder_dict_来显示具有类别到 WoE 对的字典。

  1. 最后,让我们对训练集和 测试集中的三个分类变量进行编码:

     X_train_enc = woe_enc.transform(X_train)
    X_test_enc = woe_enc.transform(X_test)
    

<st c="57527">feature-engine</st> 返回 <st c="57551">pandas</st> DataFrames,其中包含可用于机器 学习模型的编码分类变量。

它是如何工作的...

在这个配方中,我们使用 <st c="57744">pandas</st> <st c="57755">feature-engine</st>对分类变量进行了 WoE 编码。

我们将 <st c="57794">pandas</st> <st c="57803">sum()</st> <st c="57813">groupby()</st> 以及 <st c="57827">numpy</st> <st c="57836">log()</st> 结合起来,正如我们在本配方开头所描述的那样,来确定 WoE。

接下来,我们使用 <st c="57950">feature-engine</st>自动化了该过程。我们使用了 <st c="57978">WoEEncoder()</st>,它通过 <st c="58036">fit()</st> 方法学习每个类别的 WoE,然后使用 <st c="58064">tra</st><st c="58067">nsform()</st> 将类别替换为相应的数字。

另请参阅

有关使用类别编码器实现 WoE 的示例,请访问本书的 GitHub 存储库: https://github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch02-categorical-encoding/Recipe-07-Weight-of-evidence.ipynb

分组罕见或不常见类别

稀疏类别只出现在观测值的一小部分中。 没有经验法则来确定多小的一小部分,但通常,任何低于 5%的值都可以 被认为是稀疏的。

不频繁的标签通常 只出现在训练集或测试集中,这使得算法容易过拟合或无法对观测值进行评分。 此外,当将类别编码为数字时,我们只为训练集中观察到的类别创建映射,因此我们不知道如何编码新的标签。 为了避免这些复杂性,我们可以将不频繁的类别组合成一个称为 <st c="59068">Rare</st> <st c="59076">Other</st> 的单一类别。

在这个菜谱中, 我们将使用 <st c="59141">pandas</st> <st c="59152">feature-engine</st> 将不频繁的类别进行分组。

如何操作...

首先,让我们导入必要的 Python 库并准备好数据集:

  1. 导入必要的 Python 库、函数和类:

     import numpy as np
    import pandas as pd
    from sklearn.model_selection import train_test_split
    from feature_engine.encoding import RareLabelEncoder
    
  2. 让我们加载 Credit Approval 数据集并将其分为训练集和 测试集:

     data = pd.read_csv("credit_approval_uci.csv")
    X_train, X_test, y_train, y_test = train_test_split(
        data.drop(labels=["target"], axis=1),
        data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  3. 让我们 捕获 A7 中每个类别的观测值比例 在一个变量中:

     freqs = X_train["A7"].value_counts(normalize=True)
    

    我们可以看到以下输出中 A7 每个类别的观测值比例,以小数表示,在执行 <st c="59989">print(freqs)</st> 之后:

    <st c="60003">v 0.573499</st>
    <st c="60013">h 0.209110</st>
    <st c="60024">ff 0.084886</st>
    <st c="60036">bb 0.080745</st>
    <st c="60048">z 0.014493</st>
    <st c="60059">dd 0.010352</st>
    <st c="60071">j 0.010352</st>
    <st c="60082">Missing 0.008282</st>
    <st c="60099">n 0.006211</st>
    <st c="60110">o 0.002070</st>
    <st c="60234">z</st>, <st c="60237">dd</st>, <st c="60241">j</st>, <st c="60244">Missing</st>, <st c="60253">n</st>, and <st c="60260">o</st> are rare categories.
    
  4. 让我们创建一个包含在超过 5%的 观测中存在的类别名称的列表:

     frequent_cat = [
        x for x in freqs.loc[freqs > 0.05].index.values]
    

    如果我们执行 <st c="60467">print(frequent_cat)</st>,我们将看到 A7 频繁类别:

    <st c="60646">Rare</st> string in a copy of the datasets:
    
    

    X_train_enc = X_train.copy()

    X_test_enc = X_test.copy()

    X_train_enc["A7"] = np.where(X_train["A7"].isin(

    frequent_cat), X_train["A7"], "Rare")
    

    X_test_enc["A7"] = np.where(X_test["A7"].isin(

    frequent_cat), X_test["A7"], "Rare")
    
    
    
  5. 让我们确定编码变量的观测值百分比:

     X_train["A7"].value_counts(normalize=True)
    

    我们可以看到不频繁的标签现在已经被重新组合到 <st c="61099">Rare</st> 类别中:

    <st c="61113">v       0.573499</st>
    <st c="61124">h       0.209110</st>
    <st c="61135">ff      0.084886</st>
    <st c="61147">bb      0.080745</st>
    <st c="61159">Rare    0.051760</st>
    <st c="61236">feature-engine</st>.
    
  6. 让我们 创建一个稀疏标签编码器,将出现在观测值不到 5%的类别组合在一起,前提是分类变量有超过四个 不同值:

     rare_encoder = RareLabelEncoder(tol=0.05,
        n_categories=4)
    
  7. <st c="61486">让我们拟合编码器,以便它找到分类变量,然后学习它们的最高频率类别:</st>

     rare_encoder.fit(X_train)
    

<st c="61625">注意</st>

<st c="61630">在拟合过程中,转换器将发出警告,表明许多分类变量少于四个类别,因此它们的值不会被分组。</st> <st c="61796">转换器只是通知你这种情况正在发生。</st>

我们可以通过执行<st c="61920">rare_encoder.encoder_dict_</st>来显示每个变量的频繁类别,以及通过执行<st c="61997">executing</st> <st c="62007">rare_encoder.variables_</st>`将被编码的变量。

  1. 最后,让我们将训练集和测试集中的稀有标签分组:

     X_train_enc = rare_encoder.transform(X_train)
    X_test_enc = rare_encoder.transform(X_test)
    

现在我们已经将稀有标签分组,我们准备对分类变量进行编码,就像我们在本章前面的食谱中所做的那样。

<st c="62326">它的工作原理...</st>

<st c="62342">在这个食谱中,我们使用pandas <st c="62404">和feature-engine<st c="62423">将不频繁的类别分组。</st>

我们确定了<st c="62488">A7</st> 变量每个类别的观测值比例,使用了<st c="62506">pandas</st> <st c="62515">value_counts()</st> 函数,通过将<st c="62545">normalize</st> 参数设置为<st c="62568">True</st>。使用列表推导,我们捕获了在超过 5%的观测值中存在的变量名称。 最后,使用 NumPy 的<st c="62708">where()</st>函数,我们搜索了<st c="62741">A7</st>的每一行,如果观测值是列表中的频繁类别之一,我们使用<st c="62839">pandas</st> <st c="62848">isin()</st>进行检查,则保留其值;否则,将其替换为<st c="62908">Rare</st>

我们使用 <st c="62988">feature-engine</st> <st c="63006">RareLabelEncoder()</st>自动化了前面提到的多个分类变量的步骤。通过将 <st c="63037">tol</st> 设置为 <st c="63044">0.05</st>,我们保留了在超过 5%的观测中存在的类别。 通过将 <st c="63129">n_categories</st> 设置为 <st c="63145">4</st>,我们只对具有超过四个唯一值的变量中的类别进行分组。 使用 <st c="63228">fit()</st>,转换器识别了分类变量,然后学习并存储了它们的频繁类别。 使用 <st c="63348">transform</st><st c="63357">()</st>,转换器将不频繁的类别替换为 <st c="63418">Rare</st> 字符串。

执行二进制编码

<st c="63808">1</st> 可以用序列 <st c="63850">1-0</st>表示,整数 <st c="63863">2</st> 可以用 <st c="63870">0-1</st>表示,整数 <st c="63883">3</st> 可以用 <st c="63890">1-1</st>表示,整数 <st c="63907">0</st> 可以用 <st c="63914">0-0</st>表示。二进制字符串的两个位置的数字成为 列,这些列是原始变量的编码表示:

Figure 2.10 – 表格显示对颜色变量进行二进制编码所需的步骤

Figure 2.10 – 表格显示对颜色变量进行二进制编码所需的步骤

二进制编码 比独热编码使用更少的维度。 在我们的例子中, <st c="64316">颜色</st> 变量将通过独热编码被编码为 k-1 个类别——即三个变量——但使用二进制编码,我们可以只用两个特征来表示该变量。 更一般地,我们确定编码一个变量所需的二进制特征数量为 log2(不同类别的数量);在我们的例子中, log2(4) = 2 二进制特征。

二进制编码是 one-hot 编码的替代方法,其中我们不会丢失关于变量的信息,但在编码后我们获得更少的特征。 这在处理高度基数变量时特别有用。 例如,如果一个变量包含 128 个唯一类别,使用 one-hot 编码,我们需要 127 个特征来编码该变量,而使用二进制编码,我们只需要 7 (log2(128)=7)。因此,这种编码防止了特征空间的爆炸。 此外,二进制编码的特征也适合线性模型。 然而,从衍生出的二进制特征中缺乏人类可解释性,因此如果我们需要解释模型所做的决策,这种编码方法可能不是一个 合适的选择。

在这个例子中,我们将学习如何使用 类别编码器 执行二进制编码。

如何做到这一点...

首先,让我们导入必要的 Python 库并准备好 数据集:

  1. 导入所需的 Python 库、函数、 和类:

     import pandas as pd
    from sklearn.model_selection import train_test_split
    from category_encoders.binary import BinaryEncoder
    
  2. 让我们加载 Credit Approval 数据集并将其分为训练集和 测试集:

     data = pd.read_csv("credit_approval_uci.csv")
    X_train, X_test, y_train, y_test = train_test_split(
        data.drop(labels=["target"], axis=1), data["target"],
        test_size=0.3,
        random_state=0,
    )
    
  3. 让我们检查 A7 中的唯一类别:

     X_train["A7"].unique()
    

    在以下输出中,我们可以看到 <st c="66162">A7</st> 有 10 个不同的类别:

    <st c="66315">A7</st>:
    
    

    encoder = BinaryEncoder(cols=["A7"],

    drop_invariant=True)
    
    
    

注意

<st c="66382">BinaryEncoder()</st>,以及其他来自类别编码器包的编码器,允许我们选择要编码的变量。 我们只需将列名列表传递给 <st c="66555">cols</st> 参数。

  1. 让我们将转换器拟合到训练集中,以便它计算所需的二进制变量数量并创建变量到二进制 代码表示:

     encoder.fit(X_train)
    
  2. 最后,让我们将 <st c="66770">A7</st> 编码到训练集和测试集中:

     X_train_enc = encoder.transform(X_train)
    X_test_enc = encoder.transform(X_test)
    

    我们可以 通过执行 <st c="66951">print(X_train_enc.head())</st>来显示转换后的训练集的前几行,它返回以下输出:

图 2.11 – 二进制编码后的变量 DataFrame

图 2.11 – 二进制编码后的变量 DataFrame

二进制编码为 <st c="67449">A7</st>返回了四个二进制变量,分别是 <st c="67463">A7_0</st>, <st c="67469">A7_1</st>, <st c="67475">A7_2</st>,和 <st c="67485">A</st><st c="67486">7_3</st>,而不是由 one-hot 编码返回的九个变量。

它是如何工作的...

在这个食谱中,我们使用了 Category Encoders 包进行二进制编码。我们使用了 <st c="67668">BinaryEncoder()</st> 来编码 <st c="67698">A</st><st c="67699">7</st> 变量。 使用 <st c="67720">fit()</st> 方法, <st c="67734">BinaryEncoder()</st> 创建了一个从类别到一组二进制列的映射,并且使用 <st c="67825">transform()</st> 方法,编码器在训练集和 测试集 中对 <st c="67869">A7</st> 变量进行了编码。****

第四章:3

变换数值变量

在数据分析中使用的统计方法对数据做出某些假设。 例如,在一般线性模型中,假设因变量(目标)的值是独立的,目标变量与自变量(预测变量)之间存在线性关系,以及残差——即预测值与目标真实值之间的差异——是正态分布且以 <st c="507">0</st>为中心。当这些假设不成立时,产生的概率陈述可能不准确。 为了纠正假设失败并提高模型性能,我们可以在分析之前变换变量。 分析

当我们变换一个变量时,我们用该变量的函数来替换其原始值。 使用数学函数变换变量有助于减少变量的偏度,提高值的分布范围,有时可以揭示预测变量与目标之间的线性关系和加性关系。 常用的数学变换包括对数、倒数、幂、平方和立方根变换,以及 Box-Cox 和 Yeo-Johnson 变换。 这一系列变换 通常被称为 方差稳定变换。方差稳定变换旨在将变量的分布调整为更对称——即高斯——的形状。 在本章中,我们将讨论何时使用每种变换,然后使用 NumPy、scikit-learn 和 Feature-engine 来实现它们。

本章包含以下内容: 以下食谱:

  • 使用对数函数变换变量

  • 使用倒数函数变换变量

  • 使用平方根来 变换变量

  • 使用 幂变换

  • 执行 Box-Cox 变换s

  • 执行 Yeo-Johnson 变换s

使用对数函数变换变量

对数函数是处理具有右偏分布(观测值在变量的较低值处累积)的正数据的有力转换。 一个常见的例子是 <st c="2138">收入</st> 变量,其值在较低工资处有大量累积。 对数转换对变量分布的形状有强烈的影响。

在这个 菜谱中,我们将使用 NumPy、scikit-learn 和 Feature-engine 执行对数转换。 我们还将创建一个诊断绘图函数来评估转换对变量分布的影响。

准备中

为了评估变量分布并了解转换是否改善了值分布并稳定了方差,我们可以通过直方图和分位数-分位数 (Q-Q) 图来直观地检查数据。 Q-Q 图帮助我们确定两个 变量是否显示出相似的分布。 在 Q-Q 图中,我们绘制一个变量的分位数与另一个变量的分位数。 如果我们绘制感兴趣变量的分位数与正态分布的预期分位数,那么我们可以确定我们的变量是否也是正态分布的。 如果变量是正态分布的,Q-Q 图中的点将沿着 45 度对角线。

注意

分位数是分布中低于某个分数的数据点的值。 因此,第 20 个分位数是分布中 20%的观测值低于且 80%高于该值的点。

如何做到这一点...

让我们首先导入 库并准备好 数据集:

  1. 导入所需的 Python 库 和数据集:

     import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    import scipy.stats as stats
    from sklearn.datasets import fetch_california_housing
    
  2. 让我们 将加利福尼亚住房数据集加载到一个 pandas DataFrame:

     X, y = fetch_california_housing(return_X_y=True,
        as_frame=True)
    
  3. 让我们通过绘制直方图来探索数据集中所有变量的分布 使用 pandas:

     X.hist(bins=30, figsize=(12, 12))
    plt.show()
    

    在以下输出中,我们可以看到 <st c="4084">MedInc</st> 变量显示出轻微的右偏斜分布,变量AveRooms<st c="4167">和</st>Population<st c="4182">严重右偏斜,而</st>HouseAge<st c="4225">变量在其</st>范围内` 的值分布得更加均匀:

图 3

图 3.1 – 数值变量的分布直方图

图 3.1 – 数值变量的分布直方图

  1. 为了 评估 变换对变量分布的影响,我们将创建一个函数,该函数接受 DataFrame 和变量名作为输入,并在 Q-Q 图旁边绘制直方图:

     def diagnostic_plots(df, variable):
        plt.figure(figsize=(15,6))
        plt.subplot(1, 2, 1)
        df[variable].hist(bins=30)
        plt.title(f"Histogram of {variable}")
        plt.subplot(1, 2, 2)
        stats.probplot(df[variable], dist="norm",
            plot=plt)
        plt.title(f"Q-Q plot of {variable}")
        plt.show()
    
  2. 让我们 步骤 4 中的函数绘制 <st c="5257">MedInc</st> 变量的分布图:

     diagnostic_plots(X, "MedInc")
    

    以下输出显示 <st c="5367">MedInc</st> 具有右偏斜的分布:

图 3.2 – MedInc 变量的直方图和 Q-Q 图

图 3.2 – MedInc 变量的直方图和 Q-Q 图

现在,让我们使用对数变换来转换数据:

  1. 首先,让我们复制原始 DataFrame:

     X_tf = X.copy()
    

    我们已经创建了一个副本,这样我们就可以修改副本中的值,而不是原始 DataFrame 中的值,这对于本食谱的其余部分是必需的。

注意

如果我们执行 <st c="5891">X_tf = X</st> 而不是使用 pandas 的 <st c="5925">copy()</st>函数, <st c="5942">X_tf</st> 将不会是 DataFrame 的副本;相反,它将是相同数据的另一个视图。 因此,在 <st c="6062">X_tf</st> 中做出的更改也会反映在 <st c="6088">X</st> 中。

  1. 让我们 创建一个包含我们想要转换的变量的列表:

     vars = ["MedInc", "AveRooms", "AveBedrms",
        "Population"]
    
  2. 让我们使用 NumPy 对步骤 7 中的变量应用对数变换,并将转换后的变量捕获在新 DataFrame 中:

     X_tf[vars] = np.log(X[vars])
    

注意

请记住,对数变换只能应用于严格正变量。 如果变量有零或负值,有时,添加一个常数使这些值变为正是有用的。 我们可以使用 <st c="6645">1</st> 添加一个常数值,使用 <st c="6653">X_tf[vars] = np.log(X[vars] +</st> <st c="6683">1)</st>

  1. 让我们使用 步骤 4中的诊断函数检查变换后 <st c="6719">MedInc</st> 的分布:

     diagnostic_plots(X_tf, "MedInc")
    

    在以下输出中,我们可以看到对数变换返回了一个分布更均匀的变量,在 Q-Q 图中更好地逼近理论正态分布:

图 3.3 – 对数变换后 MedInc 变量的直方图和 Q-Q 图

图 3.3 – 对数变换后 MedInc 变量的直方图和 Q-Q 图

继续 并绘制其他转换变量,以便熟悉对数变换对分布的影响。

现在,让我们 使用 <st c="7477">scikit-learn</st>应用对数变换。

  1. 让我们 导入 <st c="7504">FunctionTransformer()</st>

     from sklearn.preprocessing import FunctionTransformer
    

    在我们继续之前,我们需要复制原始数据集,就像我们在 步骤 6中所做的那样。

  2. 我们将设置变压器以应用对数并能够将转换后的变量还原到其 原始表示:

     transformer = FunctionTransformer(np.log,
        inverse_func=np.exp)
    

注意

如果我们设置 <st c="7885">FunctionTransformer()</st> 使用默认参数, <st c="7935">validate=False</st>,我们不需要在转换数据之前拟合变压器。 如果我们设置 <st c="8028">validate</st> <st c="8040">True</st>,变压器将检查输入到 <st c="8095">fit</st> 方法的数据。 后者在用 DataFrame 拟合变压器时很有用,以便它学习并存储 变量名称。

  1. 让我们将 步骤 7转换正变量:

     X_tf[vars] = transformer.transform(X[vars])
    

注意

Scikit-learn 转换器返回 NumPy 数组,并默认转换整个 DataFrame。 在这种情况下,我们将数组的输出结果直接赋值给现有的 DataFrame。 我们可以通过 <st c="8553">set_output</st> 方法更改返回的格式,并且我们可以通过 <st c="8623">ColumnTransformer()</st>限制要转换的变量。

使用 第 4 步中的诊断函数检查转换的结果。

  1. 现在让我们将转换回原始 变量表示:

     X_tf[vars] = transformer.inverse_transform(X_tf[vars])
    

    如果你通过执行 <st c="8901">diagnostic_plots(X_tf, "MedInc")</st>来检查分布,你应该看到一个与 第 5 步返回的相同的图表。

注意

为了给变量添加一个常数值,以防它们不是严格正的,请使用 <st c="9094">transformer = FunctionTransformer(lambda x: np.log(x +</st> <st c="9149">1)</st><st c="9151">)</st>.

现在,让我们使用特征-引擎应用对数变换。

  1. 让我们导入 <st c="9240">LogTransformer()</st>

     from feature_engine.transformation import LogTransformer
    
  2. 我们将设置转换器以转换第 7 步 的变量,然后拟合转换器到 数据集:

     lt = LogTransformer(variables = vars)
    lt.fit(X)
    

注意

如果 <st c="9489">变量</st> 参数留为 <st c="9519">None</st>,则 <st c="9525">LogTransformer()</st> 将对在 <st c="9608">fit()</st>期间找到的所有数值变量应用对数。或者,我们可以指定要修改的变量,就像我们在 第 15 步中所做的那样。

  1. 最后,让我们 转换 数据:

     X_tf = lt.transform(X)
    

    <st c="9752">X_tf</st> <st c="9775">X</st> DataFrame 的一个副本,其中第 7 步 的变量已用对数进行转换。

  2. 我们还可以将转换后的变量转换回它们的 原始表示:

     X_tf = lt.inverse_transform(X_tf)
    

    如果你检查第 17 步后的变量的分布,它们应该与原始数据的分布相同。

注意

<st c="10092">Feature-engine 有一个专门的转换器,在应用对数变换之前向变量添加常数值。</st> 在本方法的“更多内容…部分查看更多详细信息。

它是如何工作的...

在这个方法中,我们使用 NumPy、scikit-learn 和 Feature-engine 对正变量子集应用了对数变换

为了比较变换对变量分布的影响,我们创建了一个诊断函数来在 Q-Q 图旁边绘制直方图 为了创建 Q-Q 图,我们使用了 <st c="10633">scipy.stats.probplot()</st>,它绘制了感兴趣变量的分位数在 y 轴上与理论正态分布的分位数,我们在 x 轴上通过将 <st c="10820">dist</st> 参数设置为 <st c="10838">norm</st> 来表示。 我们使用 <st c="10866">matplotlib</st> 通过将 <st c="10912">plot</st> 参数设置为 <st c="10927">plt</st>来显示图表。

通过使用<st c="10940">plt.figure()</st> <st c="10957">figsize</st>,我们调整了图表的大小,并通过<st c="11011">plt.subplot()</st>,我们将两个图表组织在 <st c="11056">一</st> <st c="11069">两</st> 列中——也就是说,一个图表紧挨着另一个。 <st c="11139">plt.subplot()</st> 中的数字分别表示行数、列数和图表在图中的位置。 我们将直方图放在位置 1,将 Q-Q 图放在位置 2——也就是说,左和 右,分别。

为了测试该函数,我们在变换之前为<st c="11440">MedInc</st> 变量绘制了直方图和 Q-Q 图,并观察到 <st c="11501">MedInc</st> 不是正态分布。 大多数观测值位于直方图的左侧,并且在分布两端的 Q-Q 图中,值偏离了 45 度线。

接下来,使用 <st c="11700">np.log()</st>,我们对包含四个正变量的 DataFrame 的一个切片应用了对数。 为了评估变换的效果,我们绘制了变换后 <st c="11894">MedInc</st> 变量的直方图和 Q-Q 图。 我们观察到,在对数变换后,值在直方图中更加集中,而在 Q-Q 图中,它们仅从 45 度线向分布的末端偏离。

接下来,我们使用了 scikit-learn 的 <st c="12137">FunctionTransformer()</st> ,它可以将任何用户定义的函数应用于数据集。 我们将 <st c="12242">np.log()</st> 作为参数传递以应用对数变换,并使用 NumPy 的 <st c="12316">exp()</st> 进行逆变换到 <st c="12356">FunctionTransfomer()</st>。使用 <st c="12387">transform()</st> 方法,我们通过对数变换将 DataFrame 中正变量的一个切片进行了变换。 使用 <st c="12504">inverse_transform()</st>,我们将变量值还原到它们的 原始表示形式。

最后,我们使用了 Feature-engine 的 <st c="12625">LogTransformer()</st> ,并使用 <st c="12703">variables</st> 参数指定要变换的变量列表。 使用 <st c="12728">fit()</st>,变换器检查变量是否为数值且为正,而使用 <st c="12816">transform()</st>,它内部应用了 <st c="12840">np.log()</st> 来变换选定的变量。 使用 <st c="12906">inverse_transform()</st>,我们将变换后的变量还原到它们的 原始 表示形式。

还有更多……

Feature-engine 有一个 专门的 变换器,用于在应用对数之前将常数值添加到不是严格正的变量中: <st c="13161">LogCpTransformer()</st> <st c="13181">LogCpTransformer()</st> 可以:

  • 将相同的常数添加到 所有变量

  • 自动识别并添加所需的最小值以使 变量为正

  • 将用户定义的不同值添加到 不同的变量中。

您可以在本书的 GitHub 仓库中找到 <st c="13436">LogCpTransformer()</st> 的代码实现: https://github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch03-variable-transformation/Recipe-1-logarithmic-transformation.ipynb

使用倒数函数转换变量

倒数函数 定义为 1/x。当我们有比率时,它通常很有用,即两个变量的除法结果。 这些例子包括 人口密度 ,即每单位面积的人数,以及我们将在本配方中看到的房屋占用率 ,即每户的人数。

倒数变换在 0 值上未定义,尽管对于负值是定义的,但它主要用于转换 正变量。

在本配方中,我们将使用 <st c="14259">NumPy</st>, <st c="14266">scikit-learn</st> <st c="14284">Feature-engine</st>来实现倒数变换,并使用其直方图和 Q-Q 图比较其对变量分布的影响。

如何操作...

让我们 首先导入库并准备 数据集:

  1. 导入所需的 Python 库 和数据:

     import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    import scipy.stats as stats
    from sklearn.datasets import fetch_california_housing
    
  2. 让我们加载 加利福尼亚 住房数据集:

     X, y = fetch_california_housing(return_X_y=True,
        as_frame=True)
    
  3. 为了评估变量分布,我们将创建一个函数,该函数接受 DataFrame 和变量名作为输入,并在 Q-Q 图旁边绘制直方图:

     def diagnostic_plots(df, variable):
        plt.figure(figsize=(15,6))
        plt.subplot(1, 2, 1)
        df[variable].hist(bins=30)
        plt.title(f"Histogram of {variable}")
        plt.subplot(1, 2, 2)
        stats.probplot(df[variable], dist="norm",
            plot=plt)
        plt.title(f"Q-Q plot of {variable}") plt.show()
    
  4. 现在,让我们绘制 <st c="15240">AveOccup</st> 变量的分布,该变量指定了房屋的平均占用情况:

     diagnostic_plots(X, "AveOccup")
    

    变量 <st c="15348">AveOccup</st> 显示了一个非常强的 右偏分布,如下面的输出所示:

图 3.4 – AveOccup 变量的直方图和 Q-Q 图

图 3.4 – AveOccup 变量的直方图和 Q-Q 图

注意

The <st c="15772">AveOccup</st> 变量 指的是平均家庭人数 – 也就是说,在某个区域内的人数与房屋数量的比率。 这是一个适合倒数变换的有前途的变量。 您可以通过执行 <st c="16073">data = fetch_california_housing()</st> 然后 执行 <st c="16119">print(data.DESCR)</st>来找到更多关于变量和数据集的详细信息。

现在,让我们使用 NumPy 应用倒数变换

  1. 首先,让我们复制原始 DataFrame,这样我们就可以修改副本中的值,而不是原始数据,这对于接下来的 这个菜谱 是必需的:

     X_tf = X.copy()
    

注意

记住,执行 <st c="16415">X_tf = X</st> 而不是使用 pandas 的 <st c="16449">copy()</st> 会创建相同数据的额外视图。 因此,在 <st c="16537">X_tf</st> 中做出的更改也会反映在 <st c="16564">X</st>

  1. 让我们 将倒数变换应用到 <st c="16623">AveOccup</st> 变量上:

     X_tf["AveOccup"] = np.reciprocal(X_tf["AveOccup"])
    
  2. 让我们使用我们在第 3 步中创建的诊断函数检查变换后的 <st c="16729">AveOccup</st> 变量分布:

     diagnostic_plots(X_tf, "AveOccup")
    

注意

变换后, <st c="16890">AveOccup</st> 现在是某个区域内房屋数量与人口数量的比率 – 也就是说,每 位公民的房屋数量。

在这里,我们可以看到经过倒数变换后 <st c="17083">AveOccup</st> 变量分布的显著变化:

图 3.5 – 经过倒数变换后的 AveOccup 变量的直方图和 Q-Q 图

图 3.5 – 经过倒数变换后的 AveOccup 变量的直方图和 Q-Q 图的转换

现在,让我们使用 scikit-learn 应用倒数变换

  1. 让我们导入 FunctionTransformer()

     from sklearn.preprocessing import FunctionTransformer
    
  2. 让我们通过传递 <st c="17611">np.reciprocal</st> 作为参数来设置转换器:

     transformer = FunctionTransformer(np.reciprocal)
    

注意

默认情况下, <st c="17707">FunctionTransformer()</st> 在转换数据之前不需要拟合。

  1. 现在,让我们 复制原始数据集并转换变量:

     X_tf = X.copy()
    X_tf["AveOccup"] = transformer.transform(
        X["AveOccup"])
    

    您可以使用 步骤 3中的函数来检查转换的效果。

注意

倒数函数的逆变换也是倒数函数。 因此,如果您重新应用 <st c="18127">transform()</st> 到转换后的数据,您将将其还原为其原始表示。 更好的做法是将 <st c="18253">inverse_transform</st> 参数的 <st c="18288">FunctionTransformer()</st> 设置为 <st c="18313">np.reciprocal</st>

现在,让我们使用 倒数转换 <st c="18388">feature-engine</st>应用。

  1. 让我们导入 <st c="18421">ReciprocalTransformer()</st>

     from feature_engine.transformation import ReciprocalTransformer
    
  2. 让我们设置转换器以修改 <st c="18554">AveOccup</st> 变量,并将其拟合到 数据集:

     rt = ReciprocalTransformer(variables=»AveOccup»)
    rt.fit(X)
    

注意

如果将 <st c="18675">变量</st> 参数设置为 <st c="18704">None</st>,转换器将倒数函数应用于数据集中的所有数值变量。 如果某些变量包含一个 <st c="18840">0</st> 值,转换器将引发一个错误。

  1. 让我们 转换数据集中选择的变量:

     X_tf = rt.transform(X)
    

    <st c="18964">ReciprocalTransformer()</st> 将返回一个包含原始变量的新 pandas DataFrame,其中在 步骤 12 中指示的变量通过 以下 倒数函数进行转换。

如何工作...

在这个菜谱中,我们使用了 NumPy、scikit-learn 和 Feature-engine 应用了倒数转换。

为了评估变量分布,我们使用了函数来绘制一个直方图,旁边是一个 Q-Q 图,这是我们之前在本章的 如何工作… 部分中描述的。

我们绘制了<st c="19546">AveOccup</st> 变量的直方图和 Q-Q 图,这显示了严重的右偏分布;其大部分值位于直方图的左侧,并且在 Q-Q 图中偏离了向右端分布的 45 度线。

为了执行倒数变换,我们将 <st c="19783">np.reciprocal()</st> 应用于变量。 <st c="19854">变换后,</st> <st c="19880">AveOccup</st>的值在值范围内分布得更均匀,并且在 Q-Q 图中更接近正态分布的理论分位数。

接下来,我们使用了 <st c="20063">np.reciprocal()</st> 与 scikit-learn 的<st c="20099">FunctionTransformer()</st><st c="20126">transform()</st> 方法将<st c="20153">np.reciprocal()</st> 应用于 数据集。

注意

为了将 <st c="20216">FunctionTransformer()</st> 的影响限制在一组变量上,使用 <st c="20271">ColumnTransformer()</st>。要将输出更改为 pandas DataFrame,设置转换输出 `为 pandas。

最后,我们 使用了 Feature-engine 的 <st c="20406">ReciprocalTransformer()</st> 来特别修改一个变量。 使用 <st c="20472">fit()</st>,转换器检查该变量是否为数值型。 使用 <st c="20541">transform()</st>,转换器在内部应用了<st c="20578">np.reciprocal()</st> 来转换 该变量。

Feature-engine 的<st c="20653">ReciprocalTransformer()</st> 提供了通过<st c="20777">inverse_transform()</st> 方法直接将变量恢复到其原始表示的功能。

注意

使用 scikit-learn 或 Feature-engine 的转换器,而不是 NumPy 的<st c="20889">reciprocal()</st> 函数,允许我们在 scikit-learn 的<st c="21022">Pipeline</st> 对象 中应用倒数函数作为特征工程管道的额外步骤。

FunctionTransformer() <st c="21106">ReciprocalTransformer()<st c="21129">之间的区别在于,前者可以应用任何用户指定的变换,而后者仅应用倒数函数。</st>scikit-learn 默认返回 NumPy 数组,并转换数据集中的所有变量。 <st c="21344">另一方面,Feature-engine 的转换器返回 pandas DataFrame,并且可以在不使用</st> 额外类` 的情况下修改数据中的变量子集。

使用平方根变换变量

平方根变换,√x,以及其变体,Anscombe 变换,√(x+3/8),以及 Freeman-Tukey 变换,√x + √(x+1),都是方差稳定变换,可以将具有泊松分布的变量转换为具有近似标准高斯分布的变量。 <st c="21864">平方根变换是一种幂变换形式,其中指数为</st> **<st c="21951">1/2</st>** <st c="21954">,并且仅对</st> 正值` 定义。

泊松分布是一种概率分布,表示事件可能发生的次数。 <st c="22115">换句话说,它是一种计数分布。</st> 它是右偏斜的,其方差等于其均值。 `可能遵循泊松分布的变量示例包括客户的金融项目数量,例如当前账户或信用卡的数量,车辆中的乘客数量,以及家庭中的居住者数量。

在这个配方中,我们将使用 NumPy、scikit-learn 和 Feature-engine 实现平方根变换。

如何实现...

我们将 首先创建一个包含两个变量的数据集,这两个变量的值是从 <st c="22672">泊松分布</st> 中抽取的:

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

     import numpy as np import pandas as pd
    import scipy.stats as stats
    
  2. 让我们 创建一个包含两个变量的 DataFrame,这两个变量分别从均值为 <st c="22909">2</st> <st c="22915">3</st>的泊松分布中抽取,并且 <st c="22936">10000</st> 个观测值:

     df = pd.DataFrame()
    df["counts1"] = stats.poisson.rvs(mu=3, size=10000)
    df["counts2"] = stats.poisson.rvs(mu=2, size=10000)
    
  3. 让我们创建一个函数,它接受一个 <st c="23117">DataFrame</st> 和一个变量名作为输入,并在 Q-Q 图旁边绘制一个条形图,显示每个值的观测数:

     def diagnostic_plots(df, variable):
        plt.figure(figsize=(15,6))
        plt.subplot(1, 2, 1)
        df[variable].value_counts().sort_index(). plot.bar()
        plt.title(f"Histogram of {variable}")
        plt.subplot(1, 2, 2)
        stats.probplot(df[variable], dist="norm",
            plot=plt)
        plt.title(f"Q-Q plot of {variable}")
        plt.show()
    
  4. 让我们使用第 3 步中的函数为数据中的一个变量创建条形图和 Q-Q 图:

     diagnostic_plots(df, "counts1")
    

    在这里,我们可以看到输出中的泊松分布:

图 3.6 – counts1 变量的条形图和 Q-Q 图

图 3.6 – counts1 变量的条形图和 Q-Q 图

  1. 现在,让我们 <st c="23937">复制</st> <st c="23952">数据集:</st>

     df_tf = df.copy()
    
  2. 让我们将平方根转换应用于 <st c="23998">两个变量:</st>

     df_tf[["counts1", "counts2"]] = np.sqrt(
        df[["counts1","counts2"]])
    
  3. 让我们将值四舍五入到两位小数,以便更好地可视化:

     df_tf[["counts1", "counts2"]] = np.round(
        df_tf[["counts1", "counts2"]], 2)
    
  4. 让我们绘制转换后 <st c="24286">counts1</st> 的分布图:

     diagnostic_plots(df_tf, "counts1")
    

    我们看到方差更加 稳定 ,因为 Q-Q 图中的点更紧密地遵循 45 度对角线:

图 3.7 – counts1 变量平方根转换后的条形图和 Q-Q 图

图 3.7 – counts1 变量平方根转换后的条形图和 Q-Q 图

现在,让我们 <st c="24775">应用</st> <st c="24796">平方根</st> <st c="24812">转换</st> <st c="24817">scikit-learn</st>

  1. 让我们导入 <st c="24844">FunctionTransformer()</st> 并将其设置为执行平方根转换:

     from sklearn.preprocessing import FunctionTransformer
    transformer = FunctionTransformer(
        np.sqrt).set_output(transform="pandas")
    

注意

如果我们想像第 7 步中那样四舍五入值,我们可以使用以下方式设置转换器: <st c="25145">transformer = FunctionTransformer(func=lambda x:</st> <st c="25194">np.round(np.sqrt(x), 2))</st>

  1. 让我们复制数据并转换 <st c="25264">变量:</st>

     df_tf = df.copy()
    df_tf = transformer.transform(df)
    

    像我们在第 8 步中做的那样,检查转换的结果:

    要使用 Feature-engine 应用平方根,我们使用指数为 0.5 的 <st c="25469">PowerTransformer()</st>

    from feature_engine.transformation import PowerTransformer
    root_t = PowerTransformer(exp=1/2)
    
  2. 接下来,我们将转换器拟合到 <st c="25640">数据:</st>

     root_t.fit(df)
    

注意

转换器会自动识别数值变量,我们可以通过执行 <st c="25770">root_t.variables_</st>. 来探索这些变量。

  1. 最后,让我们对数据进行变换:

     df_tf = root_t.transform(df)
    

    <st c="25852">PowerTransformer()</st> 返回一个包含变换变量的 panda DataFrame。

它是如何工作的…

在这个步骤中,我们使用了 NumPy、scikit-learn 和 Feature-engine 应用了平方根变换。

我们使用了 NumPy 的 <st c="26067">sqrt()</st> 函数,或者在其内部通过 scikit-learn 的 <st c="26124">FunctionTransformer()</st> 来确定变量的平方根。 或者,我们使用了 Feature-engine 的 <st c="26227">PowerTransformer()</st>,将指数设置为 0.5,即平方根函数的指数。 NumPy 直接修改了变量。 scikit-learn 和 Feature-engine 的转换器在调用 <st c="26442">transform()</st> 方法时修改了变量。

使用幂变换

幂函数 是遵循以下 Xt=Xlambda 格式的数学变换,其中 lambda 可以取任何值。 平方和立方根变换是 lambda 分别为 1/2 或 1/3 的幂变换的特殊情况。 挑战在于找到 lambda 参数的值。 Box-Cox 变换,它是幂变换的推广,通过最大似然法找到最优的 lambda 值。 我们将在下面的步骤中讨论 Box-Cox 变换。 在实践中,我们将尝试不同的 lambda 值,并通过视觉检查变量分布来确定哪个提供了最佳的变换。 一般来说,如果数据是右偏斜的——也就是说,如果观测值累积在较低值附近——我们使用小于 1 的 lambda 值,而如果数据是左偏斜的——也就是说,在较高值附近的观测值更多——那么我们使用大于 1 的 lambda 值。

在这个步骤中,我们将使用 NumPy、scikit-learn 和 Feature-engine 执行幂变换。

如何操作...

让我们首先导入 库和准备数据集:

  1. 导入所需的 Python 库和类:

     import numpy as np
    import pandas as pd
    from sklearn.datasets import fetch_california_housing
    from sklearn.preprocessing import FunctionTransformer
    from feature_engine.transformation import PowerTransformer
    
  2. 让我们将加利福尼亚住房数据集加载到一个 pandas DataFrame 中:

     X, y = fetch_california_housing(
        return_X_y=True, as_frame=True)
    
  3. 为了评估变量分布,我们将创建一个函数,该函数接受 DataFrame 和变量名作为输入,并在 Q-Q 图旁边绘制直方图:

     def diagnostic_plots(df, variable):
        plt.figure(figsize=(15,6))
        plt.subplot(1, 2, 1)
        df[variable].hist(bins=30)
        plt.title(f"Histogram of {variable}")
        plt.subplot(1, 2, 2)
        stats.probplot(df[variable], dist="norm",
            plot=plt)
        plt.title(f"Q-Q plot of {variable}")
        plt.show()
    
  4. 让我们使用之前的函数绘制 <st c="28487">Population</st> 变量的分布图:

     diagnostic_plots(X, "Population")
    

    在之前命令返回的图表中,我们可以看到 <st c="28633">Population</st> 严重向右倾斜:

图 3.8 – 变量 Population 的直方图和 Q-Q 图

图 3.8 – 变量 Population 的直方图和 Q-Q 图

现在,让我们对 MedInc <st c="29026">Population</st> 变量应用幂变换。 由于两者都向右倾斜,指数小于 1 可能会使变量值分布更广。

  1. 让我们将需要变换的变量放入 一个列表中:

     variables = ["MedInc", "Population"]
    
  2. 让我们复制 DataFrame,然后对从步骤 5 (其中指数为 <st c="29375">0.3</st>的变量应用幂变换。

     X_tf = X.copy()
    X_tf[variables] = np.power(X[variables], 0.3)
    

注意

使用 <st c="29453">np.power()</st>,我们可以通过改变函数第二位指数的值来应用任何幂变换。

  1. 让我们检查 <st c="29629">Population</st>的分布变化:

     diagnostic_plots(X_tf, "Population")
    

    如之前命令返回的图表所示, <st c="29735">Population</st> 现在在值范围内分布更均匀,并且更接近正态分布的量级:

图 3.9 – 变量 Population 变换后的直方图和 Q-Q 图

图 3.9 – 变量 Population 变换后的直方图和 Q-Q 图

现在,让我们使用 scikit-learn 应用幂变换。

  1. 让我们设置 <st c="30178">FunctionTransformer()</st> 并使用指数转换,指数为 0.3

     transformer = FunctionTransformer(
        lambda x: np.power(x,0.3))
    
  2. 让我们复制 DataFrame 并将变量从 步骤 5转换:

     X_tf = X.copy()
    X_tf[variables] = transformer.transform(X[variables])
    

    这就完成了 – 我们现在可以检查变量分布。 最后,让我们使用 Feature-engine 执行指数转换。

  3. 让我们设置 <st c="30608">PowerTransformer()</st> 并使用指数 <st c="30647">0.3</st> 来转换变量,从 步骤 5。然后,我们将它拟合到 数据:

     power_t = PowerTransformer(variables=variables,
        exp=0.3)
    power_t.fit(X)
    

注意

如果我们没有定义要转换的变量, <st c="30847">PowerTransformer()</st> 将选择并转换 DataFrame 中的所有数值变量。

  1. 最后,让我们转换这些 两个变量:

     X_tf = power_t.transform(X)
    

该转换器 返回一个包含原始变量的 DataFrame,其中在 步骤 5 中指定的两个变量已使用转换函数进行转换。

它是如何工作的...

在这个菜谱中,我们使用了 NumPy、scikit-learn 和 Feature-engine 来应用幂转换。

要使用 NumPy 应用幂函数,我们对包含要转换的变量的数据集切片应用了 <st c="31336">power()</st> 方法。 要使用 scikit-learn 应用此转换,我们设置了 <st c="31480">FunctionTransformer()</st>,并在一个 <st c="31527">lambda</st> 函数中使用 <st c="31550">0.</st><st c="31552">3</st> 作为指数。 要使用 Feature-engine 应用幂函数,我们设置了 <st c="31632">PowerTransformer()</st> ,其中包含要转换的变量列表和指数 0.3

当我们调用 <st c="31808">transform()</st> 方法时,scikit-learn 和 Feature-engine 转换器应用了转换。 scikit-learn 的 <st c="31843">FunctionTransformer()</st> 修改整个数据集,并默认返回 NumPy 数组。 要返回 pandas DataFrames,我们需要将转换输出设置为 pandas,并且要应用转换到特定变量,我们可以使用 <st c="32072">ColumnTransformer()</st>。另一方面,Feature-engine 的 <st c="32110">PowerTransformer()</st>可以将转换应用于数据集的子集,默认返回 pandas DataFrames

执行 Box-Cox 转换

Box-Cox 转换 是幂变换家族的推广,定义为以下:

y(λ)=(yλ−1)λifλ≠0

y(λ)=logyifλ=0

在这里, y 是变量, λ 是转换参数。 它包括变换的重要特殊情况,例如未转换的 (λ = 1),对数 (λ = 0),倒数 (λ = - 1),平方根(当 λ = 0.5时,它应用了一个缩放和移位的平方根函数版本),以及立方根。

Box-Cox 转换评估了多个 λ 值,使用最大似然估计并选择返回最佳转换的 λ 参数。

在本教程中,我们将使用 scikit-learn 和 Feature-engine 执行 Box-Cox 转换 and Feature-engine.

注意

Box-Cox 转换只能用于正变量。 如果你的变量有负值,尝试使用下一道菜谱中描述的 Yeo-Johnson 转换, 执行 Yeo-Johnson 转换。或者,你可以在转换之前通过添加一个常数来移动变量的分布。

如何做到这一点...

让我们首先导入必要的库并准备好数据集: 数据集:

  1. 导入所需的 Python 库 和类:

     import numpy as np
    import pandas as pd
    import scipy.stats as stats
    from sklearn.datasets import fetch_california_housing
    from sklearn.preprocessing import PowerTransformer
    from feature_engine.transformation import BoxCoxTransformer
    
  2. 让我们将加利福尼亚住房数据集加载到一个 pandas DataFrame:

     X, y = fetch_california_housing(
        return_X_y=True, as_frame=True)
    
  3. 让我们 删除 <st c="33874">纬度</st> <st c="33887">经度</st> 变量:

     X.drop(labels=["Latitude", "Longitude"], axis=1,
        inplace=True)
    
  4. 让我们用直方图检查变量分布: 直方图:

     X.hist(bins=30, figsize=(12, 12), layout=(3, 3))
    plt.show()
    

    在以下输出中,我们可以看到 <st c="34134">中位数收入</st> 变量显示出轻微的右偏分布, <st c="34208">平均房间数</st> <st c="34221">人口</st> 变量严重右偏,而 <st c="34266">房屋年龄</st> 变量在其范围内显示出值的均匀分布:

图 3.10 – 数值变量的直方图

图 3.10 – 数值变量的直方图

  1. 让我们 将变量名捕获到一个列表中,因为我们将在下一步中使用这些变量: 以下步骤:

     variables = list(X.columns)
    
  2. 让我们创建一个函数,该函数将为数据中的所有变量绘制 Q-Q 图,每行三个图: 每个图:

     def make_qqplot(df):
        plt.figure(figsize=(10, 6),
            constrained_layout=True)
        for i in range(6):
            # location in figure
            ax = plt.subplot(2, 3, i + 1)
            # variable to plot
            var = variables[i]
            # q-q plot
            stats.probplot((df[var]), dist="norm",
                plot=plt)
            # add variable name as title
            ax.set_title(var)
        plt.show()
    
  3. 现在,让我们使用前面的函数显示 Q-Q 图: 函数:

     make_qqplot(X)
    

    通过查看以下图表,我们可以证实变量不是正态分布的:

图 3.11 – 数值变量的 Q-Q 图

图 3.11 – 数值变量的 Q-Q 图

接下来,让我们使用 scikit-learn 执行 Box-Cox 转换:

  1. 让我们设置 <st c="36083">PowerTransformer()</st> 以应用 Box-Cox 转换并将其拟合到数据中,以便找到最优的 *λ 参数:

     transformer = PowerTransformer(
        method="box-cox", standardize=False,
    ).set_output(transform="pandas")
    transformer.fit(X)
    

注意

为了避免数据泄露, λ 参数应该从训练集中学习,然后用于转换训练集和测试集。 因此,在拟合 <st c="36532">PowerTransformer()</st>之前,请记住将您的数据分为训练集和测试集。

  1. 现在,让我们转换 数据集:

     X_tf = transformer.transform(X)
    

注意

scikit-learn 的 <st c="36638">PowerTransformer()</st> 将学习到的 lambda 值存储在其 <st c="36691">lambdas_</st> 属性中,您可以通过执行 <st c="36736">transformer.lambdas_</st>.来显示它。

  1. 让我们使用直方图检查转换数据的分布:

     X_tf.hist(bins=30, figsize=(12, 12), layout=(3, 3))
    plt.show()
    

    在以下输出中,我们可以看到变量的 值在其范围内分布得更均匀:

图 3.12 – 变量变换后的直方图

图 3.12 – 变量变换后的直方图

  1. 现在,让我们返回转换变量的 Q-Q 图:

     make_qqplot(X_tf)
    

    在以下输出中,我们可以看到,经过转换后,变量更接近理论正态分布::

图 3.13 – 变量变换后的 Q-Q 图

图 3.13 – 变量变换后的 Q-Q 图:

现在,让我们使用 Feature-engine 实现 Box-Cox 转换:

  1. 让我们设置 <st c="38242">BoxCoxTransformer()</st> 以转换数据集中的所有变量,并将其拟合到 数据:

     bct = BoxCoxTransformer()
    bct.fit(X)
    
  2. 现在,让我们继续转换 变量:

     X_tf = bct.transform(X)
    

    转换返回一个包含 修改后的变量 的 pandas DataFrame。

注意

<st c="38532">scikit-learn 的 PowerTransformer()</st> 将转换整个数据集。 另一方面, <st c="38624">Feature-engine 的 BoxCoxTransformer()</st> 可以修改数据集的子集,如果我们将其名称列表传递给设置转换器时的 <st c="38742">variables</st> 参数。 如果将 <st c="38802">variables</st> 参数设置为 <st c="38832">None</st>,转换器将在 <st c="38898">fit()</st>过程中转换所有遇到的数值变量。

  1. Box-Cox 变换的最优 lambda 值存储在 <st c="38981">lambda_dict_</st> 属性中。 让我们 检查它们:

     bct.lambda_dict_
    

    上一条命令的输出如下: 以下:

    <st c="39094">{'MedInc': 0.09085449361507383,</st>
    <st c="39126">'HouseAge': 0.8093980940712507,</st>
    <st c="39158">'AveRooms': -0.2980048976549959,</st>
    <st c="39191">'AveBedrms': -1.6290002625859639,</st>
    <st c="39225">'Population': 0.235767</st><st c="39248">57812051324,</st>
    <st c="39261">'AveOccup': -0.4763032278973292}</st>
    

现在您知道了如何使用两个不同的 Python 库实现 Box-Cox 变换。

它是如何工作的...

scikit-learn 的 <st c="39421">PowerTransformer()</st> 可以应用 Box-Cox 和 Yeo-Johnson 变换,因此我们在设置变换器时通过传递 b<st c="39579">ox-cox</st> 字符串来指定了变换。 接下来,我们将变换器拟合到数据中,以便变换器学习每个变量的最优 lambda 值。 学习到的 lambda 值存储在 <st c="39746">lambdas_</st> 属性中。 最后,我们使用了 <st c="39787">transform()</st> 方法来变换 变量。

注意

记住,要返回 DataFrames 而不是数组,您需要通过 <st c="39946">set_output()</st> 方法指定变换输出。 您可以通过使用 <st c="40031">the</st> ColumnTransformer()`将变换应用于值的一个子集。

最后,我们使用 Feature-engine 应用了 Box-Cox 变换。 我们初始化了 <st c="40140">BoxCoxTransformer()</st>,将参数 <st c="40168">variables</st> 设置为 <st c="40201">None</st>。因此,变换器在 <st c="40299">fit()</st>过程中自动找到了数据中的数值变量。 我们将变换器拟合到数据中,以便它学习每个变量的最优 lambda 值,这些值存储在 <st c="40415">lambda_dict_</st>中,并使用 <st c="40469">transform()</st> 方法变换变量。 Feature-engine 的 <st c="40506">BoxCoxTransformer()</st> 可以接受整个 DataFrame 作为输入,并且它只修改了 选定的变量。

还有更多...

我们可以使用 SciPy 库应用 Box-Cox 转换。 有关代码实现,请访问本书的 GitHub 存储库: https://github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch03-variable-transformation/Recipe-5-Box-Cox-transformation.ipynb

执行 Yeo-Johnson 转换

The Yeo-Johnson 转换是 Box-Cox 转换的扩展,不再局限于正值。 换句话说,Yeo-Johnson 转换可以用于具有零值和负值的变量,以及正值。 这些转换的定义如下:

  • <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:mfracmml:mrowmml:msupmml:mrowmml:mo(</mml:mo>mml:miX</mml:mi>mml:mo+</mml:mo>mml:mn1</mml:mn>mml:mo)</mml:mo></mml:mrow>mml:mrowmml:miλ</mml:mi></mml:mrow></mml:msup>mml:mo-</mml:mo>mml:mn1</mml:mn></mml:mrow>mml:mrowmml:miλ</mml:mi></mml:mrow></mml:mfrac></mml:math>; 如果 λ ≠ 0 且 X >= 0

  • ln(X + 1); 如果 λ = 0 且 X >= 0

  • −(−X+1)2−λ−12−λ; 如果 λ ≠ 2 且 X < 0

  • -ln(-X + 1); 如果 λ = 2 且 X < 0

变量只有正值时,Yeo-Johnson 转换类似于变量加一的 Box-Cox 转换。 如果变量只有负值,那么 Yeo-Johnson 转换类似于变量负值加一的 Box-Cox 转换,其幂为 2- λ。如果变量既有正值又有负值,Yeo-Johnson 转换将对正负值应用不同的幂。

在这个菜谱中,我们将使用 scikit-learn 和 Feature-engine 执行 Yeo-Johnson 转换。

如何操作...

让我们首先 导入必要的库并准备 数据集:

  1. <st c="42052">导入所需的 Python 库</st> <st c="42090">和类:</st>

     import numpy as np import pandas as pd
    import scipy.stats as stats
    from sklearn.datasets import fetch_california_housing
    from sklearn.preprocessing import PowerTransformer
    from feature_engine.transformation import YeoJohnsonTransformer
    
  2. <st c="42338">让我们将加利福尼亚住房数据集加载到 pandas DataFrame 中,然后删除</st> <st c="42423">纬度</st> <st c="42431">和</st> <st c="42436">经度</st> <st c="42445">变量:</st>

     X, y = fetch_california_housing(
        return_X_y=True, as_frame=True)
    X.drop(labels=[«Latitude», «Longitude»], axis=1,
        inplace=True)
    

<st c="42584">注意</st>

<st c="42589">我们可以使用直方图和 Q-Q 图来评估变量分布,就像我们在</st> *步骤 4 *<st c="42687">步骤 7</st> <st c="42688">的</st> *执行 Box-Cox 变换 食谱 中做的那样。`

<st c="42738">现在,让我们</st> <st c="42750">使用 scikit-learn 应用 Yeo-Johnson 变换:</st>

  1. <st c="42805">让我们使用</st> <st c="42819">PowerTransformer()</st> <st c="42837">和</st> <st c="42847">yeo-johnson</st> <st c="42858">变换</st> <st c="42847">设置:</st>

     transformer = PowerTransformer(
        method="yeo-johnson", standardize=False,
    ).set_output(transform="pandas")
    
  2. <st c="42981">让我们</st> <st c="42988">将转换器拟合到</st> <st c="43011">数据:</st>

     transformer.fit(X)
    

<st c="43039">注意</st>

<st c="43044">*<st c="43049">λ</st>* 参数应该从训练集中学习,然后用于转换训练集和测试集。</st> <st c="43150">因此,在拟合</st> <st c="43227">PowerTransformer()</st> <st c="43245">之前,请记住将您的数据分为训练集和测试集。</st>

  1. <st c="43246">现在,让我们转换</st> <st c="43268">数据集:</st>

     X_tf = transformer.transform(X)
    

<st c="43312">注意</st>

<st c="43317">PowerTransformer()</st> <st c="43336">将学习到的参数存储在其</st> <st c="43374">lambda_</st> <st c="43381">属性中,您可以通过执行</st> <st c="43417">transformer.lambdas_</st> <st c="43447">来返回它。</st>

  1. <st c="43448">让我们使用直方图检查转换数据的分布:</st> <st c="43505">:</st>

     X_tf.hist(bins=30, figsize=(12, 12), layout=(3, 3))
    plt.show()
    

    <st c="43584">在以下输出中,我们可以看到变量的值在其范围内分布得更均匀:</st> <st c="43678">:</st>

图 3.14 – yeo-Johnson 变换后变量的直方图

<st c="43883">图 3.14 – yeo-Johnson 变换后变量的直方图</st>

<st c="43961">最后,让我们使用 Feature-engine 实现 Yeo-Johnson 变换:</st> <st c="43986">:</st>

  1. <st c="44038">让我们设置</st> <st c="44043">YeoJohnsonTransformer()</st> <st c="44076">来转换所有数值变量,然后将其拟合到</st> <st c="44133">数据:</st>

     yjt = YeoJohnsonTransformer()
    yjt.fit(X)
    

<st c="44183">注意</st>

如果将<st c="44188">变量</st> <st c="44196">参数</st> <st c="44205">设置为</st> <st c="44230">None</st> <st c="44234">,转换器将选择并转换数据集中所有数值变量。</st> `或者,我们可以传递一个列表来修改变量的名称。

与 scikit-learn 中的 <st c="44406">PowerTransformer()</st> 相比,Feature-engine 的转换器可以将整个 DataFrame 作为 <st c="44525">fit()</st> <st c="44535">transform()</st> 方法的参数,并且它只会修改 选定的变量。

  1. 让我们转换 变量:

     X_tf = yjt.transform(X)
    
  2. <st c="44662">YeoJohnsonTransformer()</st> 将每个变量的最佳参数存储在其 <st c="44734">lambda_dict_</st> 属性中,我们可以如下显示:

     yjt.lambda_dict_
    

    前一个命令返回以下字典: 以下字典:

    <st c="44862">{'MedInc': -0.1985098937827175,</st>
    <st c="44894">'HouseAge': 0.8081480895997063,</st>
    <st c="44926">'AveRooms': -0.5536698033957893,</st>
    <st c="44959">'AveBedrms': -4.3940822236920365,</st>
    <st c="44993">'Population': 0.2335</st><st c="45014">2363517075606,</st>
    <st c="45029">'AveOccup': -0.9013456270549428}</st>
    

现在你知道了 如何使用两个不同的开源库实现 Yeo-Johnson 变换。

它是如何工作的...

在这个菜谱中,我们使用了 <st c="45247">scikit-learn</st> <st c="45264">Feature-engine</st>实现了 Yeo-Johnson 变换。

<st c="45279">scikit-learn</st> <st c="45296">PowerTransformer()</st> 可以应用 Box-Cox 和 Yeo-Johnson 变换,因此我们使用 <st c="45415">yeo-johnson</st> 字符串指定了变换。 标准化的参数允许我们确定是否想要标准化(缩放)变换后的值。 接下来,我们将转换器拟合到 DataFrame 中,以便它学习每个变量的最佳 lambda 值。 <st c="45653">PowerTransformer()</st> 将学习到的 lambda 值存储在其 <st c="45706">lambdas_</st> 属性中。 最后,我们使用了 <st c="45747">transform()</st> 方法来返回变换后的变量。 我们将变换输出设置为 <st c="45834">pandas</st> 在变换后返回 DataFrames。

之后,我们使用 Feature-engine 应用了 Yeo-Johnson 转换。我们设置了 <st c="45974">YeoJohnsonTransformer()</st> ,以便它在fit()过程中转换所有数值变量。 我们将转换器拟合到数据中,使其学习每个变量的最佳 lambda 值,这些值存储在<st c="46175">lambda_dict_</st>中,并最终使用<st c="46237">transform()</st> 方法转换变量。 <st c="46257">Feature-engine 的</st> <st c="46274">YeoJohnnsonTransformer()</st> 可以接受整个 DataFrame 作为输入,但它只会转换选定的变量。

还有更多...

我们还可以使用 SciPy 库应用 Yeo-Johnson 转换。 有关代码实现,请访问本书的 GitHub 存储库: https://github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Second-Edition/blob/main/ch03-variable-transformation/Recipe-6-Yeo-Johnson-transformation.ipynb

第五章:4

执行变量离散化

离散化是将 通过创建一系列连续的区间将连续变量转换为离散特征的过程,也 称为 bins,这些区间跨越了变量值的范围。 随后,这些区间被视为 分类数据。

许多机器学习模型,例如决策树和朴素贝叶斯,与离散属性配合工作效果更好。 实际上,基于决策树的模型是根据属性上的离散划分来做出决策的。 在归纳过程中,决策树评估所有可能的特征值以找到最佳的切割点。 因此,特征值越多,树的归纳时间就越长。 从这个意义上说,离散化可以减少模型训练所需的时间。 模型。

离散化还有额外的优势。 数据被减少和简化;离散特征更容易被领域专家理解。 离散化可以改变偏斜变量的分布;当按等频对区间内的观测值进行排序时,值在范围内分布得更均匀。 此外,离散化可以通过将异常值放置在较低或较高的区间中,与分布的剩余内点 值一起,最小化它们的影响。 总的来说,离散化减少了数据并简化了数据,使学习过程更快,并可能产生更 准确的结果。

离散化也可能导致信息丢失,例如,通过将强烈关联不同类别或目标值的值组合到同一个区间中。 因此,离散化算法的目标是找到最小数量的区间,同时不造成显著的信息丢失。 在实践中,许多离散化过程需要用户输入将值排序到的区间数量。 然后,算法的任务是找到这些区间的切割点。 在这些过程中,我们发现最广泛使用的等宽和等频离散化方法。 基于决策树的离散化方法,否则,能够找到最优的分区数量,以及 切割点。

离散化过程可以分为监督****的非监督。非监督离散化方法仅使用变量的分布来确定连续箱的界限。 另一方面,监督方法使用目标信息来创建区间**。

在本章中,我们将讨论在成熟的开源库中广泛使用的监督和非监督离散化过程。 在这些过程中,我们将涵盖等宽、等频率、任意、k-means 和基于决策树的离散化。 更详细的方法,如 ChiMerge 和 CAIM,超出了本章的范围,因为它们的实现尚未开源。

本章包含以下食谱

  • 执行等宽离散化

  • 实现等频率离散化

  • 将变量离散化任意区间

  • 使用k-means 聚类进行离散化

  • 实现特征二值化

  • 使用决策树进行离散化

技术要求

在本章中,我们将使用数值计算库pandasnumpymatplotlibscikit-learnfeature-engine。我们还将使用yellowbrick Python 开源库,您可以使用pip安装它:

 pip install yellowbrick

更多关于yellowbrick的详细信息,请访问以下文档

www.scikit-yb.org/en/latest/index.html

执行等宽离散化

等宽离散化包括将变量的观察值范围划分为k等宽的区间,其中**k由用户提供。 变量 X 的区间宽度由以下公式给出: 以下公式:g:

Width=MaxX−Min(X)k

然后,如果变量的值在 0 到 100 之间变化,我们可以创建五个箱子,如下所示: 宽度 = (100-0) / 5 = 20。箱子将是 0-20,20-40,40-60 和 80-100。 第一个和最后一个箱子(0-20 和 80-100)可以通过将限制扩展到负无穷和正无穷来扩展,以容纳小于 0 或大于 100 的值。

在这个配方中,我们将使用 <st c="4265">pandas</st><st c="4273">scikit-learn</st><st c="4287">和特征工程`来执行等宽离散化。

如何操作...

首先,让我们导入必要的 Python 库并准备好 数据集:

  1. 让我们导入所需的库和函数:

     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
    
  2. 让我们加载加利福尼亚住房数据集的预测变量和目标变量:

     X, y = fetch_california_housing(
        return_X_y=True, as_frame=True)
    

注意

为了避免数据泄露,我们将使用训练集中的变量来找到区间的限制。 然后,我们将使用这些限制来离散化训练集和 测试集中的变量。

  1. 让我们将数据分为训练集和 测试集:

     X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.3, random_state=0)
    

    接下来,我们将使用 <st c="5172">pandas</st> 和配方开头描述的公式,将连续的 <st c="5130">HouseAge</st> 变量分为 10 个区间:

  2. 让我们捕获 HouseAge 的最小值和最大值:

     min_value = int(X_train["HouseAge"].min())
    max_value = int(X_train["HouseAge"].max())
    
  3. 让我们 确定区间宽度,即变量的值范围除以箱子的数量:

     width = int((max_value - min_value) / 10)
    

    如果我们执行 <st c="5541">print(width)</st>,我们将获得 <st c="5570">5</st>,这是区间的 大小。

  4. 现在我们需要定义区间限制并将它们存储在一个列表中:

     interval_limits = [i for i in range(
        min_value, max_value, width)]
    

    如果我们现在执行 <st c="5763">print(interval_limits)</st>,我们将看到 区间限制:

    <st c="5819">[1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51]</st>
    
  5. 让我们将第一个和最后一个区间的限制范围扩展,以容纳测试集或未来数据源中可能找到的较小或较大的值:

     interval_limits[0] = -np.inf
    interval_limits[-1] = np.inf
    
  6. 让我们复制一下 DataFrame,这样我们就不会覆盖原始的 DataFrame,我们将在食谱的后续步骤中需要它们:

     train_t = X_train.copy()
    test_t = X_test.copy()
    
  7. 让我们将 HouseAge 变量排序到我们在 步骤 6中定义的区间中:

     train_t["HouseAge_disc"] = pd.cut(
        x=X_train["HouseAge"],
        bins=interval_limits,
        include_lowest=True)
    test_t["HouseAge_disc"] = pd.cut(
        x=X_test["HouseAge"],
        bins=interval_limits,
        include_lowest=True)
    

注意

我们已将 <st c="6552">include_lowest=True</st> 设置为包含第一个区间的最低值。 请注意,我们使用训练集来找到区间,然后使用这些限制在两个数据集中对变量进行排序。

  1. 让我们打印离散化和原始变量的前 <st c="6763">5</st> 个观测值:

     print(train_t[["HouseAge", "HouseAge_disc"]].head(5))
    

    在以下输出中,我们可以看到 <st c="6921">52</st> 值被分配到 46-无穷区间, <st c="6977">43</st> 值被分配到 41-46 区间,以此类推:

     <st c="7033">HouseAge HouseAge_disc</st>
    <st c="7056">1989         52.0   (46.0, inf]</st>
    <st c="7078">256           43.0  (41.0, 46.0]</st>
    <st c="7100">7887         17.0  (16.0, 21.0]</st>
    <st c="7123">4581</st> <st c="7128">17.0  (16.0, 21.0]</st>
    <st c="7146">1993         50.0   (46.0, inf]</st>
    

注意

区间中的括号和方括号表示一个值是否包含在该区间内。 例如,(41, 46] 区间包含所有大于 41 且小于或等于 46 的值。

等宽离散化将不同数量的观测值分配给每个 区间。

  1. 让我们绘制一个条形图,显示训练集和测试集中 <st c="7558">HouseAge</st> 区间的观测比例:

     t1 = train_t["HouseAge_disc"].value_counts(
        normalize=True, sort=False)
    t2 = test_t["HouseAge_disc"].value_counts(
        normalize=True, sort=False)
    tmp = pd.concat([t1, t2], axis=1)
    tmp.columns = ["train", "test"]
    tmp.plot.bar(figsize=(8, 5))
    plt.xticks(rotation=45)
    plt.ylabel("Number of observations per bin")
    plt.xlabel('Discretized HouseAge')
    plt.title("HouseAge")
    plt.show()
    

    在以下输出中,我们可以看到训练集和测试集中每个区间的观测比例大致相同,但不同 区间之间不同:

图 4.1 – 离散化后每个区间的观测比例

图 4.1 – 离散化后每个区间的观测比例

使用 <st c="8386">feature-engine</st>,我们可以用更少的代码行和更多变量同时进行等宽离散化。

  1. 首先,让我们导入 离散化器:

     from feature_engine.discretisation import EqualWidthDiscretiser
    
  2. 让我们设置离散化器,将三个连续变量排序到八个区间中:

     variables = ['MedInc', 'HouseAge', 'AveRooms']
    disc = EqualWidthDiscretiser(
        bins=8, variables=variables)
    

注意

<st c="8798">EqualWidthDiscretiser()</st> 返回一个整数,表示默认情况下值是否被排序到第一个、第二个或第八个箱中。 这相当于顺序编码,我们在 用顺序数字替换类别 第二章 编码分类变量中进行了描述。要使用 <st c="9141">feature-engine</st> <st c="9159">category</st> <st c="9167">encoders</st> Python 库执行不同的编码,通过将 <st c="9245">return_object</st> 设置为 <st c="9262">True</st>将返回的变量转换为对象。或者,通过将 <st c="9342">return_boundaries</st> 设置为 <st c="9363">True</st>,使转换器返回区间限制。

  1. 让我们将离散化器拟合到训练集,以便它为每个变量学习切分点:

     disc.fit(X_train)
    

    拟合后,我们可以通过执行 <st c="9534">binner_dict_</st> 属性中的 <st c="9560">print(disc.binner_dict_)</st>来检查切分点。

注意

<st c="9600">feature-engine</st> 将自动将下限和上限区间的范围扩展到无限大,以适应未来数据中的潜在异常值。

  1. 让我们对训练集和测试集中的变量进行离散化:

     train_t = disc.transform(X_train)
    test_t = disc.transform(X_test)
    

    <st c="9872">EqualWidthDiscretiser()</st> 返回一个 DataFrame,其中选定的变量被离散化。 如果我们运行 <st c="9973">test_t.head()</st> 我们将看到以下输出,其中原始值 <st c="10050">MedInc</st> <st c="10058">HouseAge</st>,和 <st c="10072">AveRooms</st> 被替换为 区间数值:

图 4.2 – 包含三个离散化变量:HouseAge,MedInc 和 AveRooms 的 DataFrame

图 4.2 – 包含三个离散化变量:HouseAge,MedInc 和 AveRooms 的 DataFrame

  1. 现在,让我们 用每个区间的观测值比例制作条形图,以更好地理解等宽离散化的影响:

     plt.figure(figsize=(6, 12), constrained_layout=True)
    for i in range(3):
        # location of plot in figure
        ax = plt.subplot(3, 1, i + 1)
        # the variable to plot
        var = variables[i]
        # determine proportion of observations per bin
        t1 = train_t[var].value_counts(normalize=True,
            sort=False)
        t2 = test_t[var].value_counts(normalize=True,
            sort=False)
        # concatenate proportions
        tmp = pd.concat([t1, t2], axis=1)
        tmp.columns = ['train', 'test']
        # sort the intervals
        tmp.sort_index(inplace=True)
        # make plot
        tmp.plot.bar(ax=ax)
        plt.xticks(rotation=0)
        plt.ylabel('Observations per bin')
        ax.set_title(var)
    plt.show()
    

    区间包含不同数量的观测值,如下面的图表所示:

图 4.3 – 切分后每个区间的观测比例的条形图

图 4.3 – 切分后每个区间的观测比例的条形图

现在,让我们使用 scikit-learn 实现等宽切分。

  1. 让我们导入来自 scikit-learn 的类。

     from sklearn.compose import ColumnTransformer
    from sklearn.preprocessing import kBinsDiscretizer
    
  2. 让我们通过将其<st c="12035">strategy</st>设置为<st c="12047">uniform</st>来设置等宽切分器。

     disc = KBinsDiscretizer(
        n_bins=8, encode='ordinal', strategy='uniform')
    

注意

<st c="12134">KBinsDiscretiser()</st>可以通过设置<st c="12197">encoding</st><st c="12209">'ordinal'</st>或通过设置<st c="12249">encoding</st><st c="12261">'onehot-dense'</st>来返回整数形式的区间。

  1. 让我们使用<st c="12287">ColumnTransformer()</st>将切分限制为从步骤 13选定的变量。

    <st c="12378">ct = ColumnTransformer(</st>
     <st c="12401">[("discretizer", disc, variables)],</st>
     <st c="12437">remainder="passthrough",</st>
    <st c="12462">).set_output(transform="pandas")</st>
    

注意

<st c="12506">remainder</st>设置为<st c="12523">passthrough</st>时,<st c="12536">ColumnTransformer()</st>在转换后返回输入 DataFrame 中的所有变量。要仅返回转换后的变量,将<st c="12677">remainder</st>设置为<st c="12690">drop</st>

  1. 让我们将切分器拟合到训练集,以便它学习区间限制。

     ct.fit(X_train)
    
  2. 最后,让我们在训练集和测试集中对选定的变量进行切分。

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

    我们可以通过执行ct.named_transformers_["discretizer"].bin_edges_来检查 transformer 学习到的切分点。

注意

<st c="13058">ColumnTransformer()</st><st c="13091">discretize</st>附加到已切分的变量上,并将<st c="13145">remainder</st>附加到未修改的变量上。

我们可以通过执行<st c="13225">test_</st><st c="13230">t.head()</st>来检查输出。

它是如何工作的……

在这个方法中,我们将变量值排序到等距的区间中。为了使用 <st c="13360">pandas</st>进行离散化,我们首先使用 <st c="13449">max()</st> 和</st c="13459">min()<st c="13464">方法找到了</st>HouseAge<st c="13429">变量的最大值和最小值。</st> <st c="13474">然后,我们将值范围除以任意分箱的数量来估计区间宽度。</st> <st c="13573">有了宽度和最小值、最大值,我们确定了区间界限并将它们存储在一个列表中。</st> <st c="13685">我们使用这个列表和 pandas 的</st>cut()<st c="13720">函数将变量值排序到</st>区间`中。

注意

Pandas 的 <st c="13781">cut()</st> 函数默认按等大小区间对变量进行排序。 它会在每侧扩展变量范围 0.1%,以包含最小值和最大值。 我们手动生成区间的理由是为了在部署 <st c="14119">我们的模型</st>时,适应未来数据源中可能出现的比数据集中看到的更小或更大的值。

离散化后,我们通常将区间视为分类值。 默认情况下,pandas 的 <st c="14226">cut()</st> 函数返回有序整数作为区间值,这相当于顺序编码。 或者,我们可以通过设置 <st c="14390">labels</st> 参数为 <st c="14410">None</st>来返回区间界限。

为了显示每个区间的观测数,我们创建了一个条形图。 我们使用 pandas 的 <st c="14510">value_counts()</st> 函数来获取每个区间的观测数比例,该函数返回 pandas Series,其中索引是区间,计数是值。 为了绘制这些比例,首先,我们使用 pandas 的 <st c="14790">concat()</st>函数在一个 DataFrame 中连接了训练集和测试集的 Series,然后我们将 <st c="14849">train</st> <st c="14859">test</st> 列名分配给它。 最后,我们使用 <st c="14901">plot.bar()</st> 来显示条形图。 我们使用 Matplotlib 的 <st c="14975">xticks()</st>函数旋转了标签,并使用 <st c="15028">xlabels()</st> <st c="15042">ylabel()</st>添加了 x y 图例,以及使用 <st c="15073">title()</st>添加了标题。

要使用 <st c="15130">feature-engine</st>执行等宽离散化,我们使用了 <st c="15154">EqualWidth</st> Discretiser(),它接受 bin 数和要离散化的变量作为参数。 使用 <st c="15262">fit()</st>,离散化器学习每个变量的区间限制。 使用 <st c="15337">transform()</st>,它将值排序到每个 bin 中。

<st c="15385">EqualWidthDiscretiser()</st> 默认情况下返回排序后的整数 bins,这相当于顺序编码。 为了在 <st c="15586">feature-engine</st> <st c="15604">category encoders</st> 库中跟随任何其他编码过程,我们需要通过将 <st c="15687">return_object</st> 设置为 <st c="15704">True</st> 来设置转换器时,将 bins 转换为对象。

注意

<st c="15745">EqualWidthDiscretiser()</st> 默认情况下将第一个和最后一个区间的值扩展到负无穷和正无穷,以自动适应训练集中看到的较小和较大的值。

我们在离散化后使用条形图来显示每个转换变量的每个区间的观测值比例。 我们可以看到,如果原始变量是偏斜的,那么条形图也是偏斜的。 注意到 <st c="16212">MedInc</st> <st c="16218">和</st> <st c="16223">AveRooms</st> <st c="16231">变量</st> 的某些区间,这些变量具有偏斜分布,包含非常少的观测值。 特别是,尽管我们想要为 <st c="16370">AveRooms</st> <st c="16378">》 创建八个桶,但只有足够的数据创建五个,并且大多数变量的值都被分配到</st> <st c="16481">第一个区间</st>

最后,我们使用 <st c="16575">KBinsDiscretizer()</st> 从 scikit-learn 中将三个连续变量离散化成等宽桶。 <st c="16593">为了创建等宽桶,我们将</st> <st c="16652">strategy</st> <st c="16660">参数</st> <st c="16673">设置为</st> <st c="16680">uniform</st>。使用 fit()<st c="16692">,转换器学习了区间的极限,并使用</st> <st c="16756">transform()</st>,将值排序到 每个区间`。

我们使用了 <st c="16809">ColumnTransformer()</st> 来限制离散化只应用于选定的变量,并将转换输出设置为 pandas,以便在转换后获得一个 DataFrame。 <st c="16987">KBinsDiscretizer()</st> 可以返回作为序数的区间,正如我们在配方中所做的那样,或者作为 one-hot 编码的变量。 <st c="17115">通过</st> <st c="17156">encod</st><st c="17161">e</st> <st c="17163">参数</st> 可以修改这种行为。

参见也

为了比较等宽离散化与更复杂的方法,请参阅 Dougherty J, Kohavi R, Sahami M. 监督和非监督连续特征的离散化。在:第 12 届国际机器学习会议论文集。 旧金山:Morgan Kaufmann; 1995。 p。 194–202。

实现等频离散化

等宽离散化 <st c="17536">是直观且易于计算的。</st> 然而,如果变量是偏斜的,那么将会有许多空桶或只有少数值的桶,而大多数观测值将被分配到少数几个区间。 这可能会导致信息丢失。 通过自适应地找到区间切点,可以解决这个问题,使得每个区间包含相似比例的观测值。

等频率离散化 将变量的值划分为具有相同观测值比例的区间。 区间宽度由 分位数确定。分位数是分割数据为相等部分的值。 例如,中位数是一个将数据分为两半的分位数。 四分位数将数据分为四个相等的部分,而百分位数将数据分为 100 个相等大小的部分。 因此,区间可能具有不同的宽度,但观测值数量相似。 区间的数量由 用户定义。

在这个菜谱中,我们将使用 <st c="18599">pandas</st>, <st c="18607">scikit-learn</st>, <st c="18625">feature-engine</st>进行等频率离散化。

如何做到这一点...

首先,让我们 导入必要的 Python 库并准备好 数据集:

  1. 让我们导入所需的 Python 库 和函数:

     import pandas as pd
    import matplotlib.pyplot as plt
    from sklearn.datasets import fetch_california_housing
    from sklearn.model_selection import train_test_split
    
  2. 让我们将加利福尼亚住房数据集加载到 DataFrame 中:

     X, y = fetch_california_housing(
        return_X_y=True, as_frame=True)
    

注意

为了避免数据泄露,我们将从训练集中确定区间边界或分位数

  1. 让我们将数据分为训练集和 测试集:

     X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.3, random_state=0)
    
  2. 让我们复制 DataFrame:

     train_t = X_train.copy()
    test_t = X_test.copy()
    
  3. 我们将使用 pandas <st c="19420">qcut()</st>来获取 <st c="19463">HouseAge</st> 变量的离散化副本,并将其存储为训练集中的新列,以及八个 等频率区间的边界:

     train_t["House_disc"], interval_limits = pd.qcut(
        x=X_train["HouseAge"],
        q=8,
        labels=None,
        retbins=True,
    )
    

    如果你执行 <st c="19712">print(interval_limits)</st>,你会看到以下区间限制: <st c="19778">array([ 1., 14., 18., 24., 29., 34., 37.,</st> <st c="19820">44., 52.])</st>

  4. 让我们 打印离散化和 原始变量的前五个观测值:

     print(train_t[["HouseAge", "House_disc"]].head(5))
    

    在以下输出中,我们看到 <st c="20005">52</st> 值被分配到 44-52 区间, <st c="20055">43</st> 值被分配到 37-44 区间,以此类推:

     <st c="20111">HouseAge     House_disc</st>
    <st c="20131">1989       52.0   (44.0, 52.0]</st>
    <st c="20154">256        43.0   (37.0, 44.0]</st>
    <st c="20176">7887       17.0   (14.0, 18.0]</st>
    <st c="20199">4581       17.0   (14.0, 18.0]</st>
    <st c="20268">HouseAge</st> in the test set, using pandas <st c="20307">cut()</st> with the interval limits determined in *<st c="20352">step 5</st>*:
    
    

    test_t["House_disc"] = pd.cut(

    x=X_test["HouseAge"],
    
    bins=interval_limits,
    
    include_lowest=True)
    
    
    
  5. 让我们 制作一个条形图,展示训练集和 测试集中每个区间的观测值比例:

     # determine proportion of observations per bin
    t1 = train_t["House_disc"].value_counts(
        normalize=True)
    t2 = test_t["House_disc"].value_counts(normalize=True)
    # concatenate proportions
    tmp = pd.concat([t1, t2], axis=1)
    tmp.columns = ["train", "test"]
    tmp.sort_index(inplace=True)
    # plot
    tmp.plot.bar()
    plt.xticks(rotation=45)
    plt.ylabel("Number of observations per bin")
    plt.title("HouseAge")
    plt.show()
    

    <st c="20959">在下面的图表中,我们可以看到每个区间包含相似比例的观测值:</st> `

图 4.4 – 在等频率离散化后 HouseAge 每个区间的观测比例

<st c="21186">图 4.4 – 在等频率离散化后 HouseAge 每个区间的观测比例。</st>

<st c="21291">使用</st> <st c="21297">feature-engine</st> <st c="21311">,我们可以将等频率离散化应用于</st> <st c="21360">多个</st> <st c="21369">变量。</st>

  1. <st c="21379">让我们</st> 导入 离散化器:

     from feature_engine.discretisation import EqualFrequencyDiscretiser
    
  2. <st c="21477">让我们设置转换器将三个连续变量离散化成</st> 八个区间:

     variables = ['MedInc', 'HouseAge', 'AveRooms']
    disc = EqualFrequencyDiscretiser(
        q=8, variables=variables, return_boundaries=True)
    

<st c="21695">注意</st>

<st c="21700">使用</st> <st c="21706">return_boundaries=True</st> <st c="21728">,转换器将在离散化后返回区间边界。</st> <st c="21808">要返回区间编号,将其</st> <st c="21846">设置为</st> <st c="21849">False</st> <st c="21854">。</st>

  1. <st c="21855">让我们将</st> <st c="21870">离散化器拟合到训练集中,以便它学习区间限制:</st> `

    <st c="22048">disc.binner_dict_</st> attribute.
    

<st c="22076">注意</st>

<st c="22081">feature-engine</st> 将自动将下限和上限区间的范围扩展到无限大,以适应未来数据中的潜在异常值。

  1. <st c="22228">让我们将训练集和</st> 测试集中的变量 进行转换:

     train_t = disc.transform(X_train)
    test_t = disc.transform(X_test)
    
  2. <st c="22353">让我们绘制条形图,以观察每个区间的观测值比例,更好地理解等频率离散化的影响:</st> <st c="22457">。</st>

     plt.figure(figsize=(6, 12), constrained_layout=True)
    for i in range(3):
        # location of plot in figure
        ax = plt.subplot(3, 1, i + 1)
        # the variable to plot
        var = variables[i]
        # determine proportion of observations per bin
        t1 = train_t[var].value_counts(normalize=True)
        t2 = test_t[var].value_counts(normalize=True)
        # concatenate proportions
        tmp = pd.concat([t1, t2], axis=1)
        tmp.columns = ['train', 'test']
        # sort the intervals
        tmp.sort_index(inplace=True)
        # make plot
        tmp.plot.bar(ax=ax)
        plt.xticks(rotation=45)
        plt.ylabel("Observations per bin")
        # add variable name as title
        ax.set_title(var)
     plt.show()
    

    <st c="23092">在下面的</st> <st c="23110">图表中,我们可以看到区间具有相似比例的观测值:</st> <st c="23127">。</st> <st c="23170">。</st> `

图 4.5 – 在三个变量的等频率离散化后每个区间的观测比例

`图 4.5 – 在三个变量的等频率离散化后每个区间的观测比例。

<st c="23835">现在,让我们使用 scikit-learn</st> <st c="23853">执行等频率离散化:</st> `

  1. <st c="23906">让我们导入</st> <st c="23920">转换器:</st>

     from sklearn.preprocessing import KBinsDiscretizer
    
  2. <st c="23987">让我们设置离散化器将变量排序成八个</st> 等频率区间:

     disc = KBinsDiscretizer(
        n_bins=8, encode='ordinal', strategy='quantile')
    
  3. <st c="24141">让我们将</st> <st c="24156">离散化器拟合到包含来自</st> <st c="24226">步骤 10</st> <st c="24233">的变量的训练集切片中,以便它学习区间限制:</st> `

     disc.fit(X_train[variables])
    

<st c="24301">注意</st>

scikit-learn 的 <st c="24322">KBinsDiscretiser()</st> 将离散化数据集中的所有变量。 要仅离散化子集,我们将转换器应用于包含感兴趣变量的 DataFrame 切片。 或者,我们可以通过使用 <st c="24604">ColumnTransformer()</st>来限制离散化到变量的子集,就像我们在 执行等宽 离散化 食谱中做的那样。

  1. 让我们复制一个 DataFrame,我们将存储 离散化变量:

     train_t = X_train.copy()
    test_t = X_test.copy()
    
  2. 最后,让我们转换训练集和 测试集中的变量:

     train_t[variables] = disc.transform(
        X_train[variables])
    test_t[variables] = disc.transform(X_test[variables])
    

我们可以通过执行 disc.bin_edges_`来检查切点。

它是如何工作的…

在这个食谱中,我们将变量值排序到具有相似观测比例的区间。

我们使用了 pandas <st c="25194">qcut()</st> 从训练集中识别区间限制,并将 <st c="25279">HouseAge</st> 变量的值排序到这些区间。 接下来,我们将这些区间限制传递给 pandas <st c="25367">cut()</st> 以在测试集中离散化 <st c="25387">HouseAge</st> 请注意,pandas <st c="25430">qcut()</st>,就像 pandas <st c="25450">cut()</st>一样,返回了有序整数作为区间值,这相当于 序数编码

注意

使用等频率离散化,小连续范围内的许多值的出现可能会导致具有非常相似值的观测结果,从而产生不同的区间。 这个问题在于,它可能会在实际上性质相当相似的数据点之间引入人为的区别,从而偏置模型或后续 数据分析。

使用 Feature-engine 的 <st c="25938">EqualFrequencyDiscretiser()</st>,我们将三个变量离散化到八个箱子。 使用 <st c="26020">fit()</st>,离散化器学习了区间限制并将它们存储在 <st c="26094">binner_dict_</st> 属性中。 使用 <st c="26123">transform()</st>,观测值被分配到 `箱子中。

注意

<st c="26185">EqualFrequencyDiscretiser()</st> 默认返回一个整数,表示值是否被排序到第一个、第二个或第八个区间。 这相当于顺序编码,我们在 用顺序数字替换类别 食谱中进行了描述,该食谱位于 第二章编码 分类变量

为了在离散化之后使用不同类型的编码,我们可以通过将 <st c="26610">return_object</st> 设置为 <st c="26627">True</st> 来返回作为对象转换的变量,然后使用任何 <st c="26656">feature-engine</st> <st c="26674">category encoders</st> 转换器 。或者,我们可以返回区间限制,就像我们在 这个食谱中所做的那样。

最后,我们使用 <st c="26854">scikit-learn</st> <st c="26870">KBinsDiscretizer()</st>将变量离散化为八个等频率的区间。通过 <st c="26895">fit()</st>,转换器学习到了分割点并将它们存储在其 <st c="26964">bin_edges_</st> 属性中。 通过 <st c="26991">transform()</st>,它将值排序到每个区间。 请注意,与 <st c="27073">EqualFrequencyDiscretiser()</st>不同, <st c="27102">KBinsDiscretizer()</st> 将转换数据集中的所有变量。 为了避免这种情况,我们只对包含要修改的变量的数据子集应用了离散化器。

注意

scikit-learn 的 <st c="27293">KbinsDiscretizer</st> 有选项返回区间作为顺序数字或独热编码。 可以通过 <st c="27429">encode</st> 参数来修改行为。

将变量离散化到任意区间

在各个 行业中,将变量值分组到对业务有意义的段是常见的。 例如,我们可能希望将变量年龄分组到代表儿童、年轻人、中年人和退休人员的区间。 或者,我们可能将评分分组为差、好和优秀。 有时,如果我们知道变量处于某个尺度(例如,对数尺度),我们可能希望在 该尺度内定义区间分割点。

在这个菜谱中,我们将使用 <st c="27973">pan</st><st c="28058">das</st> <st c="28065">和</st> <st c="28070">feature-engine</st>.` 将一个变量离散化到预定义的用户区间。

如何做到这一点...

首先,让我们 导入必要的 Python 库并 准备好数据集:

  1. 导入 Python 库和类:

     import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    from sklearn.datasets import fetch_california_housing
    
  2. 让我们 将加利福尼亚住房数据集 加载到一个 <st c="28393">pandas</st> DataFrame:

     X, y = fetch_california_housing(
        return_X_y=True, as_frame=True)
    
  3. 让我们 绘制 <st c="28506">人口</st> 变量的直方图,以找出其 值范围:

     X["Population"].hist(bins=30)
    plt.title("Population")
    plt.ylabel("Number of observations")
    plt.show()
    

    人口 值在 0 大约 40,000 之间变化:

图 4.6 – 人口变量的直方图

图 4.6 – 人口变量的直方图

  1. 让我们 创建一个包含任意区间极限的列表,将上限设置为无穷大以适应 更大的值:

     intervals = [0, 200, 500, 1000, 2000, np.inf]
    
  2. 让我们 创建一个包含区间极限的字符串列表:

     labels = ["0-200", "200-500", "500-1000", "1000-2000",
        ">2000"]
    
  3. 让我们 复制数据集并 <st c="29229">人口</st> 变量离散化到从 步骤 4预定义的极限:

     X_t = X.copy()
    X_t[«Population_limits»] = pd.cut(
        X["Population"],
        bins=intervals,
        labels=None,
        include_lowest=True)
    
  4. 现在,让我们 <st c="29420">人口</st> 变量离散化到预定义的区间,并使用我们在 步骤 5 中定义的标签来命名区间,以便进行比较:

     X_t[«Population_range»] = pd.cut(
        X[„Population"],
        bins=intervals,
        labels=labels,
        include_lowest=True)
    
  5. 让我们 检查原始和 离散化变量的前五行:

     X_t[['Population', 'Population_range',
        'Population_limits']].head()
    

    在 DataFrame 的最后两列中,我们可以看到离散化变量:第一个以我们在 步骤 5 中创建的字符串作为值,第二个以 <st c="29963">区间极限</st>

     <st c="29979">Population Population_range Population_limits</st>
    <st c="30025">0       322.0          200-500    (200.0, 500.0]</st>
    <st c="30056">1      2401.0            >2000     (2000.0, inf]</st>
    <st c="30085">2       496.0          200-500    (200.0, 500.0]</st>
    <st c="30116">3       558.0         500-1000   (500.0, 1000.0]</st>
    <st c="30149">4       565.0         500-1000   (500.0, 1000.0]</st>
    

注意

我们只需要变量的一个版本,无论是值范围还是区间极限。 在这个菜谱中,我创建了两个,以突出 <st c="30381">pandas</st>.` 提供的不同选项。

  1. 最后,我们可以 计算并绘制每个区间内的 观察次数:

     X_t['Population_range'
        ].value_counts().sort_index().plot.bar()
    plt.xticks(rotation=0)
    plt.ylabel("Number of observations")
    plt.title("Population")
    plt.show()
    

    在下面的图中,我们可以看到每个区间的观察次数有所变化:

图 4.7 – 离散化后每个区间的观测比例

图 4.7 – 离散化后每个区间的观测比例。

为了总结 这个方法,让我们使用 <st c="30977">feature-engine</st>`来离散化多个变量 utilizing

  1. 让我们 导入 转换器:

     from feature_engine.discretisation import
        ArbitraryDiscretiser
    
  2. 让我们创建一个字典,将变量作为键,将区间极限作为值:

     intervals = {
        "Population": [0, 200, 500, 1000, 2000, np.inf],
        "MedInc": [0, 2, 4, 6, np.inf]}
    
  3. 让我们使用从 步骤 11的极限来设置离散化器:

     discretizer = ArbitraryDiscretiser(
        binning_dict=intervals, return_boundaries=True)
    
  4. 现在,我们可以继续对变量进行离散化 处理:

     X_t = discretizer.fit_transform(X)
    

    如果我们执行 <st c="31514">X_t.head()</st>,我们将看到以下输出,其中</st> 人口<st c="31580">和</st>中位收入` 变量已经被离散化:

图 4.8 – 包含离散化变量的 DataFrame

图 4.8 – 包含离散化变量的 DataFrame

使用 <st c="32147">feature-engine</st> 的优势在于我们可以同时离散化多个变量,并将任意离散化作为 scikit-learn <st c="32286">Pipeline</st>的一部分。

它是如何工作的...

在这个方法中,我们将一个变量的值排序到用户定义的区间中。首先,我们绘制了<st c="32430">Population</st>变量的直方图,以了解其值范围。接下来,我们任意确定了区间的限制,并将它们捕获在一个列表中。我们创建了包含 0-200、200-500、500-1000、1000-2000 以及超过 2,000 的区间,通过将上限设置为<st c="32711">np.inf</st>来设置。接下来,我们创建了一个包含区间名称的字符串列表。使用 pandas 的<st c="32792">cut()</st>,并传递包含区间限制的列表,我们将变量值排序到预定义的箱中。我们执行了两次命令;在第一次运行中,我们将<st c="32962">labels</st>参数设置为<st c="32981">None</st>,返回结果为区间限制。在第二次运行中,我们将<st c="33060">labels</st>参数设置为字符串列表。我们将返回的输出捕获在两个变量中:第一个变量显示区间限制作为值,第二个变量具有字符串作为值。最后,我们使用 pandas 的<st c="33311">value_counts()</st>对每个变量进行了观测次数的计数。

最后,我们使用<st c="33375">feature-engine</st><st c="33393">ArbitraryDiscretiser()</st>函数自动化了该过程。这个转换器接受一个字典作为键,其中包含要离散化的变量,以及作为值的列表,其中包含区间限制,然后使用 pandas 的<st c="33560">cut()</st>在底层对变量进行离散化。使用<st c="33615">fit()</st>,转换器不会学习任何参数,而是检查变量是否为数值型。使用<st c="33718">transform()</st>,它将变量进行离散化。

使用 k-means 聚类进行离散化

离散化过程的目标是找到一组切割点,将变量划分为几个具有良好类别一致性的小区间。为了创建将相似观测值分组在一起的分区,我们可以使用 k-means 等聚类算法。

在使用 k-means 聚类进行离散化时,分区是由 k-means 算法识别的聚类。聚类算法有两个主要步骤。 在初始化步骤中,k 个观测值被随机选择作为 k 个聚类的初始中心,其余数据点被分配到最近的聚类。 聚类之间的接近程度是通过距离度量来衡量的,例如欧几里得距离。 在迭代步骤中,聚类的中心被重新计算为聚类内所有观测值的平均值,观测值被重新分配到新创建的最近聚类。 迭代步骤会继续进行,直到找到最佳 k 中心

使用 k-means 进行离散化需要一个参数,即 k ,聚类的数量。有一些方法可以确定最佳聚类数量。 其中之一是肘部方法,我们将在本菜谱中使用。 这种方法包括使用不同的 k 值在数据上训练多个 k-means 算法,然后确定聚类返回的解释变异。 在下一步中,我们将解释变异作为聚类数量 k 的函数进行绘图,并选择曲线的 肘部 作为要使用的聚类数量。 肘部是拐点,表明增加 k 的数量不会显著增加模型解释的方差。 有几种不同的指标可以量化解释变异。 我们将使用每个点到其分配中心的平方距离之和。

在这个菜谱中,我们将使用 Python 库 <st c="35649">yellowbrick</st> <st c="35697">来决定最佳聚类数量,然后使用 scikit-learn 执行 k-means 离散化 `

如何操作...

让我们首先导入必要的 Python 库,并准备好数据集:

  1. 导入所需的 Python 库 和类:

     import pandas as pd
    from sklearn.cluster import KMeans
    from sklearn.datasets import fetch_california_housing
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import KBinsDiscretizer
    from yellowbrick.cluster import KElbowVisualizer
    
  2. 让我们将加利福尼亚住房数据集加载到一个 pandas DataFrame

     X, y = fetch_california_housing(
        return_X_y=True, as_frame=True)
    
  3. k-means 最佳簇应该使用训练集来确定,因此让我们将数据分为训练集和 测试集:

     X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.3, random_state=0)
    
  4. 让我们创建一个包含要转换的变量的列表:

     variables = ['MedInc', 'HouseAge', 'AveRooms']
    
  5. 让我们设置一个 k-means 聚类算法:

     k_means = KMeans(random_state=10)
    
  6. 现在,使用 Yellowbrick 的可视化器和肘部方法,让我们找到每个变量的最佳簇数量:

     for variable in variables:
        # set up a visualizer
        visualizer = KElbowVisualizer(
            k_means, k=(4,12),
            metric='distortion',
            timings=False)
        visualizer.fit(X_train[variable].to_frame())
        visualizer.show()
    

    以下 图表中,我们看到前两个变量的最佳簇数量是六个,第三个是七个:

图 4.9 – MedInc、HouseAge 和 AveRooms 变量中簇的数量与解释变异的关系,从上到下

图 4.9 – MedInc、HouseAge 和 AveRooms 变量中簇的数量与解释变异的关系,从上到下

  1. 让我们设置一个使用 k-means 聚类创建六个分区并将簇作为 one-hot 编码变量返回的离散化器:

     disc = KBinsDiscretizer(
        n_bins=6,
        encode="onehot-dense",
        strategy="kmeans",
        subsample=None,
    ).set_output(transform="pandas")
    
  2. 让我们将离散化器拟合到包含要离散化变量的 DataFrame 切片中,以便它为每个变量找到簇:

     disc.fit(X_train[variables])
    

注意

在这个菜谱中,我们将所有三个变量的值排序到六个簇中。 要将 <st c="38234">MedInc</st> <st c="38245">HouseAge</st> 离散化到六个分区,并将 <st c="38278">AveRooms</st> 离散化到七个,我们需要为每个变量组设置一个离散化器实例,并使用 <st c="38383">ColumnTransformer()</st> 将离散化限制在每个组中:

  1. 让我们检查 切割点:

     disc.bin_edges_
    

    每个数组包含 <st c="38555">MedInc</st> <st c="38563">HouseAge</st> <st c="38577">AveRooms</st>的六个簇的切割点:

    <st c="38587">array([array([0.4999, 2.49587954, 3.66599029, 4.95730115, 6.67700141, 9.67326677, 15.0001]),</st>
    <st c="38679">array([1., 11.7038878, 19.88430419, 27.81472503, 35.39424098, 43.90930314, 52.]),</st>
    <st c="38761">array([0.84615385, 4.84568771, 6.62222005, 15.24138445, 37.60664483, 92.4473438, 132.53333333])], dtype=object)</st>
    
  2. 让我们获取训练集测试集中变量的离散化形式:

     train_features = disc.transform(X_train[variables])
    test_features = disc.transform(X_test[variables])
    

    使用 <st c="39056">print(test_features)</st>,我们可以 检查离散化器返回的 DataFrame。 它包含 18 个二元变量,对应于每个三个数值变量返回的六个簇的 one-hot 编码转换: 转换

     <st c="39300">MedInc_0.0  MedInc_1.0  MedInc_2.0  MedInc_3.0  MedInc_4.0  MedInc_5.0  \</st>
    <st c="39368">14740            0.0            0.0</st> <st c="39383">1.0            0.0            0.0            0.0</st>
    <st c="39398">10101            0.0            0.0            0.0            1.0            0.0</st> <st c="39425">0.0</st>
    <st c="39428">20566            0.0            0.0            1.0            0.0            0.0            0.0</st>
    <st c="39458">2670              1.0</st> <st c="39468">0.0            0.0            0.0            0.0            0.0</st>
    <st c="39487">15709            0.0            0.0            0.0            1.0</st> <st c="39510">0.0            0.0</st>
     <st c="39517">HouseAge_0.0  HouseAge_1.0  HouseAge_2.0  HouseAge_3.0  HouseAge_4.0  \</st>
    <st c="39584">14740               0.0</st> <st c="39594">0.0               1.0               0.0               0.0</st>
    <st c="39610">10101               0.0               0.0               0.0               1.0               0.0</st>
    <st c="39636">20566               0.0               0.0               0.0               1.0               0.0</st>
    <st c="39662">2670                 0.0               0.0               0.0</st> <st c="39679">0.0               1.0</st>
    <st c="39687">15709               0.0               0.0               1.0               0.0               0.0</st>
     <st c="39713">HouseAge_5.0</st> <st c="39726">AveRooms_0.0  AveRooms_1.0  AveRooms_2.0  AveRooms_3.0  \</st>
    <st c="39780">14740               0.0               0.0               1.0               0.0</st> <st c="39803">0.0</st>
    <st c="39806">10101               0.0               0.0               1.0               0.0               0.0</st>
    <st c="39832">20566               0.0               0.0</st> <st c="39847">1.0               0.0               0.0</st>
    <st c="39858">2670                 0.0               0.0               1.0               0.0               0.0</st>
    <st c="39883">15709</st> <st c="39890">0.0               1.0               0.0               0.0               0.0</st>
     <st c="39909">AveRooms_4.0  AveRooms_5.0</st>
    <st c="39935">14740               0.0</st> <st c="39946">0.0</st>
    <st c="39949">10101               0.0               0.0</st>
    <st c="39963">20566               0.0               0.0</st>
    <st c="39977">2670                 0.0               0.0</st>
    <st c="39990">15709               0.0               0.0</st>
    

可以使用 <st c="40013">pandas</st> 将结果连接到原始 DataFrame,然后删除原始数值变量。 或者,使用 <st c="40146">ColumnTransformer()</st> 类将离散化限制在所选变量上,并通过将remainder 设置为 <st c="40285">"passthrough"</st>将结果添加到数据中。

它是如何工作的...

在这个配方中,我们使用 k-means 聚类进行了离散化。 首先,我们使用 Yellowbrick 的 <st c="40473">KElbowVisualizer()</st>通过肘部方法确定了最佳聚类数量。

为了执行 k-means 离散化,我们使用了 scikit-learn 的 <st c="40565">KBinsDiscretizer()</st>,将 <st c="40593">strategy</st> 设置为 <st c="40605">kmeans</st> ,并在 <st c="40653">n_bins</st> 参数中将聚类数量设置为六个。 使用 <st c="40675">fit()</st>,转换器通过 k-means 算法学习了聚类边界。 使用 <st c="40763">transform()</st>,它将变量值排序到相应的聚类。 我们将 <st c="40845">encode</st> 设置为 <st c="40855">"onehot-dense"</st>;因此,在离散化之后,转换器对聚类应用了独热编码。 我们还设置了离散化器的输出为 <st c="41008">pandas</st>,因此,转换器返回了作为 DataFrame 的聚类变量的独热编码版本。

另请参阅

实现特征二值化

一些数据集 包含稀疏变量。 稀疏变量是指大多数值都是 0 的变量。 稀疏变量的一个经典例子是通过词袋模型从文本数据中得到的,其中每个变量是一个单词,每个值代表该单词在某个文档中出现的次数。 由于一个文档包含有限数量的单词,而特征空间包含所有文档中出现的单词,因此大多数文档,即大多数行,对于大多数列将显示值为 0。 然而,单词并不是 唯一的例子。 如果我们考虑房屋细节数据,桑拿数量 变量对于大多数房屋也将是 0。 总之,一些变量具有非常偏斜的分布,其中大多数观测值显示相同的值,通常是 0,而只有少数观测值显示不同的值,通常是 更高的值。

为了更简单地表示这些稀疏或高度偏斜的变量,我们可以通过将所有大于 1 的值裁剪为 1 来对它们进行二值化。实际上,二值化通常在文本计数数据上执行,我们考虑的是特征的呈现或缺失,而不是单词出现的量化次数。 一个单词。

在这个菜谱中, 我们将使用 <st c="43035">scikit-learn</st>. 进行二值化。

准备工作

我们将使用一个包含词袋的语料库,它可在 UCI 机器学习仓库(https://archive.ics.uci.edu/ml/datasets/Bag+of+Words)找到。该数据集遵循 CC BY 4.0 https://creativecommons.org/licenses/by/4.0/legalcode)许可协议。

我下载并准备了一个小词袋,它代表了一个数据集的简化版本。您可以在附带的 GitHub 仓库中找到这个数据集:

https://github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/tree/main/ch04-discretization

如何实现...

让我们首先导入库并加载数据:

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

     import pandas as pd
    import matplotlib.pyplot as plt
    from sklearn.model_selection import train_test_split
    from sklearn.preprocessing import Binarizer
    
  2. 让我们加载包含单词作为列和不同文本作为行的词袋数据集:

     data = pd.read_csv("bag_of_words.csv")
    
  3. 让我们 显示直方图以可视化变量的稀疏性:

     data.hist(bins=30, figsize=(20, 20), layout=(3,4))
    plt.show()
    

    在以下直方图中,我们可以看到不同的单词在大多数文档中 没有出现:

图 4.10 – 表示每个单词在文档中出现的次数的直方图

图 4.10 – 表示每个单词在文档中出现的次数的直方图

  1. 让我们 设置 <st c="44737">binarizer</st> 将所有大于 1 的值裁剪为 1,并返回结果为 DataFrame:

     binarizer = Binarizer(threshold = 0) .set_output(transform="pandas")
    
  2. 让我们二进制化 变量:

     data_t = binarizer.fit_transform(data)
    

    现在我们可以通过显示直方图,如 第 3 步中所示,或者更好的方法,通过创建 条形图来探索二进制变量的分布。

  3. 让我们 创建一个条形图,显示每个变量中每个箱的观测数

     variables = data_t.columns.to_list()
    plt.figure(figsize=(20, 20), constrained_layout=True)
    for i in range(10):
        ax = plt.subplot(3, 4, i + 1)
        var = variables[i]
        t = data_t[var].value_counts(normalize=True)
        t.plot.bar(ax=ax)
        plt.xticks(rotation=0)
        plt.ylabel("Observations per bin")
        ax.set_title(var)
    plt.show()
    

    在下面的图中,我们可以看到二进制变量,其中大多数出现显示的是 <st c="45580">0</st> 值:

图 4.11 – 包含显示或未显示每个单词的文档数量的条形图

图 4.11 – 包含显示或未显示每个单词的文档数量的条形图(部分显示)

这就完成了; 现在 我们有了数据的更简单表示。

它是如何工作的…

在这个配方中,我们将稀疏变量的表示方式改为考虑出现与否,在我们的案例中,是一个单词。 数据由一个词袋组成,其中每个变量(列)是一个单词,每行是一个文档,值表示单词在文档中出现的次数。 大多数单词没有出现在 大多数文档中;因此,数据中的大多数值是 0。 我们通过直方图证实了数据的稀疏性。

scikit-learn 的 <st c="46899">Binarizer()</st> 将大于阈值的值映射到 <st c="46987">1</st> 值,而小于或等于阈值的值被映射到 0。 <st c="47063">Binarizer()</st> 具有 <st c="47083">fit()</st> <st c="47093">transform()</st> 方法,其中 <st c="47120">fit()</st> 不执行任何操作,而 <st c="47151">transform()</st> 将变量二值化。

<st c="47187">Binarizer()</st> 通过默认返回 NumPy 数组来修改数据集中的所有变量。 要返回 <st c="47281">pandas</st> DataFrame,我们将转换输出设置为 <st c="47337">pandas</st>

使用决策树进行离散化

在本章的所有先前 食谱中,我们任意确定区间的数量,然后离散化算法会以某种方式找到区间限制。 决策树可以自动找到区间限制和最优的箱数。

决策树方法在学习过程中将连续属性离散化。 在每个节点,决策树评估一个特征的所有可能值,并选择最大化类别分离或样本一致性的切割点,这通过使用性能指标如熵或基尼不纯度进行分类,或平方误差或绝对误差进行回归来实现。 因此,观察结果根据其特征值是否大于或小于某些 切割点而最终落在某些叶子节点。

在下面的图中,我们可以看到训练用于根据房产平均房间数预测房价的决策树图: :

图 4.12 – 基于房产平均房间数的房价预测决策树图

图 4.12 – 基于房产平均房间数的房价预测决策树图

根据这个决策树,平均房间数小于 5.5 的房屋将进入第一个叶子节点,平均房间数在 5.5 到 6.37 之间的房屋将进入第二个叶子节点,平均房间数在 6.37 到 10.77 之间的房屋将进入第三个叶子节点,而 平均房间数大于 10.77 的房屋将落在 第四个叶子节点。

如您所见,按照设计,决策树可以找到一组分割变量的切点,将变量分割成具有良好 类别一致性的区间。

在这个菜谱中, 我们将使用 Feature-engine 执行基于决策树的离散化:

如何做到这一点...

让我们首先导入一些库并加载数据: 数据:

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

     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 sklearn.tree import plot_tree
    from feature_engine.discretisation import DecisionTreeDiscretiser
    
  2. 让我们将加利福尼亚住房数据集加载到一个 <st c="49903">pandas</st> DataFrame 中,并将其分为训练集和 测试集:

     X, y = fetch_california_housing(return_X_y=True,
        as_frame=True)
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.3, random_state=0)
    
  3. 让我们创建一个包含要离散化变量名称的列表:

     variables = list(X.columns)[:-2]
    

    如果我们执行 <st c="50230">print(variables)</st>,我们将看到以下变量名称: <st c="50288">['MedInc'</st>, <st c="50299">'HouseAge'</st>, <st c="50311">'AveRooms'</st>, <st c="50323">'AveBedrms'</st>, <st c="50336">'</st>``<st c="50337">Population'</st>, <st c="50350">'AveOccup']</st>.

  4. 让我们设置 转换器以离散化 步骤 3中的变量。我们希望转换器根据三折交叉验证的负均方误差指标优化每个树的超参数的最大深度和每个叶子的最小样本数。我们希望离散化的输出是区间的范围: 作为离散化的输出,我们希望得到区间的范围:

     disc = DecisionTreeDiscretiser(
        bin_output="boundaries",
        precision=3,
        cv=3,
        scoring="neg_mean_squared_error",
        variables=variables,
        regression=True,
        param_grid={
            "max_depth": [1, 2, 3],
            "min_samples_leaf": [10, 20, 50]},
    )
    
  5. 让我们使用训练集来拟合离散化器,以便它为每个变量找到最佳的决策树:

     disc.fit(X_train, y_train)
    

注意

您可以通过执行 <st c="51181">disc.binner_dict_</st>来检查每个变量在 <st c="51145">binner_dict_</st> 属性中找到的区间的范围。注意离散化器如何将负无穷和正无穷添加到极限中,以适应训练集中观察到的较小和较大的值: 训练集:

  1. 让我们对变量进行离散化,然后显示转换后训练集的前五行:

     train_t = disc.transform(X_train)
    test_t = disc.transform(X_test)
    train_t[variables].head()
    

    在以下 输出中,我们可以看到每个观测值被分配到的区间的范围:

图 4.13 – 包含离散化变量的转换后训练集的前五行

图 4.13 – 包含离散化变量的转换后训练集的前五行

注意

如果你选择返回区间限制并希望使用这些数据集来训练机器学习模型,你需要对离散化进行后续的一热编码或顺序编码。 有关更多详细信息,请参阅 第二章 编码分类变量

  1. 我们不是返回区间限制,而是可以通过设置 将每个观测值分配到相应区间的转换器 来返回区间编号:

     disc = DecisionTreeDiscretiser(
        bin_output="bin_number",
        cv=3,
        scoring="neg_mean_squared_error",
        variables=variables,
        regression=True,
        param_grid={
            "max_depth": [1, 2, 3],
            "min_samples_leaf": [10, 20, 50]})
    
  2. 我们现在可以拟合并转换训练集和 测试集:

     train_t = disc.fit_transform(X_train, y_train)
    test_t = disc.transform(X_test)
    

    如果你现在执行 <st c="53076">train_t[variables].head()</st>,你将看到整数结果而不是区间限制:

图 4.14 – 包含离散化变量的转换训练集的前五行

图 4.14 – 包含离散化变量的转换训练集的前五行

为了总结这个配方,我们将使离散化器返回树的预测值作为离散化变量的替换值:

  1. 让我们设置 转换器以返回 预测值,然后将其拟合到训练集,最后转换 这两个数据集:

     disc = DecisionTreeDiscretiser(
        bin_output="prediction",
        precision=1,
        cv=3,
        scoring="neg_mean_squared_error",
        variables=variables,
        regression=True,
        param_grid=
            {"max_depth": [1, 2, 3],
                "min_samples_leaf": [10, 20, 50]},
    )
    train_t = disc.fit_transform(X_train, y_train)
    test_t = disc.transform(X_test)
    
  2. 让我们探索在离散化前后 <st c="54012">AveRooms</st> 变量的唯一值数量:

     X_test["AveRooms"].nunique(), test_t["AveRooms"].nunique()
    

    在以下输出中,我们可以看到决策树的预测值也是离散的或有限的,因为树包含有限数量的末端叶子节点; <st c="54290">7</st>,而原始 变量 包含超过 6000 个不同的值:

    (6034, 7)
    
  3. 为了更好地理解树的结构,我们可以将其捕获到一个 变量中:

     tree = disc.binner_dict_["AveRooms"].best_estimator_
    

注意

当我们设置转换器返回整数或箱限值时,我们将在 <st c="54614">binner_dict_</st> 属性中获得箱限值。 如果我们设置转换器返回树预测值, <st c="54696">binner_dict_</st> 将包含每个变量的训练树。

  1. 现在,我们可以显示 树结构:

     fig = plt.figure(figsize=(20, 6))
    plot_tree(tree, fontsize=10, proportion=True)
    plt.show()
    
  2. 在下面的图中,我们可以看到树根据房间数量的平均值将样本分配到不同的末端叶子节点所使用的值:

图 4.15 – 训练用于离散化 AveRooms 的决策树结构

图 4.15 – 训练用于离散化 AveRooms 的决策树结构

  1. 为了总结这个方法,我们可以绘制三个变量每个区间的观测数:

     plt.figure(figsize=(6, 12), constrained_layout=True)
    for i in range(3):
        ax = plt.subplot(3, 1, i + 1)
        var = variables[i]
        t1 = train_t[var].value_counts(normalize=True)
        t2 = test_t[var].value_counts(normalize=True)
        tmp = pd.concat([t1, t2], axis=1)
        tmp.columns = ["train", "test"]
        tmp.sort_index(inplace=True)
        tmp.plot.bar(ax=ax)
        plt.xticks(rotation=0)
        plt.ylabel("Observations per bin")
        ax.set_title(var)
    plt.show()
    

    我们可以在以下输出中看到每个区间的观测数:

图 4.16 – 使用决策树对变量进行离散化后的观测比例

图 4.16 – 使用决策树对变量进行离散化后的观测比例

如图中所示,使用决策树进行离散化在每个节点或区间返回不同比例的观测值。

它是如何工作的...

为了执行使用决策树的离散化,我们使用了feature-engineDecisionTreeDiscretiser()。这个转换器使用每个变量作为输入来拟合决策树,并优化模型的超参数以找到基于性能指标的最佳分区。它自动找到最佳区间数量及其限制,返回结果可以是限制、区间编号或预测。

还有更多...

<feature-engine>的实现灵感来源于 KDD 2009 数据科学竞赛的获胜方案。获胜者通过基于连续特征的决策树预测创建了新特征。你可以在文章系列的第 27 页找到更多细节,文章标题为《用集成选择赢得 KDD Cup Orange 挑战》Winning the KDD Cup Orange Challenge with Ensemble Selection

对于离散化技术的回顾,你可能觉得以下文章有用:

第六章:5

处理异常值

异常值 是指一个数据点在变量中的其他值中显著偏离。 异常值可能源于特征本身的固有变异性,表现为在分布中不常出现的极端值(通常出现在分布的尾部)。 它们可能是实验误差或数据收集过程中的不准确性的结果,或者它们可能表明重要事件。 例如,信用卡交易中异常高的费用可能表明欺诈活动,需要标记并可能阻止卡片以保护客户。 同样,异常明显的肿瘤形态可能表明恶性,需要进一步检查。

异常值可以对统计分析产生不成比例的大影响。 例如,少数异常值可以改变测试的统计显著性,无论是正向还是负向(想想 A/B 测试),或者直接影响统计模型参数的估计(想想系数)。 一些机器学习模型因其对异常值的敏感性而闻名,如线性回归。 其他模型因其对异常值的鲁棒性而闻名,如基于决策树的模型。 AdaBoost 据说对目标变量的异常值敏感,原则上,基于距离的模型,如 PCA 和 KNN,也可能受到异常值的存在的影响。 的影响。

没有严格的数学定义来界定什么算是异常值,也没有关于如何在统计或机器学习模型中处理异常值的共识。 如果异常值源于数据收集的缺陷,丢弃它们似乎是一个安全的选择。 然而,在许多数据集中,确定异常值的准确性质是具有挑战性的。 最终,检测和处理异常值仍然是一项主观的练习,依赖于领域知识和对它们对模型潜在影响的了解。

在本章中,我们将首先讨论识别潜在异常值的方法,或者更精确地说,识别那些显著偏离其他观察值的观察值。 然后,我们将在假设这些观察值对分析不相关的情况下继续,并展示如何通过截断来移除它们或减少它们对模型的影响。

本章包含以下内容 以下菜谱:

  • 使用箱线图和四分位数间距规则可视化异常值

  • 使用均值和 标准差

  • 使用中位数绝对偏差来 查找异常值

  • 移除异常值

  • 将异常值带回 可接受的范围

  • 应用 Winsorization

技术要求

在本章中,我们将使用 Python <st c="2626">numpy</st>, <st c="2633">pandas</st>, <st c="2641">matplotlib</st>, <st c="2653">seaborn</st>, 以及 <st c="2666">feature-engine</st> 库。

使用箱线图和四分位距规则可视化异常值

一种常见的可视化异常值的方法是使用箱线图。 箱线图 基于四分位数提供了一个变量的分布的标准化显示。 箱体包含第一和第三四分位数内的观测值,被称为 四分位距(IQR)。 第一四分位数是低于该值的观测值占 25%(相当于 25 百分位数),而第三四分位数是低于该值的观测值占 75%(相当于 75 百分位数)。 四分位距的计算 如下:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:miI</mml:mi>mml:miQ</mml:mi>mml:miR</mml:mi>mml:mo=</mml:mo>mml:mn3</mml:mn>mml:mir</mml:mi>mml:mid</mml:mi>mml:mo </mml:mo>mml:miq</mml:mi>mml:miu</mml:mi>mml:mia</mml:mi>mml:mir</mml:mi>mml:mii</mml:mi>mml:mil</mml:mi>mml:mie</mml:mi>mml:mo−</mml:mo>mml:mn1</mml:mn>mml:mis</mml:mi>mml:mit</mml:mi>mml:mo </mml:mo>mml:miq</mml:mi>mml:miu</mml:mi>mml:mia</mml:mi>mml:mir</mml:mi>mml:mii</mml:mi>mml:mil</mml:mi>mml:mie</mml:mi></mml:math>

箱线图还 显示触须,这些是从小箱体两端向外延伸到最小值和最大值的线条,并延伸到一个极限。 这些极限由分布的最小值或最大值给出,或者在存在极端值的情况下,由以下方程给出:

upperlimit=3rdquartile+IQR×1.5

lowerlimit=1stquartile−IQR×1.5

根据 IQR 接近规则,如果我们 的值落在由前述方程确定的触须限制之外,我们可以将其视为 异常值。 在箱线图中,异常值用点表示。

注意

如果变量具有正态分布,大约 99%的观测值将位于触须限定的区间内。 因此,我们可以将超出触须的值视为异常值。 然而,箱线图是非参数的,这就是为什么我们也使用它们来可视化 偏斜变量中的异常值。

在这个 菜谱中,我们将首先通过箱线图可视化变量分布,然后我们将手动计算触须的限制以确定哪些点可以被视为 异常值。

如何做到这一点...

我们将使用 <st c="4461">seaborn</st> 库创建箱线图。 让我们首先导入 Python 库并加载数据集:

  1. 让我们导入 Python 库和 数据集:

     import matplotlib.pyplot as plt
    import seaborn as sns
    from sklearn.datasets import fetch_california_housing
    
  2. 将默认背景从 <st c="4743">seaborn</st> (它使图表更美观,但这很主观, 当然):

     sns.set(style="darkgrid")
    
  3. 从 scikit-learn 加载加利福尼亚房价数据集

     X, y = fetch_california_housing(
        return_X_y=True, as_frame=True)
    
  4. 绘制 <st c="4985">MedInc</st> 变量的箱线图以可视化其分布:

     plt.figure(figsize=(8, 3))
    sns.boxplot(data=X["MedInc"], orient="y")
    plt.title("Boxplot")
    plt.show()
    

    在下面的箱线图中,我们识别出包含在四分位数范围(IQR)内的箱子,即第一和第三四分位数之间的观测值。 我们还可以看到触须。 在左边,触须延伸到 <st c="5371">MedInc</st>的最小值;在右边,触须延伸到第三四分位数加上 1.5 倍的四分位数范围。 超出右触须的值用点表示,可能 构成异常值:

Figure 5.1 – Boxplot of the MedInc variable highlighting potential outliers on the right tail of the distribution

图 5.1 – MedInc 变量的箱线图,突出显示了分布右尾的潜在异常值

注意

如图图 5.1*所示,箱线图返回不对称的边界,由左尾和右尾的长度变化表示。 这使得箱线图成为识别高度偏斜分布中异常值的一种合适方法。 正如我们将在接下来的菜谱中看到的那样,识别异常值的替代方法会在分布中心周围创建对称边界,这可能不是处理不对称分布的最佳选择。

  1. 现在让我们创建一个函数来 在直方图旁边绘制箱线图:

     def plot_boxplot_and_hist(data, variable):
        f, (ax_box, ax_hist) = plt.subplots(
            2, sharex=True,
            gridspec_kw={"height_ratios": (0.50, 0.85)})
        sns.boxplot(x=data[variable], ax=ax_box)
        sns.histplot(data=data, x=variable, ax=ax_hist)
        plt.show()
    
  2. 让我们使用 前面的 函数来创建 <st c="6487">MedInc</st> 变量的绘图:

     plot_boxplot_and_hist(X, "MedInc")
    

    在下面的图中,我们可以看到箱线图与直方图中显示的变量分布之间的关系。 注意大多数<st c="6685">MedInc</st>的观测值都位于 IQR 框内。</st> MedInc<st c="6746">的潜在异常值位于右尾,对应于收入异常高的个人:

图 5.2 – 箱线图和直方图 – 显示变量分布的两种方式

图 5.2 – 箱线图和直方图 – 显示变量分布的两种方式

现在我们已经看到了如何可视化异常值,让我们看看如何计算分布每侧的异常值所在的极限:

  1. 让我们创建一个函数,该函数根据 IQR 接近规则返回限制:

     def find_limits(df, variable, fold):
        q1 = df[variable].quantile(0.25)
        q3 = df[variable].quantile(0.75)
        IQR = q3 - q1
        lower_limit = q1 - (IQR * fold)
        upper_limit = q3 + (IQR * fold)
        return lower_limit, upper_limit
    

注意

请记住,第一四分位数和第三四分位数等同于 25%和 75%的分位数。 这就是为什么我们使用 pandas 的<st c="7577">quantile</st> 来确定这些值的原因。

  1. 使用步骤 7中的函数,我们将计算<st c="7683">MedInc</st>`的极端限制:

     lower_limit, upper_limit = find_limits(
        X, "MedInc", 1.5)
    

    如果我们现在执行 <st c="7768">lower_limit</st> <st c="7784">upper_limit</st>,我们将看到值 <st c="7820">-0.7063</st> <st c="7832">8.013</st>。下限超出了 <st c="7865">MedInc</st>的最小值,因此 在箱线图中,触须只延伸到最小值。 另一方面,上限与右侧 触须的界限相一致。

注意

乘以 IQR 的常见值是 <st c="8080">1.5</st>,这是箱线图中的默认值,或者 <st c="8128">3</st> 如果我们想更保守的话。

  1. 让我们显示 <st c="8215">HouseAge</st> 变量的箱线图和直方图:

     plot_boxplot_and_hist(X, "HouseAge")
    

    我们可以看到这个变量似乎不包含异常值,因此箱线图中的触须延伸到最小值和 最大值:

图 5.3 – HouseAge 变量的箱线图和直方图

图 5.3 – HouseAge 变量的箱线图和直方图

  1. 让我们 根据 IQR 邻近规则找到变量的界限:

     lower_limit, upper_limit = find_limits(
        X, "HouseAge", 1.5)
    

如果我们执行 <st c="8642">lower_limit</st> <st c="8658">upper_limit</st>,我们将看到值 <st c="8694">-10.5</st> <st c="8704">65.5</st>,这些值超出了图表的边缘,因此我们看不到 任何 异常值

它是如何工作的

在这个方法中,我们使用了 <st c="8829">boxplot</st> 方法从 Seaborn 创建箱线图,然后我们根据 IQR 邻近规则计算一个值被认为是异常值的界限。

图 5.2中,我们看到MedInc的箱线图的箱子从大约 2 延伸到 5,对应于第一和第三分位数(你可以通过执行X[MedInc].quantile(0.25)X[MedInc].quantile(0.75))来确定这些值)。我们还看到触须从左边的MedInc的最小值开始,延伸到右边的8.013(我们知道这个值,因为我们已经在步骤 8中计算了它)。MedInc显示了大于8.013的值,这些值在箱线图中以点表示。这些是可以被认为是异常值的值。

图 5.3中,我们展示了HouseAge变量的箱线图。箱子包含了从大约 18 到 35 的值(你可以通过执行X[HouseAge].quantile(0.25)X[HouseAge].quantile(0.75))来确定精确值)。触须延伸到分布的最小值和最大值。图中触须的极限与基于 IQR 接近规则(我们在步骤 10中计算)的极限不一致,因为这些极限远远超出了这个变量观察到的值范围。

使用均值和标准差查找异常值

正态分布的变量,大约有99.8%的观测值落在均值加减三倍标准差的区间内。因此,超出这些限制的值可以被认为是异常值;它们是罕见的。

注意

使用均值和标准差来检测异常值有一些缺点。 首先,它假设包括异常值在内的正态分布。 其次,异常值强烈影响均值和标准差。 因此,一个推荐的替代方案是 中位数绝对偏差 (MAD),我们将在下一道菜谱中讨论。

在这个菜谱中,我们将异常值定义为那些位于均值加减三倍标准差区间之外的观测值。 标准偏差。

如何操作...

让我们从导入 Python 库和加载数据集开始这道菜谱: :

  1. 让我们导入 Python 库 和数据集:

     import numpy as np
    import matplotlib.pyplot as plt
    import seaborn as sns
    from sklearn.datasets import load_breast_cancer
    
  2. 从 scikit-learn 加载乳腺癌数据集: :

     X, y = load_breast_cancer(
        return_X_y=True, as_frame=True)
    
  3. 创建一个函数来绘制 一个箱线图并放在 直方图旁边:

     def plot_boxplot_and_hist(data, variable):
        f, (ax_box, ax_hist) = plt.subplots(
            2, sharex=True,
            gridspec_kw={"height_ratios": (0.50, 0.85)})
        sns.boxplot(x=data[variable], ax=ax_box)
        sns.histplot(data=data, x=variable, ax=ax_hist)
        plt.show()
    

注意

我们在上一道菜谱的 步骤 3 中讨论了该函数, 使用箱线图和四分位距规则可视化异常值

  1. 让我们绘制 <st c="11761">均值</st> <st c="11766">平滑度</st> 变量的分布:

     plot_boxplot_and_hist(X, "mean smoothness")
    

    在下面的箱线图中,我们看到变量的值显示出类似于 正态分布的分布,并且它有六个异常值 – 一个在左侧,五个在 右侧尾部:

图 5.4 – 变量均值平滑度的箱线图和直方图

图 5.4 – 变量均值平滑度的箱线图和直方图

  1. 创建一个函数,该函数返回均值加减 <st c="12185">倍数</st> 的标准差,其中 <st c="12226">倍数</st> 是函数的参数:

     def find_limits(df, variable, fold):
        var_mean = df[variable].mean()
        var_std = df[variable].std()
        lower_limit = var_mean - fold * var_std
        upper_limit = var_mean + fold * var_std
        return lower_limit, upper_limit
    
  2. 使用该函数来识别 <st c="12527">均值</st> <st c="12532">平滑度</st> 变量的极端极限:

     lower_limit, upper_limit = find_limits(
        X, "mean smoothness", 3)
    

    如果我们现在执行 <st c="12637">lower_limit</st> <st c="12652">upper_limit</st>,我们将看到值 <st c="12688">0.0541</st> <st c="12699">0.13855</st>,对应于我们可以考虑一个值 为异常值的极限。

注意

如果变量呈正态分布,则均值加减三倍标准差之间的区间包含 99.87% 的观测值。 对于不太保守的限制,我们可以将标准差乘以 2 或 2.5,这将产生包含 95.4% 和 97.6% 的 观测值的区间。

  1. 创建 一个布尔向量,用于标记超出在 步骤 6中确定的限制值的观测值

     outliers = np.where(
        (X[«mean smoothness»] > upper_limit) |
        (X[«mean smoothness»] < lower_limit),
        True,
        False
    )
    

    如果我们现在执行 <st c="13358">outliers.sum()</st>,我们将看到值 <st c="13396">5</st>,表示有五个异常值或观测值小于或大于使用均值和标准差找到的极端值。 根据这些限制,我们将比 IQR 规则少识别一个异常值。

  2. 让我们从 步骤 3 开始,在直方图中添加红色垂直线,以突出显示由 使用均值和标准差 确定的限制值:

     def plot_boxplot_and_hist(data, variable):
        f, (ax_box, ax_hist) = plt.subplots(
            2, sharex=True,
            gridspec_kw={"height_ratios": (0.50, 0.85)})
        sns.boxplot(x=data[variable], ax=ax_box)
        sns.histplot(data=data, x=variable, ax=ax_hist)
        plt.vlines(
            x=lower_limit, ymin=0, ymax=80, color='r')
        plt.vlines(
            x=upper_limit, ymin=0, ymax=80, color='r')
         plt.show()
    
  3. 现在 让我们制作 这些图表:

     plot_boxplot_and_hist(X, "mean smoothness")
    

    在下面的图中,我们可以看到箱线图中 IQR 邻近规则观察到的限制值比均值和标准差确定的限制值更保守。 这就是为什么我们在箱线图中观察到六个潜在的异常值,但基于均值和标准差计算只有五个:

图 5.5 – 比较箱线图中触须与使用均值和标准差确定的限制值(直方图中的垂直线)

图 5.5 – 比较箱线图中触须与使用均值和标准差确定的限制值(直方图中的垂直线)

均值和标准差推导出的 边界是对称的。 它们从分布的中心等距离地向两端延伸。 如前所述,这些边界仅适用于正态 分布的变量。

它是如何工作的…

使用 pandas 的 <st c="15013">mean()</st> <st c="15024">std()</st>,我们捕获了变量的平均值和标准差。 我们将界限设定为平均值加减三倍标准差。 为了突出显示异常值,我们使用了 NumPy 的 <st c="15223">where()</st>。该 <st c="15236">where()</st> 函数扫描变量的行,如果值大于上限或小于下限,则被分配 <st c="15387">True</st>,否则分配 <st c="15411">False</st>。最后,我们使用 pandas 的 <st c="15443">sum()</st> 对这个布尔向量求和,以计算异常值的总数。

最后,我们比较了边界以确定由 IQR 邻近规则返回的异常值,我们在之前的菜谱中讨论了该规则,使用箱线图和四分位数邻近规则可视化异常值,以及平均值和标准差。 我们观察到 IQR 规则的界限不太保守。 这意味着使用 IQR 规则,我们会在这个特定变量中标记出更多的异常值。

使用中位数绝对偏差来寻找异常值

平均值 和标准差会受到异常值的影响很大。 因此,使用这些参数来识别异常值可能会适得其反。 识别异常值的一个更好的方法是 使用 MAD。 MAD 是每个观测值与变量中位数绝对偏差的中位数:

MAD=b×Median(xi−MedianX)

在之前的方程中, <st c="16340">xi</st> 是变量 <st c="16370">X</st> 中的每个观测值。 MAD 的美丽之处在于它使用中位数而不是平均值,这使得它对异常值具有鲁棒性。 常数 <st c="16481">b</st> 用于从 MAD 估计标准差,如果我们假设正态分布,那么 <st c="16578">b =</st> <st c="16582">1.4826</st>

注意

如果假设变量具有不同的分布, <st c="16656">b</st> 随后计算为 75 百分位数的倒数。 在正态分布的情况下,1/75 百分位数= 1.4826。

在计算 MAD 之后,我们使用中位数和 MAD 来建立分布限制,将超出这些限制的值指定为异常值。 限制设置为中位数加减 MAD 的倍数,通常范围在 2 到 3.5 之间。 我们选择的乘数因子反映了我们希望有多严格(越高,越保守)。 在这个菜谱中,我们将使用 MAD 来识别异常值。

如何操作...

让我们从导入 Python 库和加载数据集开始

  1. 让我们导入 Python 库 和数据集:

     import numpy as np
    import matplotlib.pyplot as plt
    import seaborn as sns
    from sklearn.datasets import load_breast_cancer
    
  2. 从 scikit-learn 加载 乳腺癌数据集

     X, y = load_breast_cancer(
        return_X_y=True, as_frame=True)
    
  3. 创建一个 基于 MAD 返回限制的函数

     def find_limits(df, variable, fold):
        median = df[variable].median()
        center = df[variable] - median
        MAD = center.abs().median() * 1.4826
        lower_limit = median - fold * MAD
        upper_limit = median + fold * MAD
        return lower_limit, upper_limit
    
  4. 让我们使用这个函数来捕捉 <st c="17891">均值</st> <st c="17896">平滑度</st> 变量的极端限制:

     lower_limit, upper_limit = find_limits(
        X, "mean smoothness", 3)
    

    如果我们执行 <st c="17996">lower_limit</st> <st c="18011">upper_limit</st>,我们将看到值 <st c="18047">0.0536</st> <st c="18058">0.13812</st>,对应于我们可以考虑一个值 为异常值的限制。

  5. 让我们创建一个布尔向量,标记超出 限制的观测值:

     outliers = np.where(
        (X[«mean smoothness»] > upper_limit) |
        (X[«mean smoothness»] < lower_limit),
        True,
        False
    )
    

    如果我们 现在执行 <st c="18359">outliers.sum()</st>,我们将看到值 <st c="18397">5</st>,表示有五个异常值或观测值小于或大于 MAD 找到的极端值。

  6. 让我们 编写一个函数,在变量的直方图旁边绘制箱线图,突出显示在直方图中计算的 步骤 4中的限制:

     def plot_boxplot_and_hist(data, variable):
        f, (ax_box, ax_hist) = plt.subplots(
            2, sharex=True,
            gridspec_kw={"height_ratios": (0.50, 0.85)})
        sns.boxplot(x=data[variable], ax=ax_box)
        sns.histplot(data=data, x=variable, ax=ax_hist)
        plt.vlines(
            x=lower_limit, ymin=0, ymax=80, color='r')
        plt.vlines(
            x=upper_limit, ymin=0, ymax=80, color='r')
        plt.show()
    
  7. 现在让我们制作 图表:

     plot_boxplot_and_hist(X, "mean smoothness")
    

    在下面的图中,我们可以看到箱线图中 IQR 接近规则观察到的限制比使用 MAD 确定的限制更为保守。 MAD 返回 对称边界,而箱线图生成非对称边界,这对于高度 偏斜的分布 更为合适:

图 5.6 – 箱线图胡须与使用 MAD 确定的限制之间的比较

图 5.6 – 箱线图中触须的极限与使用 MAD 确定的极限的比较

注意

使用中位数绝对偏差(MAD)检测异常值需要变量具有一定的变异性。 如果一个变量的超过 50%的值是相同的,则中位数与最频繁出现的值相同,MAD=0。 这意味着所有与中位数不同的值都将被标记为异常值。 这构成了使用 MAD 进行异常值检测的另一个局限性。 异常值检测。

就是这样! 你现在知道如何使用中位数 和中位数绝对偏差(MAD)来识别异常值。

它是如何工作的…

我们 使用 pandas 的 <st c="20011">median()</st> 和 pandas 的 <st c="20061">abs()</st>计算绝对差异。 接下来,我们使用 NumPy 的 <st c="20092">where()</st> 函数创建一个布尔向量,如果值大于上限或小于下限,则为 <st c="20141">True</st> ,否则为 <st c="20233">False</st> 最后,我们使用 pandas 的 <st c="20265">sum()</st> 计算布尔向量的总数 ,以确定异常值的总数。

最后,我们 比较了边界以确定 IQR 邻近规则返回的异常值,我们在 *使用箱线图和四分位数范围邻近规则可视化异常值 * 菜谱中讨论了这一点,以及使用 MAD 返回的异常值。 IQR 规则返回的边界不太保守。 可以通过将 IQR 乘以 3 而不是默认的 1.5 来改变这种行为,这是箱线图中的默认值。 此外,我们注意到 MAD 返回对称边界,而箱线图提供的是不对称的极限,这可能更适合 不对称分布。

另请参阅

为了全面讨论检测异常值的不同方法的优缺点,请参阅以下资源: 以下资源:

移除异常值

最近 的研究区分了三种类型的异常值:错误异常值、有趣异常值和随机异常值。 错误异常值可能是由于人为或方法错误造成的,应该纠正或从数据分析中移除。 在这个食谱中,我们假设异常值是错误(您不想移除有趣或随机异常值),并从 数据集中移除 它们。

如何操作...

我们将使用四分位数间距规则来查找异常值,然后使用 pandas 和 Feature-engine 从数据中移除它们 和特征工程:

  1. 让我们导入 Python 库、函数、 和类:

     import matplotlib.pyplot as plt
    import seaborn as sns
    from sklearn.datasets import fetch_california_housing
    from sklearn.model_selection import train_test_split
    from feature_engine.outliers import OutlierTrimmer
    
  2. 从 scikit-learn 加载加利福尼亚住房数据集,并将其分为训练集和 测试集:

     X, y = fetch_california_housing(
        return_X_y=True, as_frame=True)
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.3, random_state=0)
    
  3. 让我们创建一个函数,使用四分位数间距规则来确定我们将视为异常值的范围: 接近规则:

     def find_limits(df, variable, fold):
        q1 = df[variable].quantile(0.25)
        q3 = df[variable].quantile(0.75)
        IQR = q3 - q1
        lower_limit = q1 - (IQR * fold)
        upper_limit = q3 + (IQR * fold)
        return lower_limit, upper_limit
    

注意

步骤 3中,我们使用四分位数间距规则来确定数据点超出范围时的限制,我们已在 使用箱线图和四分位数间距规则可视化异常值 食谱中讨论过。 或者,您可以使用均值和标准差或 MAD 来识别异常值,正如我们在 使用均值和标准差查找异常值 使用中位数绝对偏差查找 异常值 食谱中所述。

  1. 使用 步骤 3 中的 *变量 <st c="23568">MedInc</st> 限制:

     lower, upper = find_limits(X_train, "MedInc", 3)
    

    如果您执行 <st c="23649">print(lower_limit, upper_limit)</st>,您将看到上一个命令的结果: <st c="23729">(-</st>``<st c="23731">3.925900000000002, 11.232600000000001)</st>

  2. 让我们保留训练集和测试集中那些值大于或等于(<st c="23872">ge</st>)下限的观测值:

     inliers = X_train["MedInc"].ge(lower)
    train_t = X_train.loc[inliers]
    inliers = X_test["MedInc"].ge(lower)
    test_t = X_test.loc[inliers]
    
  3. 让我们保留那些值低于或等于(<st c="24101">le</st>)上限的观测值:

     inliers = X_train["MedInc"].le(upper)
    train_t = X_train.loc[inliers]
    inliers = X_test["MedInc"].le(upper)
    test_t = X_test.loc[inliers]
    

    继续执行 ,然后执行 <st c="24280">X_train.shape</st> ,接着执行 <st c="24306">train_t.shape</st> ,以证实转换后的 DataFrame 在移除异常值后比原始 DataFrame 包含的观测值更少。

    我们可以使用 <st c="24514">feature-engine</st>同时从多个变量中移除异常值。

  4. 设置一个转换器,使用 IQR 规则识别三个变量中的异常值:

     trimmer = OutlierTrimmer(
        variables = [«MedInc", "HouseAge", "Population"],
        capping_method="iqr",
        tail="both",
        fold=1.5,
    )
    

注意:

<st c="24741">OutlierTrimmer</st> 可以使用 IQR 识别边界,正如我们在本食谱中所示,以及通过使用平均值和标准差,或 MAD。 您需要将 <st c="24903">capping_method</st> 更改为 <st c="24921">gaussian</st> <st c="24933">mad</st>,分别。

  1. 将转换器拟合到训练集,以便它学习这些限制:

     trimmer.fit(X_train)
    

注意:

通过执行 <st c="25063">trimmer.left_tail_caps_</st>,我们可以可视化三个变量的下限: <st c="25147">{'MedInc': -0.6776500000000012, 'HouseAge': -10.5, 'Population': -626.0}</st>。通过执行 <st c="25234">trimmer.right_tail_caps_</st>,我们可以看到变量的上限: <st c="25300">{'MedInc': 7.984350000000001, 'HouseAge': 65.5, '</st>``<st c="25349">Population': 3134.0}</st>

  1. 最后,让我们从训练集和测试集中移除异常值:

     X_train_t = trimmer.transform(X_train)
    X_test_t = trimmer.transform(X_test)
    

    为了完成这个食谱,让我们比较移除异常值前后变量的分布。

  2. 让我们创建 一个函数来在直方图上显示箱线图:

     def plot_boxplot_and_hist(data, variable):
        f, (ax_box, ax_hist) = plt.subplots(
            2, sharex=True,
            gridspec_kw={"height_ratios": (0.50, 0.85)}
        )
        sns.boxplot(x=data[variable], ax=ax_box)
        sns.histplot(data=data, x=variable, ax=ax_hist)
        plt.show()
    

注意:

我们之前在本章中较早的步骤 10 使用箱线图可视化异常值 食谱中讨论了代码。

  1. 让我们在移除 <st c="26071">MedInc</st> 的异常值之前绘制其分布图:

     plot_boxplot_and_hist(X_train, "MedInc")
    

    在下面的图中,我们可以看到 <st c="26184">MedInc</st> 是偏斜的,并且大于 8 的观测值被标记为异常值:

图 5.7–移除异常值前 MedInc 的箱线图和直方图

图 5.7–移除异常值前 MedInc 的箱线图和直方图。

  1. 最后,让我们绘制移除 <st c="26371">MedInc</st> 的异常值之后的分布图:

     plot_boxplot_and_hist(train_t, "MedInc")
    

    移除异常值后, <st c="26493">MedInc</st> 似乎不那么偏斜,其值分布更 均匀:

图 5.8 – 移除异常值后 MedInc 的箱线图和直方图

图 5.8 – 移除异常值后 MedInc 的箱线图和直方图

注意

使用 IQR 规则对转换变量进行分析会揭示新的异常值。 这并不奇怪;移除分布两端的观测值会改变参数,如中位数和四分位数,这些参数反过来又决定了触须的长度,从而可能将更多的观测值识别为异常值。 我们用来识别异常值的工具只是工具。 为了明确地识别异常值,我们需要用额外的 数据分析来支持这些工具。

如果您想从数据集中移除错误异常值,请确保比较并报告有异常值和无异常值的结果,以了解其对模型的影响程度。

它的工作原理...

The <st c="27340">ge()</st> <st c="27349">le()</st> 方法 来自 pandas,创建了布尔向量,用于识别超过或低于由 IQR 邻近规则设定的阈值的观测值。 我们使用这些向量与 pandas <st c="27526">loc</st> 一起使用,以保留在 IQR 定义的区间内的观测值。

The <st c="27597">feature-engine</st> 库的 <st c="27622">OutlierTrimmer()</st> 自动化了为多个变量移除异常值的程序。 <st c="27708">OutlierTrimmer()</st> 可以根据均值和标准差、IQR 邻近规则、MAD 或分位数来识别异常值。 我们可以通过 <st c="27868">capping_method</st> 参数来修改这种行为。

通过改变我们乘以四分位距(IQR)、标准差或 MAD 的系数,可以使得识别异常值的方法更加或更加保守。使用 <st c="28055">OutlierTrimmer()</st>,我们可以通过 <st c="28128">fold</st> 参数来控制方法的强度。

<st c="28149">tails</st> 设置为 <st c="28162">"both"</st><st c="28170">OutlierTrimmer()</st> 找到并移除了变量分布两端的异常值。要仅移除一端的异常值,我们可以将 <st c="28316">"left"</st><st c="28322">"right"</st> 传递给 <st c="28341">tails</st> 参数。

<st c="28357">OutlierTrimmer()</st> 采用 scikit-learn 功能,使用 <st c="28422">fit()</st> 方法来学习参数,以及 <st c="28461">transform()</st> 来修改数据集。使用 <st c="28501">fit()</st>,转换器学习并存储了每个变量的界限。使用 <st c="28578">transform()</st>,它从数据中移除了异常值,返回 <st c="28640">pandas</st> 数据框。

参见

这是之前提到的研究,它将异常值分类为错误;有趣且随机:Leys C, et.al. 2019. 如何分类、检测和管理单变量和多变量异常值,重点在于预注册。国际社会心理学评论。doi.org/10.5334/irsp.289

将异常值带回可接受范围内

移除错误异常值可能是一个有效的策略。然而,这种方法可能会降低统计功效,特别是在许多变量都有异常值的情况下,因为我们最终移除了数据集的大部分内容。处理错误异常值的另一种方法是将其带回可接受范围内。在实践中,这意味着用四分位距(IQR)邻近规则、均值和标准差或 MAD 确定的某些阈值来替换异常值。在本食谱中,我们将使用 <st c="29572">pandas</st><st c="29583">feature-engine</st> 来替换异常值。

如何做...

我们将使用均值和标准差来查找异常值,然后使用 <st c="29711">pandas</st><st c="29722">feature-engine</st> 替换它们的值:

  1. 让我们导入所需的 Python 库和函数:

     from sklearn.datasets import load_breast_cancer
    from sklearn.model_selection import train_test_split
    from feature_engine.outliers import Winsorizer
    
  2. 从 scikit-learn 加载乳腺癌数据集并将其分为训练集和测试集:

     X, y = load_breast_cancer(
        return_X_y=True, as_frame=True)
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=0.3, random_state=0)
    
  3. 让我们创建一个函数来使用均值和标准差查找异常值:

     def find_limits(df, variable, fold):
        var_mean = df[variable].mean()
        var_std = df[variable].std()
        lower_limit = var_mean - fold * var_std
        upper_limit = var_mean + fold * var_std
        return lower_limit, upper_limit
    

注意

步骤 3 中,我们使用均值和标准差来查找数据点被认为是异常值的极限,如 使用均值和标准差查找异常值 菜谱中所述。或者,您可以使用 IQR 规则或 MAD 识别异常值,正如我们在 使用箱线图和四分位数间距规则可视化异常值使用中位数绝对偏差查找异常值 菜谱中所述。

  1. 使用来自 步骤 3 的函数,让我们确定 <st c="30977">平均平滑度</st> 变量的极限,该变量大约遵循高斯分布:

     var = "worst smoothness"
    lower_limit, upper_limit = find_limits(
        X_train, var, 3)
    
  2. 让我们复制原始数据集:

     train_t = X_train.copy()
    test_t = X_test.copy()
    
  3. 现在,在新数据框中使用 步骤 4 中的下限或上限替换异常值:

     train_t[var] = train_t[var].clip(
        lower=lower_limit, upper=upper_limit)
    test_t[var] = test_t[var].clip(
        lower=lower_limit, upper=upper_limit)
    

    为了证实异常值已被替换为在 步骤 4 中确定的值,执行 <st c="31553">train_t["worst smoothness"].agg(["min", "max"])</st> 以获得新的最大和最小值。它们应该与变量的最小和最大值一致,或者与 步骤 4 中返回的极限一致。

    我们可以通过利用 <st c="31829">feature-engine</st> 同时替换多个变量中的异常值。

  4. 让我们设置一个转换器来替换两个变量中的异常值,使用由均值和 <st c="31952">标准差</st> 确定的极限:

     capper = Winsorizer(
        variables=[«worst smoothness», «worst texture»],
        capping_method="gaussian",
        tail="both",
        fold=3,
    )
    

注意

<st c="32096">Winsorizer</st> 可以 使用均值和标准差来识别边界,正如我们在本食谱中所示,以及 IQR 邻近规则和 MAD。 您需要将 <st c="32260">capping_meth</st><st c="32272">od</st> 更改为 <st c="32279">iqr</st> <st c="32286">mad</st>,分别。

  1. 让我们将变压器拟合到数据中,以便它学习 这些限制:

     capper.fit(X_train)
    

    通过执行 <st c="32408">capper.left_tail</st><st c="32424">_caps_</st>,我们可以可视化两个变量的下限: <st c="32490">{'worst smoothness': 0.06364743973736293, 'worst texture': 7.115307053129349}</st>。通过执行 <st c="32582">capper.right_tail_caps_</st>,我们可以看到变量的上限: <st c="32647">{'worst smoothness': 0.20149734880520967, 'worst</st> <st c="32696">texture': 43.97692158753917}</st>

  2. 最后,让我们用第 8 步中的限制来替换异常值:

     X_train = capper.transform(X_train)
    X_test = capper.transform(X_test)
    

    如果我们现在执行 <st c="32880">train_t[capper.variables_].agg(["min", "max"])</st>,我们将看到转换后的 DataFrame 的最大值和最小值与变量的最大值和最小值或识别到的限制值相匹配,以先到者为准:

     worst smoothness  worst texture
    min              0.071170        12.020000
    max              0.201411        43.953738
    

    如果您计划限制变量,确保在替换异常值前后比较您模型的性能或分析结果。

它是如何工作的...

The <st c="33378">clip()</st> 函数来自 pandas,用于将值限制在指定的上下限。 在本食谱中,我们使用均值和标准差找到了这些限制,然后剪裁了变量,使得所有观测值都位于这些限制之内。 worst smoothness 变量的最小值实际上大于我们在 第 4 步中找到的下限,因此其分布左侧没有值被替换。 然而,有一些值大于第 4 步中的上限,这些值被替换为限制值。 这意味着转换变量的最小值与原始变量的最小值相同,但最大值 则不同。

我们使用了 <st c="34048">feature-engine</st> 来同时替换多个变量的异常值。 <st c="34121">Winsorizer()</st> 可以根据均值和标准差、IQR 邻近规则、MAD 或使用百分位数来识别异常值。 我们可以通过 <st c="34292">capping_method</st> 参数来修改这种行为。

通过改变我们乘以 IQR、标准差或 MAD 的系数,可以使得识别异常值的方法更加或更保守。 使用 <st c="34479">Winsorizer()</st>,我们可以通过 <st c="34548">fold</st> 参数来控制方法的强度。

设置 <st c="34569">tails</st> <st c="34582">"both"</st> <st c="34590">Winsorizer()</st> 在变量的分布两端找到了并替换了异常值。 要替换任一端的异常值,我们可以将 <st c="34723">"left"</st> <st c="34733">"right"</st> 传递给 <st c="34748">tails</st> 参数。

<st c="34764">Winsorizer()</st> 采用 scikit-learn 的功能,使用 <st c="34825">fit()</st> 方法来学习参数,以及 <st c="34864">transform()</st> 方法来修改数据集。 使用 <st c="34904">fit()</st>,转换器学习并存储了每个变量的界限。 使用 <st c="34981">transform()</st>,它替换了异常值,返回 pandas DataFrames。

另请参阅

<st c="35072">feature-engine</st> 具有 <st c="35092">ArbitraryOutlierCapper()</st>,它可以将变量限制在任意最小和最大 值: https://feature-engine.readthedocs.io/en/latest/api_doc/outliers/ArbitraryOutlierCapper.html

应用 Winsorizer

截尾处理,或称为截尾化,包括 替换极端、不太知名的 观测值,即异常值,用下一个最大(或最小)观测值的幅度。 这与前一个菜谱中描述的 将异常值拉回到可接受范围内的过程类似,但并不完全相同。 截尾处理涉及在分布的两端替换 相同数量的异常值 ,这使得截尾处理成为一个对称过程。 这保证了 截尾均值,即替换异常值后的估计均值,仍然是变量中心趋势的稳健估计量。

在实践中,为了 在两端去除相似数量的观测值,我们会使用百分位数。 例如,第 5 百分位数是低于该值的观测值占总观测值的 5%,而第 95 百分位数是高于该值的观测值占总观测值的 5%。 使用这些值作为替换可能会在两端替换相似数量的观测值,但这并不保证。 如果数据集包含重复值,获得可靠的百分位数具有挑战性,并且可能导致每个尾部的值替换不均匀。 如果发生这种情况,那么截尾均值就不是中心趋势的良好估计量。 在本菜谱中,我们将 应用截尾处理。 应用截尾处理。

如何做到这一点...

我们将把乳腺癌数据集的所有变量限制在其第 5 和第 95 百分位数:

  1. 让我们导入所需的 Python 库 和函数:

     import matplotlib.pyplot as plt
    import seaborn as sns
    from sklearn.datasets import load_breast_cancer
    from sklearn.model_selection import train_test_split
    
  2. 从 scikit-learn 加载乳腺癌数据集

     X, y = load_breast_cancer(
        return_X_y=True, as_frame=True)
    
  3. 将数据 分离为训练集和 测试集:

     X_train, X_test, y_train, y_test = train_test_split(
        X,
        y,
        test_size=0.3,
        random_state=0,
    )
    
  4. 在字典中捕获每个变量的第 5 和第 95 百分位数

     q05 = X_train.quantile(0.05).to_dict()
    q95 = X_train.quantile(0.95).to_dict()
    
  5. 现在让我们一次性将所有变量的值替换为相应的百分位数 超过这些百分位数:

     train_t = X_train.clip(lower=q05, upper=q95)
    test_t = X_test.clip(lower=q05, upper=q95)
    
  6. 让我们显示在截尾处理之前一个变量的最小值、最大值和平均值:

     var = 'worst smoothness'
    X_train[var].agg(["min", "max", "mean"])
    

    我们可以在以下输出中看到值:

    <st c="37705">min      0.071170</st>
    <st c="37718">max      0.222600</st>
    <st c="37731">mean     0.132529</st>
    <st c="37745">Name: worst smoothness, dtype: float64</st>
    
  7. 显示截尾处理后的同一变量的最小值、最大值和平均值

     train_t[var].agg([„min", „max"])
    

    在以下输出中,我们可以看到最小值和最大值对应于百分位数。 然而,平均值与变量的原始平均值 非常相似:

    <st c="38081">min      0.096053</st>
    <st c="38094">max      0.173215</st>
    <st c="38107">mean     0.132063</st>
    <st c="38121">Name: worst smoothness, dtype: float64</st>
    

注意

如果您想将 Winsorization 作为 scikit-learn 管道的一部分使用,您可以使用 <st c="38251">feature-engine</st> 库的 <st c="38276">Winsorizer()</st>,通过以下方式设置: 设置如下:

<st c="38318">capper =</st> <st c="38328">Winsorizer(</st>

**<st c="38339">capping_method="quantiles",</st>**

****<st c="38367">tail="both",</st>**

****<st c="38380">fold=0.05,</st>**

**<st c="38391">)</st>

在此之后,按照 <st c="38422">fit()</st> <st c="38432">transform()</st> 方法进行操作,这些方法在 将异常值拉回到可接受 范围内 的食谱中有所描述。

值得注意的是,尽管使用了百分位数,但该过程并没有精确地替换分布两边的相同数量的观测值。 如果您打算对变量进行 Winsorization,请在 Winsorization 之前和之后比较分析结果。

它是如何工作的...

我们使用了 pandas 的 <st c="38834">quantiles()</st> 来获取数据集中所有变量的第 5 和第 95 个百分位数,并将其与 <st c="38943">to_dict()</st> 结合使用,以保留这些百分位数在字典中,其中键是变量,值是百分位数。 然后我们将这些字典传递给 pandas 的 <st c="39114">clip()</st> ,以将这些百分位数之外的值替换为百分位数。 通过使用字典,我们一次将多个变量 进行了上限处理。

另请参阅

有关 Winsorization 如何影响对称和不对称替换中的均值和标准差的更多详细信息,请查看 原始文章:

Dixon W. Simplified Estimation from Censored Normal Samples. The Annals of Mathematical Statistics, 1960. http://www.jstor.org/stable/2237953******

第七章:6

从日期和时间变量中提取特征

日期和时间变量包含有关日期、时间或两者的信息,在编程中,我们将它们统称为 <st c="177">datetime</st> 特征。出生日期、事件发生时间以及最后付款的日期和时间是 <st c="291">datetime</st> 变量的例子。

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

在本章中,我们将探讨如何通过利用 <st c="957">pandas</st> <st c="963">dt</st> 模块从日期和时间中提取特征,然后使用 <st c="1013">feature-engine</st> 自动化此过程。

本章将涵盖以下内容:

  • 使用 <st c="1112">pandas</st> 从日期中提取特征

  • 使用 <st c="1154">pandas</st> 从时间中提取特征

  • 捕获 <st c="1192">datetime</st> 变量之间的经过时间

  • 在不同的时区中处理时间

  • 使用 <st c="1297">feature-engine</st> 自动化 <st c="1264">datetime</st> 特征提取

技术要求

在本章中,我们将使用 <st c="1368">pandas</st><st c="1376">numpy</st><st c="1387">feature-engine</st> Python 库。

使用 pandas 从日期中提取特征

<st c="1477">datetime</st> 变量的值可以是日期、时间或两者。我们将首先关注包含日期的变量。我们很少使用原始数据与机器学习算法一起使用。相反,我们提取更简单的特征,如年份、月份或星期几,这些特征允许我们捕捉到季节性、周期性和趋势等洞察。

<st c="1809">The</st> <st c="1814">pandas</st> <st c="1820">Python 库非常适合处理日期和时间。</st> <st c="1877">利用</st> <st c="1891">pandas</st> <st c="1897">dt</st> <st c="1900">模块,我们可以访问</st> <st c="1927">datetime</st> <st c="1935">属性,从而提取许多特征。</st> <st c="1992">然而,为了利用此功能,变量需要转换为支持这些操作的数据类型,例如</st> <st c="2120">datetime</st> <st c="2128">或</st> <st c="2132">timedelta</st> <st c="2141">.</st>

<st c="2142">注意</st>

<st c="2147">The</st> <st c="2152">datetime</st> <st c="2160">variables can be cast as objects, particularly when we load the data from a CSV file.</st> <st c="2247">为了提取本章中我们将讨论的日期和时间特征,有必要将变量重新转换为</st> <st c="2374">datetime</st> <st c="2382">.</st>

在这个菜谱中,我们将学习如何通过利用 <st c="2462">panda</st><st c="2467">s</st> <st c="2469">.</st> 从日期中提取特征。

<st c="2470">准备就绪</st>

<st c="2484">以下是一些我们可以从</st> <st c="2553">date</st> <st c="2557">部分</st> <st c="2570">datetime</st> <st c="2578">变量</st> <st c="2602">使用</st> <st c="2608">pandas</st> <st c="2614">直接提取的特征</st>:

  • <st c="2616">pandas.Series.dt.year</st>

  • <st c="2637">pandas.Series.dt.quarter</st>

  • <st c="2662">pandas.Series.dt.month</st>

  • <st c="2685">pandas.Series.dt.isocalendar().week</st>

  • <st c="2721">pandas.Series.dt.day</st>

  • <st c="2742">pandas.Series.dt.day_of_week</st>

  • <st c="2771">pandas.Series.dt.weekday</st>

  • <st c="2796">pandas.Series.dt.dayofyear</st>

  • <st c="2823">pandas.Series.dt.day_of_year</st>

<st c="2852">我们可以</st> <st c="2860">使用我们使用</st> <st c="2897">pandas</st> <st c="2903">获得的特征</st> <st c="2906">创建更多特征,例如学期或</st> <st c="2958">是否为周末。</st> <st c="2983">我们将在下一节中学习如何做到这一点。</st>

<st c="3033">How to do it...</st>

<st c="3049">为了继续这个菜谱,让我们导入</st> <st c="3093">pandas</st> <st c="3099">和</st> <st c="3104">numpy</st> <st c="3109">,并创建一个</st> <st c="3124">样本 DataFrame</st> <st c="3374">:</st>

  1. <st c="3141">Let’s import</st> <st c="3155">the libraries:</st>

     import numpy as np
    import pandas as pd
    
  2. <st c="3208">我们将从创建 20 个</st> <st c="3236">datetime</st> <st c="3244">值开始,从</st> <st c="3267">2024-05-17</st> <st c="3277">午夜开始,然后以 1 天的增量递增。</st> <st c="3327">然后,我们将这些值捕获在一个</st> <st c="3365">DataFrame</st> <st c="3374">实例中,并显示前五行:</st>

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

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

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

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

注意

我们可以通过执行 <st c="3768">data["date"].dtypes</st> 来检查变量的数据格式。如果变量被转换为对象,我们可以通过执行 <st c="3938">data["date_dt"] =</st> <st c="3956">pd.to_datetime(data["date"])</st> <st c="3984">.</st> 将其转换为 <st c="3909">datetime</st> 格式。

  1. Let’s <st c="3991">extract the year pa</st> <st c="4011">rt of the date in a</st> new column and display the top five rows of the resulting DataFrame:

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

    我们在以下输出中看到新的 <st c="4164">year</st> `变量:

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

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

  1. 让我们从日期中提取年份的四分之一到一个新列,并显示前五行:

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

    我们在以下输出中看到新的 <st c="4589">quarter</st> `变量:

Figure 6.3 – 从日期中提取四分之一变量的 DataFrame 的前五行

图 6.3 – 从日期中提取四分之一变量的 DataFrame 的前五行

  1. 通过 <st c="4825">quarte</st> <st c="4831">r</st> <st c="4833">,我们现在可以创建 <st c="4857">semester</st> `特征:

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

注意

您可以使用 <st c="5002">pandas</st> <st c="5008">’</st> <st c="5011">unique()</st> <st c="5019">,例如,通过执行 <st c="5047">df["quarter"].unique()</st> <st c="5069"><st c="5073">df["semester"].unique()</st> <st c="5096">.</st> 来探索新变量的不同值。

  1. 让我们从日期中提取 <st c="5116">month</st> `部分到一个新列,并显示 DataFrame 的前五行:

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

    我们在以下输出中看到新的 <st c="5287">month</st> `变量:

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

图 6.4 – 带有新 month 变量的 DataFrame 的前五行变量

  1. 让我们从 <st c="5545">日期</st> 中提取周数(一年有 52 周):

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

    我们在 <st c="5646">下周</st> 变量中看到 以下输出:

图 6.5 – 带有新 week 变量的 DataFrame 的前五行

图 6.5 – 带有新 week 变量的 DataFrame 的前五行

  1. 让我们提取月份的天数,它可以取 <st c="5914">1</st> <st c="5920">31</st>之间的值,到一个新列中:

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

    我们 看到 <st c="6023">day_mo</st> 变量 以下输出中:

图 6.6 – 带有新变量捕获月份天数的 DataFrame 的前几行

图 6.6 – 带有新变量捕获月份天数的 DataFrame 的前几行

  1. 让我们提取星期几,其值介于 <st c="6303">0</st> <st c="6309">6</st> (从星期一到星期日),在一个新列中,然后显示前几行:

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

    我们在 <st c="6480">day_week</st> 变量中看到 以下输出:

图 6.7 – 带有表示一周中天的新变量的 DataFrame 的前几行

图 6.7 – 带有表示一周中天的新变量的 DataFrame 的前几行](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/B22396_06_7.jpg)

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

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

    我们在 <st c="6943">看到</st> 新的 <st c="6955">is_weekend</st> 变量 以下输出中:

图 6.8 – 带有新 is_weekend 变量的 DataFrame 的前五行

图 6.8 – 带有新 is_weekend 变量的 DataFrame 的前五行

注意

我们可以通过使用 <st c="7249">feature-engine</st>来自动提取所有这些特征。 查看本章的使用 feature-engine 自动化日期时间特征提取 食谱以获取更多详细信息。

有了这些,我们就使用 <st c="7475">pandas</st> <st c="7451">datetime</st> 变量的日期部分提取了许多 新特征。 这些特征对于数据分析、可视化和 <st c="7547">预测</st><st c="7556">建模</st>非常有用。

它是如何工作的...

在这个菜谱中,我们提取了从 一个 <st c="7649">datetime</st> 变量 使用 <st c="7682">pandas</st> <st c="7680">dt</st> 模块提取的许多与日期相关的特征。 首先,我们 使用 <st c="7786">pandas</st> <st c="7795">date_range()</st> 创建了一个包含日期的变量的样本 DataFrame。 使用 <st c="7924">periods</st> 参数,我们指明了要创建的值的范围数量——即日期的数量。 使用 <st c="8040">freq</st> 参数,我们指明了日期之间的步长大小。 在我们的例子中,我们使用了 <st c="8117">D</st> 来表示天数。 最后,我们使用 <st c="8205">pandas</st> <st c="8211">DataFrame()</st>将日期范围转换为一个 DataFrame。

为了提取日期的不同部分,我们使用了 <st c="8275">pandas</st> <st c="8284">dt</st> 来访问一个 <st c="8301">pandas</st> Series 的 <st c="8309">datetime</st> 属性,然后利用了不同的属性。 我们使用了 <st c="8392">year</st> <st c="8398">month</st> <st c="8409">quarter</st> 来将年份、月份和季度捕获到 DataFrame 的新列中。 为了找到学期,我们使用 NumPy 的 <st c="8549">where()</st> 结合新创建的 <st c="8595">quarter</st> 变量创建了一个布尔值。 NumPy 的 <st c="8621">where()</st> 扫描了 <st c="8655">quarter</st> 变量的值;如果它们小于 <st c="8699">3</st>,则返回 <st c="8727">1</st> ,对应于第一学期;否则,返回 <st c="8789">2</st>,对应于第二学期。

为了提取 不同 的日和周表示,我们 使用了 <st c="8903">isocalender().week</st>, <st c="8923">day</st>, 和 <st c="8932">dayofweek</st> 属性。 有了周几的信息,我们继续 创建了一个二元变量来表示是否为周末。 我们使用了 <st c="9070">where()</st> 函数来扫描周几,如果值大于 <st c="9141">4</st>,即周六和周日,函数返回 <st c="9208">True</st> ,否则返回 <st c="9227">False</st>。最后,我们将这个布尔向量转换为整数,以得到一个由 1 和 0 组成的二元变量。 有了这个,我们就从日期中创建了多个特征,这些特征可以用于数据分析和 预测建模。

还有更多...

使用 <st c="9459">pandas</st> <st c="9468">dt</st> 模块,我们可以 从日期中提取更多特征,这些特征来自 盒子。 例如,我们可以提取月份、季度或年份的开始和结束,是否为闰年,以及月份中的天数。 这些函数允许您 这样做:

  • <st c="9730">pandas.Series.dt.is_month_start</st>

  • <st c="9762">pandas.Series.dt.is_month_end</st>

  • <st c="9792">pandas.Series.dt.is_quarter_start</st>

  • <st c="9826">pandas.Series.dt.is_quarter_end</st>

  • <st c="9858">pandas.Series.dt.is_year_start</st>

  • <st c="9889">pandas.Series.dt.is_year_end</st>

  • <st c="9918">pandas.Series.dt.is_leap_year</st>

  • <st c="9948">pandas.Series.dt.days_in_month</st>

我们还可以使用 <st c="10043">pd.dt.days_in_month</st> 来返回特定月份的天数,以及一年中的天数(从 <st c="10091">1</st> <st c="10096">365</st> ,使用 <st c="10107">pd.dt</st><st c="10112">.dayofyear</st>

有关 更多详细信息,请访问 <st c="10152">pandas</st> <st c="10161">datetime</st> 文档: https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#time-date-components

另请参阅

要了解如何使用 pandas <st c="10351">date_ranges()</st> 创建不同的 <st c="10321">datetime</st> 范围,请访问 https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases

要了解更多关于 <st c="10480">pandas</st> <st c="10489">dt</st>, 请访问 https://pandas.pydata.org/pandas-docs/stable/reference/series.html#datetime-properties

使用 pandas 从时间中提取特征

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

准备就绪

我们可以使用以下 <st c="11200">pandas</st> <st c="11209">datetime</st> 属性来提取小时、分钟和秒:

  • <st c="11229">pandas.Series.dt.hour</st>

  • <st c="11251">pandas.Series.dt.minute</st>

  • <st c="11276">pandas.Series.dt.second</st>

如何操作...

在本 教程 中,我们 提取 <st c="11357">分钟</st> <st c="11369">秒</st> 部分的时间 <st c="11386">变量</st> 让我们首先导入库并创建一个 样本数据集:

  1. 让我们导入 <st c="11484">pandas</st> <st c="11495">numpy</st>:

     import numpy as np
    import pandas as pd
    
  2. 让我们首先创建 20 个 日期时间 观察值,从 <st c="11605">2024-05-17</st> 午夜开始,然后以 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. 让我们提取小时分钟部分,并将它们捕获到三个新的列中,然后显示 DataFrame 的前五行:

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

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

图 6.10 – 从时间派生出的三个变量的 DataFrame 的前五行

图 6.10 – 从时间派生出的三个变量的 DataFrame 的前五行

注意

记住,<st c="12845">pandas</st><st c="12854">dt</st> 需要一个 <st c="12865">datetime</st> 对象才能工作。您可以使用 <st c="12964">pandas</st> <st c="12970">to_datetime()</st> 将对象变量的数据类型更改为 <st c="12946">datetime</st>

  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 的前五行

您可以使用 <st c="13592">pandas</st><st c="13601">unique()</st> 方法检查新变量的唯一值,例如,通过执行 <st c="13637">df['hour'].unique()</st>

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

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

    我们在以下输出中看到了 <st c="13869">is_morning</st> 变量:

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

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

有了这个,我们从 <st c="14294">datetime</st> 变量的时间部分 提取了多个特征。 这些特征可用于数据分析 预测建模。

它是如何工作的...

在这个菜谱中,我们创建了捕获时间表示的特征。 首先,我们创建了一个包含 <st c="14519">datetime</st> 变量的样本 DataFrame。 我们使用 <st c="14546">pandas</st> <st c="14552">date_range()</st>创建了一个从任意日期开始,以 1 小时、15 分钟和 10 秒为间隔的 20 个值的范围。 我们使用 <st c="14615">1h15min10s</st> 字符串作为 <st c="14757">freq</st> 参数的频率项,以指示所需的增量。 接下来,我们使用 <st c="14865">pandas</st> <st c="14874">DataFrame()</st>将日期范围转换为 DataFrame。

为了提取不同的时间部分,我们使用 <st c="14932">pandas</st> <st c="14941">dt</st> 来访问 <st c="14958">hour</st>, <st c="14964">minute</st>, 和 <st c="14976">second</st> <st c="14982">时间</st> 属性。 在从 <st c="15027">time</st>中提取 <st c="15017">hour</st> 之后,我们使用它通过 NumPy 的 <st c="15123">where()</st>创建了一个新特征,该特征指示是否为早晨。NumPy 的 <st c="15140">where()</st> 扫描了 <st c="15160">hour</st> 变量;如果其值小于 12 且大于 6,则分配值为 <st c="15253">1</st>;否则,分配值为 <st c="15290">0</st>。通过这些操作,我们在 DataFrame 中添加了几个可用于数据分析和 训练机器学习模型的特征。

还有更多…

我们还可以使用以下 <st c="15514">pandas</st> 属性提取微秒和纳秒:

  • <st c="15532">pandas.Series.dt.microsecond</st>

  • <st c="15561">pandas.Series.dt.nanosecond</st>

有关更多 详细信息请访问 https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#time-date-components

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

我们可以从每个 datetime 变量中提取强大的功能,就像我们在前两个食谱中所做的那样。 我们可以通过组合多个 <st c="15932">datetime</st> 变量来创建额外的特征。 一个常见的例子是通过比较 出生日期 事件日期 来提取事件发生时的 年龄

在这个食谱中,我们将学习如何通过利用 <st c="16181">pandas</st> <st c="16192">feature-engine</st>来捕获两个 <st c="16149">datetime</st> 变量之间的时间。

如何实现...

要继续这个食谱,我们将创建一个包含两个 <st c="16293">datatime</st> 变量的 DataFrame:

  1. 让我们首先导入 <st c="16338">pandas</st><st c="16346">numpy</st>,和 <st c="16353">datetime</st>

     import datetime
    import numpy as np
    import pandas as pd
    
  2. 我们将首先创建两个 <st c="16451">datetime</st> 变量,每个变量包含 20 个值;值从 <st c="16513">2024-05-17</st> 开始,第一个变量的间隔为 <st c="16553">1</st> 小时,第二个变量的间隔为 <st c="16588">1</st> 月。 然后,我们将变量捕获到 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 – 以天数和周数表示的 datetime 变量之间的 时间差

  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 – 以不同时间单位表示的两个 datetime 变量之间时间差的 DataFrame

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

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

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

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

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

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

注意

你的电脑上的 <st c="20191">to_today</st> 变量将与本书中的不同,这是由于写作时的当前日期与 你执行 代码时的日期之间的差异造成的。

这就完成了! 我们现在已经通过比较两个 <st c="20470">datetime</st> 变量创建的新特征丰富了我们的数据集。

它是如何工作的...

在这个菜谱中,我们 捕捉了两个 <st c="20584">datetime</st> 变量之间时间的不同表示。 要继续这个菜谱,我们创建了一个包含两个变量的样本 DataFrame,每个变量从任意日期开始有 20 个日期。 第一个变量以 <st c="20778">1</st> 小时的间隔增加,而第二个变量以 <st c="20838">1</st> 月的间隔增加。 我们使用 <st c="20877">pandas</st> <st c="20886">date_range()</st>创建了这些变量,我们将在本章前两个菜谱中讨论它。

为了确定变量之间的差异——即确定它们之间的时间差——我们直接从另一个 <st c="21089">datetime</st> 变量中减去一个 <st c="21137">pandas</st> Series ——即从一个 <st c="21198">pandas</st> Series 中减去另一个。 两个 <st c="21227">pandas</st> Series 之间的差异返回了一个新的 <st c="21242">pandas</st> Series为了捕捉天数差异,我们使用了 <st c="21285">pandas</st> <st c="21294">dt</st>,后面跟着 <st c="21310">days</st>。要将时间差转换为月份,我们使用了 <st c="21368">timedelta()</st> 从 NumPy,表示我们想要以周为单位传递差异,通过在方法的第二个参数中传递 <st c="21453">W</st> 为了捕捉秒和分钟的差异,我们分别将 <st c="21557">s</st> <st c="21563">m</st> 字符串传递给 <st c="21576">timedelta()</st>

注意

NumPy 的 <st c="21634">timedelta</st> 的参数是一个数字, <st c="21658">– 1</st>,在我们的例子中,表示单位数量,以及一个 <st c="21719">datetime</st> 单位,例如天(<st c="21747">D</st>),周(<st c="21758">W</st>),小时(<st c="21770">h</st>),分钟(<st c="21784">m</st>),或者 秒(<st c="21801">s</st>)。

最后,我们 <st c="21818">从</st> <st c="21846">一个</st> <st c="21851">日期</st> <st c="21855">时间</st> <st c="21860">变量到今天的日期</st> <st c="21887">捕获了差异。</st> <st c="21887">我们通过使用</st> <st c="21960">today()</st> <st c="21967">从内置的</st> <st c="21986">datetime</st> <st c="21994">Python 库</st> `获得了今天的日期和时间(撰写本文时)。

[还有更多...

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

  1. [让我们导入 `pandas` `` `feature-engine` `的转换器:

     import pandas as pd
    from feature_engine.datetime import (
        DatetimeSubtraction
    )
    
  2. [让我们重新创建我们在 步骤 2 <st c="22372">的</st> *<st c="22380">如何做</st> *它… 部分中描述的样本数据集:

     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",
    )
    

[注意

[我们可以通过在`variables` `` `reference` `参数中传递变量列表来获得两个以上变量的差值。

  1. [让我们创建 `然后显示` `新功能:

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

    [在以下输出中,我们看到捕获了两个 日期时间 变量 之间时间差的变量,以天为单位:

![图 6.18 – 包含两个日期时间变量差值的新变量的 DataFrame

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

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

参见

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

[在不同时区处理时间

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

如何操作...

为了继续本食谱,我们将创建一个包含两个不同 时区变量的样本 DataFrame:

  1. 让我们 导入 <st c="24413">pandas</st>

     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)
    

    如果我们现在执行 <st c="25119">df</st>,我们将看到包含不同时区变量的 DataFrame,如下面的输出所示:

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

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

注意

时区用 <st c="25668">+02</st> <st c="25676">-05</st> 值表示,分别表示与协调世界时 UTC的时间差。

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

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

如果我们现在执行 <st c="26033">df</st>,我们将看到新的变量,它们与 UTC 相比有 <st c="26076">00</st> 小时的时间差:

图 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()
    

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

    <st c="27127">0    21</st>
    <st c="27132">1    21</st>
    <st c="27137">2    21</st>
    <st c="27142">0   -40</st>
    <st c="27148">1   -40</st>
    <st c="27231">datetime</st> variables to the <st c="27257">London</st> and <st c="27268">Berlin</st> 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']]

    
    <st c="27503">We see the variables in their</st> <st c="27534">respective time zones in the</st> <st c="27563">following output:</st>
    

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

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

当更改时区时,不仅时区的值会改变——也就是说,之前图像中的<st c="28064">+01</st> <st c="28072">+02</st> 值——而且小时的值也会改变。

它是如何工作的...

在这个菜谱中,我们更改了时区并执行了不同时区变量之间的操作。 首先,我们创建了一个包含两个变量的 DataFrame,这些变量的值从一个任意日期开始,每小时增加;这些值设置在不同的时区。 为了在一个 DataFrame 列中组合不同的时区变量,我们使用<st c="28547">pandas</st> <st c="28556">date_range()</st> 返回的序列,通过利用<st c="28582">pandas</st> <st c="28591">concat()</st>。我们将<st c="28612">axis</st> 参数设置为<st c="28629">0</st> ,以指示我们想要将序列垂直地拼接在一列中。 我们在本章前面的菜谱中广泛介绍了<st c="28733">pandas</st> <st c="28742">date_range()</st> 的参数;请参阅《从日期中提取特征》 和《从时间中提取特征》* 的菜谱以获取更多详细信息。

要将变量的时区重置为中心时区,我们使用了<st c="28994">pandas</st> <st c="29003">to_datetime()</st>,传递<st c="29026">utc=True</st>。最后,我们通过从一个序列减去另一个序列并捕获天数差异来确定变量之间的时间差。 要重新分配不同的时区,我们使用了<st c="29223">pandas</st> <st c="29232">tz_convert</st><st c="29242">()</st>,将新的时区作为参数指定。

另请参阅

了解更多关于 <st c="29321">pandas</st> <st c="29330">to_datetime()</st>, 请访问 https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.to_datetime.html.

要了解更多关于 pandas<st c="29461">’</st> tz_convert()`, 请访问 https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.dt.tz_convert.html.

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

<st c="29640">feature-engine</st> 是一个 Python 库,用于特征工程和选择,非常适合与 <st c="29750">pandas</st> DataFrames 一起使用。 <st c="29773">DatetimeFeatures()</st> 类可以通过使用 <st c="29861">pandas</st> <st c="29870">dt</st> 自动提取日期和时间特征。 <st c="29889">DatetimeFeatures()</st> 允许您 提取以下 特征:

  • 季度

  • 学期

  • 的日

  • 月份的

  • 年份的

  • 周末

  • 月始

  • 月末

  • 季度始

  • 季度尾

  • 年始

  • 年尾

  • 闰年

  • 一个月中的 天数

  • 小时

  • 分钟

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

如何做...

为了 展示 <st c="30288">feature-engine</st>’s 功能,我们将 创建一个包含 <st c="30368">datetime</st> 变量的 `样本 DataFrame:

  1. 让我们从导入 <st c="30404">pandas</st> <st c="30415">DatetimeFeatures()</st>:

     import pandas as pd
    from feature_engine.datetime import DatetimeFeatures
    
  2. 让我们创建一个包含 20 个值 <st c="30524">datetime</st> 变量,从 <st c="30573">2024-05-17</st> 午夜开始,然后以 <st c="30626">1</st> 天为增量。 然后,我们将此变量存储在一个 DataFrame 中:

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

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

注意

<st c="30933">DatetimeFeatures()</st> 自动找到 <st c="30994">datetime</st> 类型的变量,或者当 <st c="31063">变量</st> 参数设置为 <st c="31088">None</st>时可以解析为 <st c="31036">datetime</st> 的变量。或者,您可以传递一个包含您想要提取 <st c="31188">日期</st> 和时间 <st c="31197">特征</st> 的变量名称的列表。

  1. 让我们将 <st c="31226">日期</st> <st c="31235">时间</st> 特征添加到 `数据中:

     dft = dtfs.fit_transform(data)
    

注意

默认情况下, <st c="31310">DatetimeFeatures()</st> 从每个 <st c="31371">datetime</st> 变量中提取以下特征: <st c="31390">月份</st> <st c="31397">年份</st> <st c="31403">星期几</st> <st c="31416">月份中的天数</st> <st c="31430">小时</st> <st c="31436">分钟</st>,以及 <st c="31448">秒</st>。我们可以通过 <st c="31495">features_to_extra</st><st c="31513">ct</st> 参数来修改此行为,就像我们在 步骤 3中做的那样。

  1. 让我们 将新变量的名称捕获到一个列表中:

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

注意

<st c="31656">DatetimeFeatures()</st> 使用原始变量名称(在本例中为 <st c="31747">date</st>)后跟一个下划线和创建的特征类型来命名新变量,例如, <st c="31831">date_day_of_week</st> 包含从 <st c="31896">date</st> 变量中提取的星期几。

如果我们执行 <st c="31925">vars_</st>,我们将看到创建的特征的名称:

<st c="31986">['date_month',</st>
 <st c="32001">'date_quarter',</st>
 <st c="32017">'date_semester',</st>
 <st c="32034">'date_year',</st>
 <st c="32047">'date_week',</st>
 <st c="32060">'date_day_of_week',</st>
 <st c="32080">'date_day_of_month',</st>
 <st c="32101">'date_day_of_year',</st>
 <st c="32121">'date_weekend',</st>
 <st c="32137">'date_month_start',</st>
 <st c="32157">'date_month_end',</st>
 <st c="32175">'date_quarter_start',</st>
 <st c="32197">'date_quarter_end',</st>
 <st c="32217">'date_year_start',</st>
 <st c="32236">'date_year_end',</st>
 <st c="32253">'date_leap_year',</st>
 <st c="32271">'date_days_in_month',</st>
 <st c="32293">'date_hour',</st>
 <st c="32306">'date_minute',</st>
<st c="32396">dft[vars_].head()</st>. We can’t show the resulting DataFrame in the book because it is too big.
			<st c="32487">Note</st>
			<st c="32492">We can create specific features by passing their names to the</st> `<st c="32555">features_to_extract</st>` <st c="32574">parameter.</st>
			<st c="32585">For example, to extract</st> `<st c="32610">week</st>` <st c="32614">and</st> `<st c="32619">year</st>`<st c="32623">, we set the transformer like this:</st> `<st c="32659">dtfs = DatetimeFeatures(features_to_extract=["week", "year"])</st>`<st c="32720">. We can also extract all supported features by setting the</st> `<st c="32780">features_to_extract</st>` <st c="32799">parameter</st> <st c="32810">to</st> `<st c="32813">"all"</st>`<st c="32818">.</st>
			`<st c="32819">DatetimeFe</st><st c="32830">atures()</st>` <st c="32839">can also</st> <st c="32849">create features from variables in different time zones.</st> <st c="32905">Let’s learn how to correctly set up the transformer in</st> <st c="32960">this situation.</st>

				1.  <st c="32975">Let’s create a sample DataFrame with a variable’s values in different</st> <st c="33046">time zones:</st>

    ```

    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

    <st c="33308">If we</st> <st c="33314">execute</st> `<st c="33323">df</st>`<st c="33325">, we</st> <st c="33330">will se</st><st c="33337">e the DataFrame from</st> *<st c="33359">Step 6</st>*<st c="33365">, as shown in the</st> <st c="33383">following output:</st>

			![Figure 6.22 – A DataFrame with a variable’s values in different time zones](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/B22396_06_22.jpg)

			<st c="33562">Figure 6.22 – A DataFrame with a variable’s values in different time zones</st>

				1.  <st c="33636">We’ll</st> <st c="33643">set the transformer to</st> <st c="33666">extract three specific features from this variable after setting it to</st> <st c="33737">the UTC:</st>

    ```

    dfts = DatetimeFeatures(

        features_to_extract=

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

        drop_original=False,

        utc=True,

    )

    ```py

    				2.  <st c="33858">Let’s create the</st> <st c="33876">new features:</st>

    ```

    dft = dfts.fit_transform(df)

    ```py

    `<st c="33918">DatetimeFeatures()</st>` <st c="33937">will set all variables into UTC before deriving the</st> <st c="33989">features.</st> <st c="34000">With</st> `<st c="34005">dft.head()</st>`<st c="34015">, we can see the</st> <st c="34032">resulting DataFrame:</st>

			![Figure 6.23 – A DataFrame with the original and new variables](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/B22396_06_23.jpg)

			<st c="34269">Fig</st><st c="34272">ure 6.23 – A DataFrame with t</st><st c="34302">he original and new variables</st>
			<st c="34332">With that, we’ve</st> <st c="34350">created multiple date</st> <st c="34372">and time-related features in a few lines of code.</st> `<st c="34422">feature-engine</st>` <st c="34436">offers a great alternative to manually creating features per feature with</st> `<st c="34511">pandas</st>`<st c="34517">. In addition,</st> `<st c="34532">DatetimeFeatures()</st>` <st c="34550">can be integrated in</st><st c="34571">to scikit-learn’s</st> `<st c="34590">Pipeline</st>` <st c="34598">and</st> `<st c="34603">GridSearchCV</st>`<st c="34615">, among</st> <st c="34623">other classes.</st>
			<st c="34637">How it works...</st>
			`<st c="34653">DatetimeFeatures()</st>` <st c="34672">extracts several date and time features from</st> `<st c="34718">datetime</st>` <st c="34726">variables automatically by utilizing</st> `<st c="34764">pandas</st>`<st c="34770">’</st> `<st c="34773">dt</st>` <st c="34775">under the hood.</st> <st c="34792">It works with variables whose original data types are</st> `<st c="34846">datetime</st>`<st c="34854">, as well as with object-like and categorical variables, provided that they can be parsed into a</st> `<st c="34951">datetime</st>` <st c="34959">format.</st>
			`<st c="34967">DatetimeFeatures()</st>` <st c="34986">extracts the following features by default:</st> `<st c="35031">month</st>`<st c="35036">,</st> `<st c="35038">year</st>`<st c="35042">,</st> `<st c="35044">day_of_week</st>`<st c="35055">,</st> `<st c="35057">day_of_month</st>`<st c="35069">,</st> `<st c="35071">hour</st>`<st c="35075">,</st> `<st c="35077">minute</st>` <st c="35083">and</st> `<st c="35088">second</st>`<st c="35094">. We can make the transformer return all the features it supports by setting the parameter</st> `<st c="35185">features_to_extract</st>` <st c="35204">to</st> `<st c="35208">all</st>`<st c="35211">. In addition, we can extract a specific subset of features by passing the feature names in a list, as we did in</st> *<st c="35324">Step 7</st>*<st c="35330">.</st>
			`<st c="35331">DatetimeFeatures()</st>` <st c="35350">automatically finds</st> `<st c="35371">datetime</st>` <st c="35379">variables or variables that can be parsed as</st> `<st c="35425">datetime</st>` <st c="35433">in the DataFrame passed to the</st> `<st c="35465">fit()</st>` <st c="35470">method.</st> <st c="35479">To extract features from a selected variable or group of variables, we can pass their name in a list to the</st> `<st c="35587">variables</st>` <st c="35596">parameter when we set up</st> <st c="35622">the transformer.</st>
			<st c="35638">With</st> `<st c="35644">fit()</st>`<st c="35649">,</st> `<st c="35651">Dat</st><st c="35654">etimeFeatures()</st>` <st c="35670">doe</st><st c="35674">s not learn any parameters; instead, it checks that the variables entered by the user are, or can be, parsed into a</st> `<st c="35791">datetime</st>` <st c="35799">format.</st> <st c="35808">If the user does not indicate variable names,</st> `<st c="35854">DatetimeFeatures()</st>` <st c="35872">finds the</st> `<st c="35883">datetime</st>` <st c="35891">variables</st> <st c="35902">automatically.</st> <st c="35917">With</st> `<st c="35922">transform()</st>`<st c="35933">, the</st> <st c="35939">tr</st><st c="35941">ansformer adds the date and time-derived variables to</st> <st c="35996">the DataFrame.</st>

第八章:7

执行特征缩放

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

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

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

本章将涵盖以下内容: 以下食谱:

  • 标准化 特征

  • 缩放到最大值和 最小值

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

  • 执行 均值归一化

  • 实现最大 绝对缩放

  • 缩放到向量 单位长度

技术要求

在本章中,我们使用的主要库是用于缩放的 scikit-learn (<st c="1375">sklearn</st>),用于处理数据的 pandas ,以及 <st c="1430">matplotlib</st> 用于绘图。

标准化特征

标准化 是将变量中心化到 <st c="1542">0</st> 并将方差标准化到 <st c="1578">1</st>的过程。为了标准化特征,我们从每个观测值中减去均值,然后将结果除以 标准差:

xscaled=x−mean(x)std(x)

前面变换的结果称为 z 分数 ,它表示一个给定观测值相对于平均值的偏离程度。

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

准备中

标准化时,变量分布不会改变;改变的是它们值的幅度,正如我们在以下图中看到的: 图 7.1 – 标准化前后正态和偏态变量的分布。

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

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

z 分数(x 轴在底部面板中)表示一个观测值偏离平均值的多少个标准差。 当 z 分数为 <st c="3010">1</st>时,观测值位于平均值右侧 1 个标准差处,而当 z 分数为 <st c="3109">-1</st>时,样本位于平均值左侧 1 个标准差处。

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

注意

均值和标准差对异常值敏感;因此,在使用标准化时,特征可能在存在异常值的情况下以不同的方式缩放。

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

如何做到这一点...

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

  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 中,并删除 <st c="4758">纬度</st><st c="4766">经度</st> 变量:

     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 的 <st c="5086">StandardScaler()</st> 函数,并将其拟合到训练集,以便它学习每个变量的均值和标准差:

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

注意

Scikit-learn 的缩放器,就像任何 scikit-learn 转换器一样,默认返回 NumPy 数组。要返回 <st c="5403">pandas</st><st c="5413">polars</st> 数据帧,我们需要使用 <st c="5481">set_output()</st> 方法指定输出容器,就像我们在 步骤 4 中所做的那样。

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

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

    <st c="5678">StandardScaler()</st><st c="5772">fit()</st> 过程中存储从训练集学习到的均值和标准差。让我们可视化学习到的参数。

  2. 首先,我们将打印 <st c="5875">scaler</st> 学习到的均值:

     scaler.mean_
    

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

    <st c="6133">scaler</st>:
    
    

    scaler.scale_

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

    
    <st c="6331">Let’s compare the transformed data with the original data to understand</st> <st c="6404">the changes.</st>
    
  3. 让我们打印测试集中原始变量的描述性统计信息:

     X_test.describe()
    

    在以下输出中,我们可以看到变量的均值与零不同,并且方差有所变化:

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

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

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

     X_test_scaled.describe()
    

    在下面的输出中,我们看到变量的均值现在集中在 <st c="7469">0</st> ,方差大约为 1`

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

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

注意

The <st c="8206">AveRooms</st>, <st c="8216">AveBedrms</st>, and <st c="8231">AveOccup</st> 变量高度偏斜,这可能导致测试集中的观察值远大于或远小于训练集中的值,因此我们观察到方差偏离了 <st c="8438">1</st>。这是可以预料的,因为标准化对异常值和非常 偏斜的分布敏感。

我们在 “准备” 部分提到,分布的形状不会随着标准化而改变。 继续执行 <st c="8704">X_test.hist()</st> 然后 <st c="8727">X_test_scaled.hist()</st> 来比较转换前后变量的分布。

它是如何工作的...

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

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

注意

<st c="9849">StandardScaler()</st> 默认会减去均值并除以标准差。 如果我们只想将分布中心化而不标准化方差,我们可以在初始化转换器时设置 <st c="10046">with_std=False</st> 如果我们想将方差设置为 <st c="10130">1</st>,而不对分布进行中心化,我们可以在 步骤 4中设置 <st c="10193">with_mean=False</st>

缩放到最大和最小值

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

xsca<mi{l}ed=x−min(x)maxx−min(x)

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

准备就绪

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

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

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

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

注意

缩放到最小值和最大值对异常值敏感。 如果训练集中存在异常值,缩放会将值挤压到其中一个尾部。 相反,如果测试集中存在异常值,变量在缩放后将会显示大于 <st c="12210">1</st> 或小于 <st c="12228">0</st> 的值,具体取决于异常值是在左侧还是 右侧尾部。

如何操作...

在这个配方中,我们将 将加利福尼亚住房数据集的变量缩放到 0 <st c="12420">1</st> 之间 的值:

  1. 让我们首先导入 <st c="12451">pandas</st> 和所需的类 和函数:

     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 中加载 加利福尼亚住房数据集到 <st c="12739">pandas</st> DataFrame 中,删除 <st c="12770">纬度</st> <st c="12783">经度</st> 变量:

     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)
    

注意

<st c="13465">MinMaxScale</st><st c="13477">r()</st> 存储最大值和最小值以及值范围在其 <st c="13548">data_max_</st>, <st c="13559">min_</st>, 和 <st c="13569">data_range_</st> 属性中,分别。

我们可以通过执行 <st c="13687">X_test_scaled.min()</st> 来验证变换变量的最小值,这将返回以下输出:

<st c="13747">MedInc           0.000000</st>
<st c="13763">HouseAge        0.000000</st>
<st c="13781">AveRooms        0.004705</st>
<st c="13799">AveBedrms      0.004941</st>
<st c="13818">Population     0.000140</st>
<st c="13838">AveOccup      -0.000096</st>
<st c="13886">X_test_scaled.max()</st>, we see that the maximum values of the variables are around <st c="13966">1</st>:

MedInc 1.000000

HouseAge 1.000000

AveRooms 1.071197

AveBedrms 0.750090

Population 0.456907

AveOccup 2.074553

dtype: float64


			<st c="14091">Note</st>
			<st c="14096">If you check the maximum values of the variables in the train set after the transformation, you’ll see that they are exactly</st> `<st c="14222">1</st>`<st c="14223">. Yet, in the test set, we see values greater and smaller than</st> `<st c="14286">1</st>`<st c="14287">. This occurs because, in the test set, there are observations with larger or smaller magnitudes than those in the train set.</st> <st c="14413">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).</st> <st c="14564">This behavior is expected because scaling to the minimum and maximum values is sensitive to outliers and very</st> <st c="14674">skewed distributions.</st>
			<st c="14695">Scaling to the</st> <st c="14710">minimum and maximum value does not change the shape</st> <st c="14762">of the variable’s distribution.</st> <st c="14795">You can corroborate that by displaying the histograms before and after</st> <st c="14866">the tr</st><st c="14872">ansformation.</st>
			<st c="14886">How it works...</st>
			<st c="14902">In this rec</st><st c="14914">ipe, we scaled the variables of the California housing dataset to values between</st> `<st c="14996">0</st>` <st c="14997">and</st> `<st c="15001">1</st>`<st c="15002">.</st>
			`<st c="15003">MinMaxScaler()</st>` <st c="15018">from scikit-learn learned the minimum and maximum values and the value range of each variable when we called the</st> `<st c="15132">fit()</st>` <st c="15137">method and stored these parameters in its</st> `<st c="15180">data_max_</st>`<st c="15189">,</st> `<st c="15191">min_</st>`<st c="15195">, and</st> `<st c="15201">data_range_</st>` <st c="15212">attributes.</st> <st c="15225">By using</st> `<st c="15234">transform()</st>`<st c="15245">, we made the scaler remove the minimum value from each variable in the train and test sets and divide the result by the</st> <st c="15366">value range.</st>
			<st c="15378">Note</st>
			`<st c="15383">MinMaxScaler()</st>` <st c="15398">will scale all variables by default.</st> <st c="15436">To scale only a subset of the variables in the dataset, you can use</st> `<st c="15504">ColumnTransformer()</st>` <st c="15523">from scikit-learn or</st> `<st c="15545">SklearnTransformerWrapper()</st>` <st c="15572">from</st> `<st c="15578">Feature-engine</st>`<st c="15592">.</st>
			`<st c="15593">MinMaxScaler()</st>` <st c="15608">will scale the variables between</st> `<st c="15642">0</st>` <st c="15643">and</st> `<st c="15648">1</st>` <st c="15649">by default.</st> <st c="15662">However, we have the option to scale to a different range by adjusting the tuple passed to the</st> `<st c="15757">feature_range</st>` <st c="15770">parameter.</st>
			<st c="15781">By default,</st> `<st c="15794">MinMaxScaler()</st>` <st c="15808">returns</st> <st c="15817">NumPy arrays, but we can modify this</st> <st c="15854">behavior to return</st> `<st c="15873">pandas</st>` <st c="15879">DataFrames with the</st> `<st c="15900">set_output()</st>` <st c="15912">method, as we</st> <st c="15926">did in</st> *<st c="15934">Step 4</st>*<st c="15940">.</st>
			<st c="15941">Scaling with the median and quantiles</st>
			<st c="15979">When</st> <st c="15985">scaling variab</st><st c="15999">les to</st> <st c="16007">the median and quantiles, the median value is removed from the observations, and the result is divided by</st> <st c="16112">the</st> **<st c="16117">Inter-Quarti</st><st c="16129">le Range</st>** <st c="16138">(</st>**<st c="16140">IQR</st>**<st c="16143">).</st> <st c="16147">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</st> <st c="16291">25th percent</st><st c="16303">ile:</st>
			![<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://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/27.png)
			<st c="16310">This method is known</st> <st c="16330">as</st> **<st c="16334">robust</st> <st c="16340">scaling</st>** <st c="16348">because it produces more robust estimates for the center and value range of the variable.</st> <st c="16439">Robust scaling is a suitable alternative to standardization when models require the variables to be centered and the data contains outliers.</st> <st c="16580">It is worth noting that robust scaling will not change the overall shape of the</st> <st c="16660">vari</st><st c="16664">able distribution.</st>
			<st c="16683">How to do it...</st>
			<st c="16699">In this recipe, we will implement scaling with the median and IQR by</st> <st c="16769">utilizing scikit-learn:</st>

				1.  <st c="16792">Let’s start</st> <st c="16805">by importing</st> `<st c="16818">pandas</st>` <st c="16824">and the required scikit-learn classes</st> <st c="16863">and functions:</st>

    ```

    导入 pandas as pd

    从 `sklearn.datasets` 导入 `fetch_california_housing`

    从 `sklearn.model_selection` 导入 `train_test_split`

    从 `sklearn.preprocessing` 导入 `RobustScaler`

    ```py

    				2.  <st c="17051">Let’s load the California housing dataset into a</st> `<st c="17101">pandas</st>` <st c="17107">DataFrame and drop the</st> `<st c="17131">Latitude</st>` <st c="17139">and</st> `<st c="17144">Longitude</st>` <st c="17153">variables:</st>

    ```

    X, y = fetch_california_housing(

        return_X_y=True, as_frame=True)

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

        inplace=True)

    ```py

    				3.  <st c="17294">Let’s divide the data into train and</st> <st c="17332">test sets:</st>

    ```

    X_train, X_test, y_train, y_test = train_test_split(

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

    ```py

    				4.  <st c="17432">Let’s set up scikit-learn’s</st> `<st c="17461">RobustScaler()</st>`<st c="17475">and fit it to the train set so that it learns and stores the median</st> <st c="17544">and IQR:</st>

    ```

    scaler = RobustScaler().set_output(

        transform="pandas")

    scaler.fit(X_train)

    ```py

    				5.  <st c="17628">Finally, let’s</st> <st c="17643">scale the</st> <st c="17654">variables in the train and test sets with the</st> <st c="17700">trained scaler:</st>

    ```

    X_train_scaled = scaler.transform(X_train)

    X_test_scaled = scaler.transform(X_test)

    ```py

    				6.  <st c="17799">Let’s print the variable median values learned</st> <st c="17847">by</st> `<st c="17850">RobustScaler()</st>`<st c="17864">:</st>

    ```

    scaler.center_

    ```py

    <st c="17881">We see the parameters learned by</st> `<st c="17915">RobustScaler()</st>` <st c="17929">in the</st> <st c="17937">following output:</st>

    ```

    <st c="18097">RobustScaler()</st>:

    ```py
     scaler.scale_
    <st c="18189">array([2.16550000e+00, 1.90000000e+01, 1.59537022e+00,</st> <st c="18244">9.41284380e-02, 9.40000000e</st><st c="18272">+02, 8.53176853e-01])</st>
    ```

    <st c="18294">此缩放过程不会改变变量的分布。</st> <st c="18362">继续使用直方图比较变换前后变量的分布</st> <st c="18452">。</st>

    ```py

			<st c="18473">How it works...</st>
			<st c="18489">To scale the</st> <st c="18502">features using</st> <st c="18517">the median and IQR, we created an instance of</st> `<st c="18564">RobustScaler()</st>`<st c="18578">. With</st> `<st c="18585">fit()</st>`<st c="18590">, the scaler learned the median and IQR for each variable from the train set.</st> <st c="18668">With</st> `<st c="18673">transform()</st>`<st c="18684">, the scaler subtracted the median from each variable in the train and test sets and divided the result by</st> <st c="18791">the IQR.</st>
			<st c="18799">After the transformation, the median values of the variables were centered at</st> `<st c="18878">0</st>`<st c="18879">, but the overall shape of the distributions did not change.</st> <st c="18940">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</st> `<st c="19135">X_test.describe()</st>` <st c="19152">and</st> `<st c="19157">X_test_scaled.b()</st>`<st c="19174">.</st>
			<st c="19175">Performing mean normalization</st>
			<st c="19205">I</st><st c="19207">n mean normalization, we</st> <st c="19232">center the variable at</st> `<st c="19255">0</st>` <st c="19256">and rescale the distribution to the value range, so that its values lie between</st> `<st c="19337">-1</st>` <st c="19339">and</st> `<st c="19344">1</st>`<st c="19345">. This procedure involves subtracting the mean from each observation and then dividing the result by the difference between the minimum and maximum values, as</st> <st c="19504">sh</st><st c="19506">own here:</st>
			![<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://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/28.png)
			<st c="19558">Note</st>
			<st c="19562">Mean normalization is an alternative to standardization.</st> <st c="19620">In both cases, the variables are centered at</st> `<st c="19665">0</st>`<st c="19666">. In mean normalization, the variance varies, while the values lie between</st> `<st c="19741">-1</st>` <st c="19743">and</st> `<st c="19748">1</st>`<st c="19749">. On the other hand, in standardization, the variance is set to</st> `<st c="19813">1</st>` <st c="19814">and the value</st> <st c="19829">range varies.</st>
			<st c="19842">Mean</st> <st c="19848">normalization is a suitable alternative for models that need the variables to be centered at zero.</st> <st c="19947">However, it is sensitive to outliers and not a suitable option for sparse data, as it will d</st><st c="20039">estroy the</st> <st c="20051">sparse nature.</st>
			<st c="20065">How to do it...</st>
			<st c="20081">In this recipe, we will implement mean normalization</st> <st c="20135">with</st> `<st c="20140">pandas</st>`<st c="20146">:</st>

				1.  <st c="20148">Let’s import</st> `<st c="20161">pandas</st>` <st c="20167">and the required scikit-learn class</st> <st c="20204">and function:</st>

    ```

    导入 pandas as pd

    从 `sklearn.datasets` 导入 `fetch_california_housing`

    从 `sklearn.model_selection` 导入 `train_test_split`

    ```py

    				2.  <st c="20344">Let’s load the California housing dataset from scikit-learn into a</st> `<st c="20412">pandas</st>` <st c="20418">DataFrame, dropping the</st> `<st c="20443">Latitude</st>` <st c="20451">and</st> `<st c="20456">Longitude</st>` <st c="20465">variables:</st>

    ```

    X, y = fetch_california_housing(

        return_X_y=True, as_frame=True)

    X.drop(labels=[

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

    ```py

    				3.  <st c="20606">Let’s divide the data into train and</st> <st c="20644">test sets:</st>

    ```

    X_train, X_test, y_train, y_test = train_test_split(

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

    ```py

    				4.  <st c="20744">Let’s learn the mean values from the variables in the</st> <st c="20799">train set:</st>

    ```

    means = X_train.mean(axis=0)

    ```py

			<st c="20839">Note</st>
			<st c="20844">We set</st> `<st c="20852">axis=0</st>` <st c="20858">to take the mean of the rows – that is, of the observations in each variable.</st> <st c="20937">If we set</st> `<st c="20947">axis=1</st>` <st c="20953">instead,</st> `<st c="20963">pandas</st>` <st c="20969">will calculate the mean value per observation across all</st> <st c="21027">the columns.</st>
			<st c="21039">By</st> <st c="21043">executing</st> `<st c="21053">print(mean)</st>`<st c="21064">, we display the mean values</st> <st c="21093">per variable:</st>

MedInc 3.866667

HouseAge 28.618702

AveRooms 5.423404

AveBedrms 1.094775

Population 1425.157323

AveOccup 3.040518

dtype: float64


				1.  <st c="21235">Now, let’s determine the difference between the maximum and minimum values</st> <st c="21311">per variable:</st>

    ```

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

    ```py

    <st c="21373">By executing</st> `<st c="21387">print(ranges)</st>`<st c="21400">, we display the value ranges</st> <st c="21430">per variable:</st>

    ```

    <st c="21443">MedInc</st> <st c="21450">14.500200</st>

    <st c="21460">HouseAge         51.000000</st>

    <st c="21479">AveRooms        131.687179</st>

    <st c="21499">平均卧室数        33.733333</st>

    <st c="21519">人口        35679.000000</st>

    <st c="21543">平均占用        598.964286</st>

    <st c="21563">dtype: float64</st>

    ```py

			<st c="21578">Note</st>
			<st c="21583">The</st> `<st c="21588">pandas</st>` `<st c="21594">mean()</st>`<st c="21601">,</st> `<st c="21603">max()</st>`<st c="21608">, and</st> `<st c="21614">min()</st>` <st c="21619">methods return a</st> `<st c="21637">pandas</st>` <st c="21643">series.</st>

				1.  <st c="21651">Now, we’ll</st> <st c="21662">apply mean normalization to the train and test se</st><st c="21712">ts by utilizing the</st> <st c="21733">learned parameters:</st>

    ```

    X_train_scaled = (X_train - means) / ranges

    X_test_scaled = (X_test - means) / ranges

    ```py

			<st c="21838">Note</st>
			<st c="21843">In order to transform future data, you will need to store these parameters, for example, in a</st> `<st c="21938">.txt</st>` <st c="21942">or</st> `<st c="21946">.</st>``<st c="21947">csv</st>` <st c="21951">file.</st>
			*<st c="21957">Step 6</st>* <st c="21964">returns</st> `<st c="21973">pandas</st>` <st c="21979">DataFrames with the transformed train and test sets.</st> <st c="22033">Go ahead and compare the variables before and after the transformations.</st> <st c="22106">You’ll see that the distributions did not change, but the variables are centered at</st> `<st c="22190">0</st>`<st c="22191">, and their v</st><st c="22204">alues lie between</st> `<st c="22223">-1</st>` <st c="22225">and</st> `<st c="22230">1</st>`<st c="22231">.</st>
			<st c="22232">How it works…</st>
			<st c="22246">To implement mean normalization, we captured the mean values of the numerical variables in the train set using</st> `<st c="22358">mean()</st>` <st c="22364">from</st> `<st c="22370">pandas</st>`<st c="22376">. Next, we determined the difference between the maximum and minimum values of the numerical variables in the train set by utilizing</st> `<st c="22509">max()</st>` <st c="22514">and</st> `<st c="22519">min()</st>` <st c="22524">from</st> `<st c="22530">pandas</st>`<st c="22536">. Finally, we used the</st> `<st c="22559">pandas</st>` <st c="22565">series returned by these functions containing the mean values and the value ranges to normalize the train and test sets.</st> <st c="22687">We subtracted the mean from each observation in our train and test sets</st> <st c="22758">and divided the result by the value ranges.</st> <st c="22803">This returned the normalized vari</st><st c="22836">ables in a</st> `<st c="22848">pandas</st>` <st c="22854">DataFrame.</st>
			<st c="22865">There’s more...</st>
			<st c="22881">There is no dedicated scikit-learn transformer to implement mean normalization, but we can combine the use of two transformers to</st> <st c="23012">do so.</st>
			<st c="23018">To do this, we need to import</st> `<st c="23049">pandas</st>` <st c="23055">and load the data, just like we did in</st> *<st c="23095">Steps 1</st>* <st c="23102">to</st> *<st c="23105">3</st>* <st c="23107">in the</st> *<st c="23115">How to do it...</st>* <st c="23130">section of this recipe.</st> <st c="23155">Then, follow</st> <st c="23168">these steps:</st>

				1.  <st c="23180">Import the</st> <st c="23192">scikit-learn transformers:</st>

    ```

    from sklearn.preprocessing import (

        StandardScaler, RobustScaler

    )

    ```py

    				2.  <st c="23285">Let’s set up</st> `<st c="23299">StandardScaler()</st>` <st c="23315">to learn and subtract</st> <st c="23338">the mean without dividing the result by the</st> <st c="23382">standard deviation:</st>

    ```

    scaler_mean = StandardScaler(

        with_mean=True, with_std=False,

    ).set_output(transform="pandas")

    ```py

    				3.  <st c="23496">Now, let’s set up</st> `<st c="23515">RobustScaler()</st>` <st c="23529">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</st> <st c="23670">minimum values:</st>

    ```

    scaler_minmax = RobustScaler(

        with_centering=False,

        with_scaling=True,

        quantile_range=(0, 100)

    ).set_output(transform="pandas")

    ```py

			<st c="23813">Note</st>
			<st c="23818">To divide by the difference between the minimum and maximum values, we need to specify</st> `<st c="23906">(0, 100)</st>` <st c="23914">in the</st> `<st c="23922">quantile_range</st>` <st c="23936">argument</st> <st c="23946">of</st> `<st c="23949">RobustScaler()</st>`<st c="23963">.</st>

				1.  <st c="23964">Let’s fit the scalers to the train set so that they learn and store the mean, maximum, and</st> <st c="24056">minimum values:</st>

    ```

    scaler_mean.fit(X_train)

    scaler_minmax.fit(X_train)

    ```py

    				2.  <st c="24123">Finally, let’s apply mean normalization to the train and</st> <st c="24181">test sets:</st>

    ```

    X_train_scaled = scaler_minmax.transform(

        scaler_mean.transform(X_train)

    ])

    X_test_scaled = scaler_minmax.transform(

        scaler_mean.transform(X_test)

    )

    ```py

			<st c="24339">We transformed the data with</st> `<st c="24369">StandardScaler()</st>` <st c="24385">to remove the mean and then transformed the resulting DataFrame with</st> `<st c="24455">RobustScaler()</st>` <st c="24469">to divide the result by the range between the minimum and maximum values.</st> <st c="24544">We described the functionality of</st> `<st c="24578">StandardScaler()</st>` <st c="24594">in this chapter’s</st> *<st c="24613">Standardizing the features</st>* <st c="24639">recipe and</st> `<st c="24651">RobustScaler()</st>` <st c="24665">in the</st> *<st c="24673">Scaling with the median and quant</st><st c="24706">iles</st>* <st c="24711">recipe of</st> <st c="24722">this chapter.</st>
			<st c="24735">Implementing maximum absol</st><st c="24762">ute scaling</st>
			<st c="24774">Maximum absolute scaling</st> <st c="24800">scales the data to its maximum value – that is, it divides every observation by the maximum value of</st> <st c="24901">the variable:</st>
			![<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://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/29.png)
			<st c="24936">As a result, the maximum value of each feature will be</st> `<st c="24991">1.0</st>`<st c="24994">. Note that maximum absolute scaling does not center the data, and hence, it’s suitable for scaling sparse data.</st> <st c="25107">In this recipe, we will implement maximum absolute scaling</st> <st c="25166">with scikit-learn.</st>
			<st c="25184">Note</st>
			<st c="25189">Scikit-learn recommends using this transformer on data that is cen</st><st c="25256">tered at</st> `<st c="25266">0</st>` <st c="25267">or on</st> <st c="25274">sparse data.</st>
			<st c="25286">Getting ready</st>
			<st c="25300">Maximum absolute scaling</st> <st c="25325">was specifically designed to scale sparse data.</st> <st c="25374">Thus, we will use a bag-of-words dataset that contains sparse variables for the recipe.</st> <st c="25462">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.</st> <st c="25610">Most entries</st> <st c="25623">in the data</st> <st c="25635">are</st> `<st c="25639">0</st>`<st c="25640">.</st>
			<st c="25641">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</st> <st c="25838">4.0 (</st><st c="25843">https://creativecommons.org/licenses/by/4.0/legalcode</st><st c="25897">).</st>
			<st c="25900">I downloaded and prepared a small bag of words representing a simplified version of one of those datasets.</st> <st c="26008">You will find this dataset in the accompanying GitHub</st> <st c="26062">repository:</st> [<st c="26074">https://github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/tree/main/ch07-scaling</st>](https://github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/tree/main/ch07-scaling)<st c="26181">.</st>
			<st c="26182">How to do it...</st>
			<st c="26198">Let’s begin by</st> <st c="26213">importing the required packages and loading</st> <st c="26258">the dataset:</st>

				1.  <st c="26270">Let’s import the required libraries and</st> <st c="26311">the scaler:</st>

    ```

    import matplotlib.pyplot as plt

    import pandas as pd

    from sklearn.preprocessing import MaxAbsScaler

    ```py

    				2.  <st c="26421">Let’s load the</st> <st c="26437">bag-of-words dataset:</st>

    ```

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

    ```py

    <st c="26497">If we execute</st> `<st c="26512">data.head()</st>`<st c="26523">, 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</st> <st c="26667">as values:</st>

			![Figure 7.5 – DataFrame with the bag of words](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/B22396_07_5.jpg)

			<st c="26944">Figure 7.5 – DataFrame with the bag of words</st>
			<st c="26988">Note</st>
			<st c="26993">Although we omit this step in the recipe, remember that the maximum absolute values should be learned from a training dataset only.</st> <st c="27126">Split the dataset into train and test sets when carrying out</st> <st c="27187">your analysis.</st>

				1.  <st c="27201">Let’s set</st> <st c="27211">up</st> `<st c="27215">MaxAbsScaler()</st>` <st c="27229">and fit it to the data so that it learns the variables’</st> <st c="27286">maximum values:</st>

    ```

    scaler = MaxAbsScaler().set_output(

        transform="pandas")

    scaler.fit(data)

    ```py

    				2.  <st c="27374">Now, let’s scale the variables by utilizing the</st> <st c="27423">trained scaler:</st>

    ```

    data_scaled = scaler.transform(data)

    ```py

			<st c="27475">Note</st>
			`<st c="27480">MaxAbsScaler ()</st>` <st c="27496">stores the maximum values in its</st> `<st c="27530">max_ab</st><st c="27536">s_</st>` <st c="27539">attribute.</st>

				1.  <st c="27550">Let’s display the maximum values stored by</st> <st c="27594">the scaler:</st>

    ```

    scaler.max_abs_

    <st c="27715">array([ 7.,  6.,  2.,  2., 11.,  4.,  3.,  6., 52.,  2.])</st>

    ```py

    <st c="27766">To follow up, let’s plot the distributions of the original and</st> <st c="27830">scaled variables.</st>

    				2.  <st c="27847">Let’s make a histogram with the bag of words before</st> <st c="27900">the scaling:</st>

    ```

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

    plt.show()

    ```py

    <st c="27960">In the</st> <st c="27968">following output, we see histograms with the number of times e</st><st c="28030">ach word appears in</st> <st c="28051">a document:</st>

			![Figure 7.6 – Histograms with different word counts](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/B22396_07_6.jpg)

			<st c="28575">Figure 7.6 – Histograms with differen</st><st c="28612">t word counts</st>

				1.  <st c="28626">Now, let’s make a histogram with the</st> <st c="28664">scaled variables:</st>

    ```

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

    plt.show()

    ```py

    <st c="28736">In the following output, we can corroborate the change of scale of the v</st><st c="28809">ariables, but their</st> <st c="28830">distr</st><st c="28835">ibution shape remains</st> <st c="28858">the same:</st>

			![Figure 7.7 – Histograms of the word counts after the scaling](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/B22396_07_7.jpg)

			<st c="29068">Figure 7.7 – Histograms of the word counts after the scaling</st>
			<st c="29128">With scaling to the maximum absolute value, we linearly scale down</st> <st c="29196">the magnitude of</st> <st c="29213">the features.</st>
			<st c="29226">How</st> <st c="29231">it works...</st>
			<st c="29242">In this recipe, we</st> <st c="29261">scaled the sparse variables of a bag of words to their absolute maximum values by using</st> `<st c="29350">MaxAbsScaler()</st>`<st c="29364">. With</st> `<st c="29371">fit()</st>`<st c="29376">, the scaler learned the maximum absolute values for each variable and stored them in its</st> `<st c="29466">max_abs_</st>` <st c="29474">attribute.</st> <st c="29486">With</st> `<st c="29491">transform()</st>`<st c="29502">, the scaler divided the variables by their ab</st><st c="29548">solute maximum values, returning a</st> `<st c="29584">pandas</st>` <st c="29590">DataFrame.</st>
			<st c="29601">Note</st>
			<st c="29606">Remember that you can change the output container to a NumPy array or a</st> `<st c="29679">polars</st>` <st c="29685">DataFrame through the</st> `<st c="29708">set_output()</st>` <st c="29720">method of the scik</st><st c="29739">it-learn</st> <st c="29749">library’s transformers.</st>
			<st c="29772">There’s m</st><st c="29782">ore...</st>
			<st c="29789">If you want to center the variables’ distribution at</st> `<st c="29843">0</st>` <st c="29844">and then scale them to their absolute maximum, you can do so by combining the use of two scikit-learn transformers within</st> <st c="29967">a pipeline:</st>

				1.  <st c="29978">Let’s import the required libraries, transformers,</st> <st c="30030">and functions:</st>

    ```

    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.  <st c="30275">Let’s load the California housing dataset and split it into train and</st> <st c="30346">test sets:</st>

    ```

    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.  <st c="30576">Let’s set up</st> `<st c="30590">StandardScaler()</st>` <st c="30606">from scikit-learn so that it learns and subtracts the mean but does not divide the result by the</st> <st c="30704">standard deviation:</st>

    ```

    scaler_mean = StandardScaler(

        with_mean=True, with_std=False)

    ```py

    				4.  <st c="30785">Now, let’s set up</st> `<st c="30804">MaxAbsScaler()</st>` <st c="30818">with its</st> <st c="30828">default parameters:</st>

    ```

    scaler_maxabs = MaxAbsScaler()

    ```py

    				5.  <st c="30878">Let’s include both scalers within a pipeline that returns</st> <st c="30937">pandas DataFrames:</st>

    ```

    scaler = Pipeline([

        ("scaler_mean", scaler_mean),

        ("scaler_max", scaler_maxabs),

    ]).set_output(transform="pandas")

    ```py

    				6.  <st c="31070">Let’s fit the scalers to the train set so that they learn the</st> <st c="31133">required parameters:</st>

    ```

    scaler.fit(X_train)

    ```py

    				7.  <st c="31173">Finally, let’s transform the train and</st> <st c="31213">test sets:</st>

    ```

    X_train_scaled = scaler.transform(X_train)

    X_test_scaled = scaler.transform(X_test)

    ```py

    <st c="31307">The pipeline applies</st> `<st c="31329">StandardScaler()</st>` <st c="31345">and</st> `<st c="31350">MaxAbsScaler()</st>` <st c="31364">in sequence to first remove the mean and then scale</st> <st c="31417">the resulting</st> <st c="31431">va</st><st c="31433">riables to their</st> <st c="31451">maximum values.</st>

			<st c="31466">Scaling to vector unit length</st>
			<st c="31496">Scaling to the vector unit length involves scaling individual observations (not features) to have a unit norm.</st> <st c="31608">Each sample (that is, each row of the data) is rescaled independently of other samples so that its norm equals one.</st> <st c="31724">Each</st> <st c="31729">row constitutes a</st> **<st c="31747">feature vector</st>** <st c="31761">containing the values of every variable for that row.</st> <st c="31816">Hence, with this scaling method, we rescale the</st> <st c="31864">feature vector.</st>
			<st c="31879">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 (</st>*<st c="32007">l1</st>*<st c="32010">) or the Euclidean (</st>*<st c="32031">l2</st>*<st c="32034">) distance.</st> <st c="32047">The Manhattan distance is given by the sum of the absolute components of</st> <st c="32120">the vector:</st>
			![<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://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/30.png)
			<st c="32163">The Euclidean distance is given by the square root of the square sum of the component of</st> <st c="32252">the vector:</st>
			![<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://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/31.png)
			<st c="32284">Here,</st> ![<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://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/32.png)<st c="32290"><st c="32301">and</st> ![<math xmlns="http://www.w3.org/1998/Math/MathML"><mrow><msub><mi>x</mi><mi>n</mi></msub></mrow></math>](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/33.png)<st c="32305"><st c="32306">are the values of variables</st> *<st c="32334">1</st>*<st c="32335">,</st> *<st c="32337">2</st>*<st c="32338">, and</st> *<st c="32344">n</st>* <st c="32345">for each observation.</st> <st c="32368">Scaling to unit norm consists of dividing each feature vector’s value by either</st> *<st c="32448">l1</st>* <st c="32450">or</st> *<st c="32454">l2</st>*<st c="32456">, so that after the</st> <st c="32475">scaling, the norm of the feature vector is</st> *<st c="32519">1</st>*<st c="32520">. To be clear, we divide each of</st> ![<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://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/34.png)<st c="32553"><st c="32564">and</st> ![<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://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/35.png)<st c="32568"><st c="32569">by</st> *<st c="32572">l1</st>* <st c="32574">or</st> *<st c="32578">l2</st>*<st c="32580">.</st></st></st></st></st>
			<st c="32581">This scaling procedure changes the variables’ distribution, as illustrated in the</st> <st c="32664">following figure:</st>
			![Figure 7.8 – Distribution of a normal and skewed variable before and after scaling each observation’s feature vector to its norm](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/py-feat-engi-cb/img/B22396_07_8.jpg)

			<st c="33144">Figure 7.8 – Distribution of a normal and skewed variable before and after scaling each observation’s feature vector to its norm</st>
			<st c="33272">Note</st>
			<st c="33277">This scaling technique scales each observation and not each variable.</st> <st c="33348">The scaling methods that we discussed so far in this chapter aimed at shifting and resetting the scale of the variables’ distribution.</st> <st c="33483">When we scale to the unit length, however, we normalize each observation individually, contemplating</st> <st c="33584">their values across</st> <st c="33603">all features.</st>
			<st c="33617">Scaling</st> <st c="33626">to the unit norm can be used when utilizing kernels to quantify similarity for text classification and clustering.</st> <st c="33741">In this recipe, we will scale each observation’s feature vector to a un</st><st c="33812">it length of</st> `<st c="33826">1</st>` <st c="33827">using scikit-learn.</st>
			<st c="33846">How to do it...</st>
			<st c="33862">To begin, we’ll</st> <st c="33879">import the required packages, load the dataset, and prepare the train and</st> <st c="33953">test sets:</st>

				1.  <st c="33963">Let’s import the required Python packages, classes,</st> <st c="34016">and functions:</st>

    ```

    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.  <st c="34221">Let’s load the California housing dataset into a</st> `<st c="34271">pandas</st>` <st c="34277">DataFrame:</st>

    ```

    X, y = fetch_california_housing(

        return_X_y=True, as_frame=True)

    X.drop(labels=[

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

    ```py

    				3.  <st c="34418">Let’s divide the data into train and</st> <st c="34456">test sets:</st>

    ```

    X_train, X_test, y_train, y_test = train_test_split(

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

    ```py

    				4.  <st c="34556">Let’s set up the scikit-learn library’s</st> `<st c="34597">Normalizer()</st>` <st c="34609">transformer to scale each observation to the Manhattan distance</st> <st c="34674">or</st> `<st c="34677">l1</st>`<st c="34679">:</st>

    ```

    scaler = Normalizer(norm='l1')

    ```py

			<st c="34712">Note</st>
			<st c="34717">To normalize to the Euclidean distance, you need to set the norm to</st> `<st c="34786">l2</st>` <st c="34788">using</st> `<st c="34795">scaler =</st>` `<st c="34804">Normalizer(no</st><st c="34817">rm='l2')</st>`<st c="34826">.</st>

				1.  <st c="34827">Let’s</st> <st c="34834">transform the train and test sets – that is, we’ll divide each observation’s feature vector by</st> <st c="34929">its norm:</st>

    ```

    X_train_scaled = scaler.fit_transform(X_train)

    X_test_scaled = scaler.transform(X_test)

    ```py

    <st c="35026">We can calculate the length (that is, the Manhattan distance of each observation’s feature vector) using</st> `<st c="35132">linalg()</st>` <st c="35140">from NumPy.</st>

    				2.  <st c="35152">Let’s calculate the norm (Manhattan distance) before scaling</st> <st c="35214">the variables:</st>

    ```

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

    ```py

    <st c="35280">As expected, the norm of each</st> <st c="35311">observation varies:</st>

    ```

    <st c="35330">array([ 255.3,  889.1, 1421.7, ...,  744.6, 1099.5,</st>

    <st c="35380">1048.9])</st>

    ```py

    				3.  <st c="35389">Let’s now calculate the norm after</st> <st c="35425">the scaling:</st>

    ```

    np.round(np.linalg.norm(

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

    ```py

			<st c="35497">Note</st>
			<st c="35502">You need to set</st> `<st c="35519">ord=1</st>` <st c="35524">for the Manhattan distance and</st> `<st c="35556">ord=2</st>` <st c="35561">for the Euclidean distance as arguments of NumPy’s</st> `<st c="35613">linalg()</st>`<st c="35621">function, depending on whether you scaled the features to the</st> `<st c="35684">l1</st>` <st c="35686">or</st> `<st c="35690">l2</st>` <st c="35692">norm.</st>
			<st c="35698">We see that the Manhattan distance of each feature vector is</st> `<st c="35760">1</st>` <st c="35761">after scaling:</st>

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


			<st c="35812">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</st> <st c="35982">the similarity of a pair</st> <st c="36007">of samples.</st>
			<st c="36018">How it wo</st><st c="36028">rks...</st>
			<st c="36035">In this recipe, we</st> <st c="36054">scaled the observations from the California housing dataset to their feature vector unit norm by utilizing the Manhattan or Euclidean distance.</st> <st c="36199">To scale the feature vectors, we created an instance of</st> `<st c="36255">Normalizer()</st>` <st c="36267">from scikit-learn and set the norm to</st> `<st c="36306">l1</st>` <st c="36308">for the Manhattan distance.</st> <st c="36337">For the Euclidean distance, we set the norm to</st> `<st c="36384">l2</st>`<st c="36386">. Then, we applied the</st> `<st c="36409">fit()</st>` <st c="36414">method, although there were no parameters to be learned, as this normalization procedure depends exclusively on the values of the features for each observation.</st> <st c="36576">Finally, with the</st> `<st c="36594">transform()</st>` <st c="36605">method, the scaler divided each observation’s feat</st><st c="36656">ure vector by its norm.</st> <st c="36681">This returned</st> <st c="36695">a NumPy array with the scaled dataset.</st> <st c="36734">After the scaling, we used NumPy’s</st> `<st c="36769">linalg.norm</st>` <st c="36780">function to calculate the norm (</st>`<st c="36813">l1</st>` <st c="36816">and</st> `<st c="36821">l2</st>`<st c="36823">) of each vector to confirm that after the transformation, it</st> <st c="36886">was</st> `<st c="36890">1</st>`<st c="36891">.</st>

第九章:8

创建新特征

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

在某些情况下,一个变量可能与目标没有线性或单调关系,但多项式组合可能会有。 例如,如果我们的变量与目标有二次关系, y=x2,我们可以通过平方原始变量将其转换为线性关系。 我们还可以通过使用样条函数或决策树来转换预测变量,从而帮助线性模型更好地理解变量和目标之间的关系。

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

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

本章将涵盖以下配方: 以下内容:

  • 将特征与 数学函数结合

  • 比较特征与 参考变量

  • 执行 多项式展开

  • 将特征与 决策树结合

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

  • 创建 样条特征

技术要求

在本章中,我们将使用 <st c="2059">pandas</st>, <st c="2067">numpy</st>, <st c="2074">matplotlib</st>, <st c="2086">scikit-learn</st>, 和 <st c="2104">feature-engine</st> Python 库。

使用数学函数组合特征

新特征 可以通过将现有变量与数学和统计函数相结合来创建。 以金融行业为例,我们可以通过将个人金融产品(如汽车贷款、抵押贷款或信用卡债务)的债务总额加起来来计算一个人的总债务: 例如,通过将个人金融产品(如汽车贷款、抵押贷款或信用卡债务)的债务总额加起来来计算一个人的总债务: 总债务 = 汽车贷款债务 + 信用卡债务 + 抵押贷款债务

总债务 = 汽车贷款债务 + 信用卡债务 + 抵押贷款债务

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

最大债务 = max(汽车贷款余额, 信用卡余额, 抵押贷款余额)

网站平均停留时间 = mean(在主页上花费的时间, 在关于页面上花费的时间, 在 常见问题解答页面上花费的时间)

原则上,我们可以使用任何数学或统计运算来创建新特征,例如乘积、平均值、标准差,或最大或最小值。 在本配方中,我们将使用 <st c="3195">pandas</st> <st c="3206">feature-engine</st>来实现这些数学运算。

注意

虽然,在配方中,我们可以向您展示如何使用数学函数组合特征,但我们无法公正地展示在决定应用哪个函数时使用领域知识的用法,因为那会随着每个领域而变化。 因此,我们将把这个 留给您。

准备中

在本配方中,我们将使用来自 <st c="3547">scikit-learn</st>的乳腺癌数据集。这些特征是从乳腺细胞的数字化图像中计算得出的,并描述了它们的细胞核特征,包括平滑度、凹凸度、对称性和紧凑性等。 每一行都包含有关组织样本中细胞核形态的信息。 目标变量表示组织样本是否对应于癌细胞。 目标是根据细胞核形态预测组织样本是否属于良性或恶性的乳腺细胞。

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

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

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

如何操作...

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

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

     import pandas as pd
    from feature_engine.creation import MathFeatures
    from sklearn.datasets import load_breast_cancer
    
  2. 接下来,将乳腺癌数据集加载到一个 <st c="4725">pandas</st> DataFrame: 接下来,将乳腺癌数据集加载到一个 pandas DataFrame:

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

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

  3. Let’s begin by creating a list with the subset of the features that we want to combine: 让我们首先创建一个列表,包含我们想要组合的特征子集:

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

    步骤 3中的特征代表了图像中细胞核的平均特征。 可能需要获取所有 检查特征的平均值。 步骤 3中的特征代表了图像中细胞核的平均特征。 可能需要获取所有检查特征的平均值。 It might be useful to obtain the mean across all examined characteristics.

  4. 让我们计算特征的均值,然后显示 结果特征: 然后显示计算得到的特征均值:

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

    以下输出显示了从 步骤 3中得到的特征的均值: 以下输出显示了从步骤 3中得到的特征的均值:

    <st c="5539">0    0.21702</st>
    <st c="5548">1    0.10033</st>
    <st c="5558">2    0.16034</st>
    <st c="5568">3    0.20654</st>
    <st c="5578">4    0.14326</st>
    <st c="5588">Name: mean_features, dtype: float64</st>
    
  5. 同样,为了捕捉细胞核的一般 可变性,让我们确定平均特征的标准差,然后显示 结果特征: 同样,为了捕捉细胞核的一般可变性,让我们确定平均特征的标准差,然后显示结果特征:

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

    以下输出显示了从 步骤 3中得到的特征的标准差:

    <st c="5949">0    0.080321</st>
    <st c="5960">1    0.045671</st>
    <st c="5971">2    0.042333</st>
    <st c="5982">3    0.078097</st>
    <st c="5993">4    0.044402</st>
    <st c="6004">Name: std_features, dtype: float64</st>
    

注意

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

  1. Let’s make a list containing mathematical functions that we want to use to combine the features: 让我们创建一个包含我们想要用于组合特征的数学函数的列表:

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

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

    如果我们 执行 <st c="6725">df_t.head()</st>,我们将看到包含新</st> <st c="6779">创建的特征的 DataFrame:</st> <st c="6710">如果我们执行df_t.head()`,我们将看到包含新创建的特征的 DataFrame:

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

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

注意

<st c="7135">pandas</st> <st c="7142">agg</st> 可以应用多个函数来组合特性。 它可以接受一个包含函数名称的字符串列表,就像我们在第 7 中做的那样;一个包含 NumPy 函数的列表,例如 <st c="7309">np.log</st>;以及你创建的 Python 函数。

我们可以通过使用 <st c="7440">feature-engine</st>来创建与使用 <st c="7410">pandas</st> 自动创建的相同特性。

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

     new_feature_names = [
        "sum_f", "prod_f", "mean_f",
        „std_f", „max_f", „min_f"]
    
  2. 让我们设置 <st c="7609">MathFeatures()</st> 以将第 6 中的 特性 应用于第 8* 中的字符串,并将新特性命名为第 8 中的字符串:

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

     df_t = create.fit_transform(df)
    

    我们可以通过执行 <st c="8025">df_t[features +</st> <st c="8041">new_feature_name</st><st c="8057">s].head()</st>来显示输入和输出特性

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

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

<st c="8706">pandas</st> <st c="8712">agg</st> 返回一个包含操作结果的特性 DataFrame 时, <st c="8785">feature-engine</st> 会进一步操作,通过将新特性连接到 原始 DataFrame上。

它是如何工作的...

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

使用 pandas <st c="9505">agg()</st> ,我们同时应用了几个数学函数。 它接受一个字符串列表作为参数,对应要应用的功能和应该应用功能的 axis ,可以是 1 (列)或 0 (行)。 因此,pandas <st c="9774">agg()</st> 返回了一个 pandas DataFrame,它是通过将数学函数应用于特征组 而得到的。

最后, 我们 通过将变量与 <st c="9953">特征工程</st>结合 创建了相同的特征。 我们使用了 MathFeatures() 转换器,它接受要组合的特征和要应用的功能作为输入;它还可以选择指示结果的特征的名称。 当我们使用 fit() 时,转换器没有学习参数,而是检查变量确实是数值的。 <st c="10273">transform()</st> 方法触发了在底层使用 <st c="10313">pandas.agg</st> ,应用数学函数来创建新的变量。

另请参阅

要了解更多关于 <st c="10479">pandas</st> 支持的数学运算,请访问 https://pandas.pydata.org/pandas-docs/stable/reference/frame.html#computations-descriptive-stats

[了解更多关于](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.aggregate.html) <st c="10611">pandas</st> <st c="10617">聚合</st> 的信息。

比较特征与参考变量

之前的配方中通过数学函数组合特征,我们通过应用数学 或统计函数,如总和或平均值,到一组变量上,创建了新的特征。 然而,一些数学运算,例如减法或除法,是在 特征之间 执行的。 这些操作对于推导比率,如 债务收入比,很有用:

债务收入比 = 总债务 / 总收入

这些操作 也很有用,可以计算差异,例如 可支配收入

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

在本配方中,我们将学习如何通过使用 <st c="11437">pandas</st> <st c="11448">feature-engine</st>来减法或除法变量创建新的特征。

注意

在配方中,我们将向您展示如何通过减法和除法创建特征。 我们希望这些与金融部门相关的示例能阐明如何使用领域知识来决定哪些特征需要 组合 以及如何组合。

如何做 它…

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

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

     import pandas as pd
    from feature_engine.creation import RelativeFeatures
    from sklearn.datasets import load_breast_cancer
    
  2. 将乳腺癌数据集加载到一个 <st c="12033">pandas</st> DataFrame:

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

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

  3. 让我们捕捉两个特征之间的差异,即 <st c="12566">最差紧密度</st> <st c="12588">平均紧密度</st> 的细胞核,在一个新的变量中,并显示其值: 其值:

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

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

    <st c="12841">0    0.38800</st>
    <st c="12851">1    0.10796</st>
    <st c="12861">2    0.26460</st>
    <st c="12871">3    0.58240</st>
    <st c="12881">4    0.07220</st>
    <st c="12891">Name: difference, dtype: float64</st>
    

注意

我们可以通过执行 <st c="12979">df["difference"] = df["worst compactness"] - (</st>```<st c="13025">df["mean compactness"])</st>来执行相同的计算。

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

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

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

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

    <st c="13472">0    1.410784</st>
    <st c="13483">1    1.214876</st>
    <st c="13494">2    1.197054</st>
    <st c="13505">3    1.305604</st>
    <st c="13516">4    1.110892</st>
    <st c="13527">Name: quotient, dtype: float64</st>
    

注意

我们可以通过执行一个替代命令来计算比率, <st c="13628">df["quotient"] = df["worst radius"] / (</st>```<st c="13667">df["me</st><st c="13674">an radius"])</st>

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

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

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

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

注意

我们可以通过将步骤 5 中的特征除以步骤 6 中的特征之一来创建特征,通过执行 <st c="14250">df[features].div(df["mean radius"])</st>。对于减法,我们将执行 <st c="14317">df[features].sub(df["mean radius"])</st>

  1. 让我们设置 feature-engine 库的 <st c="14396">RelativeFeatures()</st> ,以便它从步骤 5 中的每个特征中减去或除以步骤 6 中的特征:

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

注意

从步骤 5 和步骤 6 中减去特征没有生物学意义,但我们将这样做以演示使用 <st c="14748">RelativeFeatures()</st> 转换器。

  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]
    

注意

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

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

 ['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']

最后,我们可以通过 执行 <st c="16449">df</st><st c="16451">_t[new_features].head()</st>:

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

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

<st c="17039">feature-engine</st> 新特征作为列 添加到原始 DataFrame 的右侧,并自动将这些特征的变量名添加到其中。 通过这样做, <st c="17193">feature-engine</st> 自动化了我们本应使用pandas 完成的许多手动工作。

它是如何工作的...

<st c="17288">pandas</st> 有许多内置操作可以比较一个特征或一组特征与一个参考变量。 在这个配方中,我们使用了 pandas <st c="17425">sub()</st> <st c="17435">div()</st> 来确定两个变量或一组变量与一个 参考特征之间的差异或比率。

为了从一个变量中减去另一个变量,我们对第一个变量应用了<st c="17608">sub()</st> <st c="17613"><st c="17619">pandas</st> <st c="17625">系列,并将第二个变量的<st c="17670">pandas</st> <st c="17676">系列作为参数传递给<st c="17727">sub()</st> <st c="17732">。这个操作返回了一个第三个<st c="17766">pandas</st> <st c="17772">系列,其中包含第一个和第二个变量之间的差值。《st c="17840">要除以另一个变量,我们使用了 <st c="17885">div()</st> <st c="17890">,它的工作方式与<st c="17919">sub()</st> <st c="17924">相同——也就是说,它将左侧的变量除以作为<st c="18013">div()</st> <st c="18018">参数传递的变量。

然后,我们通过利用<st c="18141">ReferenceFeatures()</st> <st c="18160">from</st> Feature-engine<st c="18180">自动将几个变量与两个参考变量通过减法或除法组合起来。<st c="18186">ReferenceFeatures()</st> <st c="18205">transformer接受要组合的变量、参考变量以及用于组合它们的函数。<st c="18321">当使用</st> fit()<st c="18337">时,转换器没有学习参数,而是检查了变量是否为数值型。《st c="18433">执行 <st c="18443">transform()</st> <st c="18454">将新特征添加到<st c="18481">DataFrame</st>中。

备注

<st c="18500">ReferenceFeatures()</st> <st c="18520">还可以对与第二组参考变量相关的一组变量进行加法、乘法、取模或求幂操作。《st c="18653">您可以在其 <st c="18682">文档</st> <st c="18697">[feature-engine.readthedocs.io/en/latest/api_doc/creation/RelativeFeatures.html](https://feature-engine.readthedocs.io/en/latest/api_doc/creation/RelativeFeatures.html) <st c="18783">中了解更多信息。

参见

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

执行多项式展开

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

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

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

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

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

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

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

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

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

准备就绪

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

注意

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

The <st c="22470">PolynomialFeatures()</st> 转换器来自 <st c="22508">scikit-learn</st> ,它自动创建小于或等于用户指定 度数的特征的多项式组合。

为了轻松跟进这个菜谱,让我们首先了解当使用 <st c="22720">PolynomialFeatures()</st> 创建三个变量的二次和三次多项式组合时的输出。

三个变量的二次多项式组合 – a, b, 和 c – 将返回以下 新特征:

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

从前面的特征中, a, b, 和 c 是原始变量; ab, ac, 和 bc 是这些特征的乘积;并且 a2, b2, 和 c2 是原始特征的平方值。 <st c="23166">PolynomialFeatures()</st> 还返回偏置项 1,在创建特征时我们可能会排除它。

注意

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

三个变量的三次多项式组合 – a, b, 和 c – 将返回以下 新特征:

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

在返回的特征中,除了由二次多项式组合返回的特征外,我们现在还有特征与自身进行的三次组合(a3b3c3),每个特征与第二个特征进行线性组合的平方值(a2ba2cb2ab2cc2ac2b),以及三个特征的乘积(abc)。 注意我们如何包含了所有可能的一、二、三次度的交互以及偏差 *1

现在我们理解了由<st c="24275">scikit-lea</st><st c="24285">rn</st>实现的多项式展开的输出,让我们直接进入 步骤。

如何操作...

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

  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. 让我们设置<st c="24809">scikit-learn</st> 库的<st c="24832">set_output</st> API 全局,以便所有转换器在<st c="24919">transform()</st> 方法的结果返回一个 DataFrame:

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

     df = pd.DataFrame(np.linspace(
        0, 10, 11), columns=["var"])
    
  4. 让我们设置 <st c="25126">PolynomialFeatures()</st> 以创建单变量的所有可能的三次多项式组合,并从结果中排除偏差项 – 即,我们将排除 *1

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

     dft = poly.fit_transform(df)
    

    如果我们执行 <st c="25491">dft</st>`,我们将 看到一个包含原始特征、其值的平方以及其值的三次幂的 DataFrame:

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

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

如果我们 不返回一个 DataFrame, <st c="25946">PolynomialFeatures()</st> 返回一个 NumPy 数组,并且你想获取数组中特征的名称,你可以通过执行 <st c="26079">poly.get_feature_names_out()</st>来实现,它返回 <st c="26123">array(['var', 'var²', '</st>``<st c="26147">var³'], dtype=object)</st>

  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)
    

    如果我们执行 <st c="27272">dft</st>,我们将看到多项式展开得到的所有特征,这些特征包含原始特征,以及三个变量的所有可能的组合,但没有二次项,因为我们设置了转换器只返回 特征的交互 作用:

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

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

注意

继续创建特征的立方度多项式组合,只返回交互作用或所有可能的特征,以更好地理解 PolynomialFeatures()`的输出。

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

如果您只想组合特征的一个子集,您可以通过使用 <st c="28508">ColumnTransformer()</st>来选择要组合的特征,正如我们将在本食谱中的更多内容… 部分中展示的那样,或者使用 <st c="28615">SklearnTransformerWrapper()</st> 来自 <st c="28648">feature-engine</st>,正如您可以在随附的 GitHub 存储库中看到的那样: https://github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch08-creation/Recipe3-PolynomialExpansion.ipynb

它是如何工作的...

在这个方法中,我们通过使用特征与其自身或三个变量之间的多项式组合来创建特征。为了创建这些多项式特征,我们使用了 <st c="29044">PolynomialFeatures()</st> 来自 <st c="29070">scikit-learn</st>。默认情况下, <st c="29096">Polynomia</st><st c="29105">lFeatures()</st> 生成一个新的特征矩阵,其中包含数据中所有特征的所有多项式组合,其次数小于或等于用户指定的 <st c="29275">degree</st>。将 <st c="29294">degree</st> 设置为 <st c="29304">3</st>,我们创建了所有可能的三次或更低次数的多项式组合。 为了保留特征与其自身的组合,我们将 <st c="29447">interaction_only</st> 参数设置为 <st c="29477">False</st>。为了避免返回偏差项,我们将 <st c="29529">include_bias</st> 参数设置为 <st c="29555">False</st>

注意

<st c="29579">interaction_only</st> 参数设置为 <st c="29609">True</st> 仅返回交互项——即由两个或更多变量组合而成的变量。

<st c="29727">fit()</st> <st c="29732">method</st>根据指定的参数确定了所有可能的特征组合。在这个阶段,转换器没有执行实际的数学计算。<st c="29917">transform()</st> <st c="29928">method</st>使用特征执行数学计算以创建新的变量。通过<st c="30032">get_feature_names()</st> <st c="30051">method</st>,我们可以识别展开的项 - 即每个新特征是如何计算的。

步骤 2中,我们设置<st c="30171">scikit-learn</st>库的<st c="30183">library’s</st> <st c="30194">set_output</st> <st c="30204">API</st> <st c="30228">pandas</st> <st c="30234">DataFrames</st>作为<st c="30265">transform()</st> <st c="30276">method</st>的结果。<st c="30285">scikit-learn transformers</st>默认返回<st c="30318">NumPy</st> <st c="30323">arrays</st>。新的<st c="30351">set_output</st> <st c="30361">API</st>允许我们将结果的容器更改为<st c="30419">pandas</st> <st c="30425">或</st> <st c="30431">polars</st> <st c="30437">DataFrame</st>。每次设置转换器时,我们都可以单独设置输出 - 例如,通过使用<st c="30543">poly = PolynomialFeatures().set_output(transform="pandas")</st> <st c="30601">。或者,就像在这个食谱中做的那样,我们可以设置全局配置,然后每次设置新的转换器时,<st c="30729">它将返回一个</st> <st c="30747">pandas</st> <st c="30753">DataFrame</st>

更多...

通过对乳腺癌数据集中变量的子集进行多项式展开来创建特征:

  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. <st c="31539">PolynomialFeatures()</st> <st c="31559">设置为创建所有可能的三次方组合:

     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)
    

就这样。

注意

<st c="32039">ColumnTransformer()</st> 将向结果变量追加单词 <st c="32081">poly</st> ,这是我们为 <st c="32159">ColumnTransformer()</st> 中的步骤 步骤 5所赋予的名称。我不是特别喜欢这种行为,因为它使得数据分析变得更加困难,因为你需要跟踪变量名称的变化。 为了避免变量名称的变化,你可以使用 <st c="32366">feature-engine</st> <st c="32383">SklearnTransformerWrapper()</st> 代替。

将特征与决策树结合

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

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

注意

您可以在本文中找到有关此过程和 2009 年 KDD 数据竞赛整体获奖方案的更多详细信息: http://proceedings.mlr.press/v7/niculescu09/niculescu09.pdf

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

如何做到...

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

  1. 让我们首先导入 <st c="34099">pandas</st> 和所需的函数、类、 以及数据集:

     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 <st c="34339">feature_engine.creation</st>, import DecisionTreeFeatures
    
  2. 让我们将加利福尼亚住房数据集加载到一个 <st c="34441">pandas</st> DataFrame 中,并删除 <st c="34473">纬度</st> <st c="34486">经度</st> 变量:

     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}")
    

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

    <st c="35234">corr MedInc vs target: 0.69</st>
    <st c="35262">corr HouseAge vs target: 0.1</st>
    <st c="35291">corr AveRooms vs target: 0.16</st>
    <st c="35321">corr AveBedrms vs target: -0.05</st>
    <st c="35353">corr Population vs target: -0.03</st>
    <st c="35422">feature-engine</st> library’s <st c="35447">DecisionTreeFeatures()</st> selects the best tree by using cross-validation.
    
  5. 创建一个超参数网格以优化每个 决策树:

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

    <st c="35632">feature-engine</st> 库的 <st c="35657">DecisionTreeFeatures()</st> 允许我们添加由一个或多个特征训练的决策树预测产生的特征。 我们可以以多种方式指导转换器组合特征。 我们将从创建两个变量之间所有可能的组合开始。

  6. 制作一个包含我们想要用作输入的 两个特征的列表:

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

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

注意

我们将 <st c="36322">回归</st> 设置为 <st c="36336">True</st> ,因为在这个数据集中目标值是连续的。 如果你有一个二元目标或正在进行分类,请将其设置为 <st c="36463">False</st>。请确保选择一个适合你模型的评估指标(<st c="36512">scoring</st>)。

  1. 调整转换器,使其在输入特征上训练决策树:

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

     dtf.input_features_
    

    以下输出中,我们可以看到 <st c="36824">DecisionTreeFeatures()</st> 已训练了三个决策树 – 两个使用单个特征 <st c="36916">AveRooms</st> <st c="36929">AveBedrms</st>,另一个使用 这两个特征:

    <st c="36971">['AveRooms', 'AveBedrms', ['AveRooms', 'AveBedrms']]</st>
    

注意

<st c="37029">DecisionTreeFeatures()</st> 也存储决策树。 您可以通过执行 <st c="37121">dtf.estimators_</st>来查看它们。

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

     train_t = dtf.transform(X_train)
    test_t = dtf.transform(X_test)
    
  2. 创建一个包含新特征名称的列表(转换器将单词 <st c="37338">tree</st> 添加到 `特征名称):

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

     test_t[tree_features].head()
    

    在以下输出中,我们可以看到由第 8 步中训练的决策树产生的新的前五个特征,步骤 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. 现在,我们需要 将这些元组传递给 <st c="38992">features_to_combine</st> 参数 <st c="39025">DecisionTreeFeatures()</st>

     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. 导入 <st c="40177">Lasso</st> <st c="40191">cross_validate</st> 函数 <st c="40220">scikit-learn</st>

     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-squared:

    <st c="40809">Results: 0.5480403481478856 +/- 0.004214649109293269</st>
    
  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的结果:

    <st c="41447">Results: 0.5800993721099441 +/- 0.002845475651622909</st>
    

我希望我已经向您展示了结合特征与决策树的力量以及如何使用 <st c="41609">feature-engine</st>来做到这一点。

它是如何工作的...

在这个配方中,我们基于在一个或多个变量上训练的决策树的预测创建了新的特征。 我们使用了 <st c="41766">DecisionTreeFeatures()</st> <st c="41793">Feature-engine</st> 自动化训练决策树的过程,包括交叉验证和 超参数优化。

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

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

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

注意

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

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

另请参阅

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

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

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

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

准备

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

图 8.11 – 正弦和余弦函数

图 8.11 – 正弦和余弦函数

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

sinx=sin(2πXXmax)

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

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

如何实现...

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

  1. 开始导入必要的库:

     import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    
  2. 创建一个包含一个变量 – <小时> – 的玩具 DataFrame,其值在 0 和 23 之间:

     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 的 文档: https://scikit-learn.org/stable/auto_examples/applications/plot_cyclical_feature_engineering.html#trigonometric-features

现在我们了解了变换的性质和效果,让我们使用来自多个变量的正弦和余弦变换自动创建新特征。 我们将使用 <st c="49846">feature-engine</st> 库的 <st c="49871">CyclicalFeatures()</st>

  1. 导入 <st c="49898">CyclicalFeatures()</st>:

     from feature_engine.creation import CyclicalFeatures
    
  2. 让我们创建 一个包含 <st c="50019">小时</st>, <st c="50025">月份</st>, 和 <st c="50036">星期</st> 变量的玩具数据集,这些变量的 值分别在 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)
    

    如果我们执行e <st c="50308">df.head()</st>,我们将看到 玩具数据集的前五行:

图 8.16 – 包含三个循环特征的玩具数据集

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

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

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

注意

通过将 <st c="50617">变量</st> 设置为 <st c="50630">None</st> <st c="50636">CyclicalFeatures()</st> 将从所有数值变量创建三角函数特征。 要从变量的子集创建三角函数特征,我们可以将变量的名称列表传递给 <st c="50831">变量</st> 参数。 使用 <st c="50944">drop_original</st> 参数,在创建周期特征后,我们可以保留或删除原始变量。

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

     dft = cyclic.fit_transform(df)
    

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

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

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

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

工作原理...

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

为了自动化多个变量的此过程,我们使用了<st c="52435">Feature-engine</st> 库的<st c="52460">CyclicalFeatures()</st>。使用<st c="52485">fit()</st>,转换器学习了每个变量的最大值,而使用<st c="52562">transform()</st>,它将正弦和余弦变换产生的特征添加到<st c="52621">DataFrame</st>中。

注意

理论上,要应用正弦和余弦变换,我们需要将原始变量缩放到 0 到 1 之间。 除以变量的最大值只会在这种缩放情况下发生,如果最小值是 0。 scikit-learn 的文档和<st c="52916">Feature-engine</st>的当前实现将变量除以其最大值(或任意周期),并没有过多关注变量是否从 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 的 文档: https://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. 创建一个包含 20 个介于-1 和 11 之间的值的训练集 <st c="56637">X</st>,以及目标变量 <st c="56699">y</st>,它是 <st c="56720">X</st> <st c="56724">正弦</st>

     X = np.linspace(-1, 11, 20)
    y = np.sin(X)
    
  3. 绘制 <st c="56798">X</st> <st c="56803">y</st>之间的关系图:

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

    在下面的图中,我们可以看到 <st c="56909">X</st> <st c="56906">正弦</st>函数:

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

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

  1. 使用 Ridge 回归将线性模型拟合到预测 <st c="57106">y</st> 来自 <st c="57113">X</st> ,然后获得模型的预测结果:

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

     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")
    

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

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

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

  1. 现在,设置 <st c="57769">SplineTransformer()</st> 以从 <st c="57824">X</st>中获取样条特征,通过使用三次多项式和五个等距节点:

     spl = SplineTransformer(degree=3, n_knots=5)
    
  2. 获取样条特征并将 NumPy 数组转换为 <st c="58033">pandas</st> DataFrame,添加样条 <st c="58082">基函数</st>的名称:

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

    通过执行 <st c="58226">X_df.head()</st>,我们可以看到 <st c="58254">样条</st>特征:

图 8.22 – 包含样条的 DataFrame

图 8.22 – 包含样条的 DataFrame

注意

<st c="58632">SplineTransformer()</st> 返回一个由 <st c="58692">n_splines = n_knots + degree –</st> <st c="58723">1</st>.

  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. 现在,拟合一个线性模型来预测y,从X获得的样条特征,通过利用岭回归,然后获得模型的预测:

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

     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. <st c="60495">scikit-learn</st>导入一些额外的类和函数:

     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_}")
    

    在以下输出中,我们可以看到模型性能,其中值是 <st c="61334">R</st>-squared:

    <st c="61415">SplineTransformer()</st> 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)

    
    

注意

记住,我们需要使用 <st c="61791">ColumnTransformer()</st> 从数据的一组变量中获取特征。 使用 <st c="61875">remainder=passthrough</st>,我们确保那些不作为样条模板的变量 – 即 <st c="61985">MedInc</st> <st c="61996">HouseAge</st> – 也会在结果 DataFrame 中返回。 要检查此步骤产生的特征,请执行 <st c="62105">ct.get_feature_names_out()</st>

  1. 现在,使用交叉验证将岭回归拟合到基于 <st c="62202">MedInc</st> <st c="62210">HouseAge</st>和样条特征来预测房价,然后获得模型的性能:

     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_}")
    

    在以下输出中,我们可以看到模型性能,其中值是 <st c="62551">R</st>-squared:

    <st c="62565">Model score: 0.5553526813919297 +- 0.02244513992785257</st>
    

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

它是如何工作的…

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

注意

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

使用 <st c="63740">fit()</st>,转换器计算样条的节点。</st> <st c="63798">使用</st> transform()<st c="63814">,它返回 B 样条的数组。 转换器返回 <st c="63875">n_splines=n_knots + degree –</st> <st c="63904">1</st>

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

最后,我们使用了 <st c="64153">ColumnTransformer()</st> 从特征子集中提取样条。 因为我们设置了 <st c="64233">remainder</st> <st c="64246">passthrough</st>ColumnTransformer()<st c="64280">将未用于获取样条的特征连接到结果样条矩阵中。</st> <st c="64379">通过这样做,我们使用样条拟合了 Ridge 回归,加上</st>MedInc<st c="64456">和</st>HouseAge` 变量,并成功提高了线性 模型的性能。

另请参阅

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

第十章:9

使用 Featuretools 从关系数据中提取特征

在前几章中,我们处理的是以行和列组织的数据,其中列是变量,行是观测值,每个观测值都是独立的。 在本章中,我们将专注于从关系型数据集中创建特征。 在关系型数据集中,数据被结构化在不同的表中,这些表可以通过唯一标识符进行连接。 这些唯一标识符表示不同表之间存在的关联。 不同表之间的关联。

关系数据的经典例子是零售公司持有的数据。 一个表包含有关客户的信息,例如姓名和地址。 第二个表包含有关客户购买的信息,例如每次购买购买的商品类型和数量。 第三个表包含有关客户与公司网站互动的信息,例如会话持续时间、使用的移动设备和访问的页面。 客户、购买和会话都使用唯一标识符进行标识。 这些唯一标识符使我们能够将这些表组合起来,这样我们就可以获取有关客户购买或会话的信息。 或会话。

如果我们想了解更多关于我们拥有的客户类型(即客户细分)或者预测他们是否会购买产品,我们可以在客户层面创建汇总或总结不同表信息的特征。 例如,我们可以创建捕捉客户在购买中花费的最大金额、他们购买的商品数量、会话之间的时间或平均会话持续时间的特征。 我们可以创建的特征数量以及我们在表中聚合数据的不同方式是丰富的。 在本章中,我们将讨论一些使用<st c="1887">featuretools</st> <st c="1899">Python 库</st>创建关系数据聚合视图的常见方法。 我们将从设置各种数据表及其关系并自动创建特征开始,然后我们将详细介绍我们可以创建的不同特征。 我们可以创建的不同特征。

在本章中,我们将介绍以下食谱: 以下食谱:

  • 设置实体集并自动创建特征 自动创建特征

  • 使用通用和累积操作创建特征 累积操作创建特征

  • 结合 数值特征

  • 从日期和时间中提取特征 和特征

  • 从文本中提取特征 从文本中提取特征

  • 使用聚合原语创建特征

技术要求

在本章中,我们将使用 <st c="2479">pandas</st>, <st c="2487">matplotlib</st>, 和 <st c="2503">featuretools</st> 开源 Python 库。 您可以使用 <st c="2562">featuretools</st> 通过 <st c="2580">pip</st>进行安装:

 pip install featuretools

此外,您还可以使用 conda来完成此操作:

 conda install -c conda-forge featuretools

这些命令安装了基本的 <st c="2727">featuretools</st> 功能,但我们也可以安装用于使用 <st c="2786">dask</st> 作为后端而不是 <st c="2888">pandas</st>来创建特征的附加组件。有关如何安装 <st c="2935">featuretools</st> 附加组件的更多信息,包括 <st c="2967">graphviz</st>,请参阅他们的文档: https://docs.featuretools.com/en/v0.16.0/getting_started/install.html

我们将 来自 UCI 机器学习仓库的 在线零售 II 数据集进行工作,该数据集可在https://archive.ics.uci.edu/ml/datasets/Online+Retail+II 找到,并受 Creative Commons Attribution 4.0 International (CC BY 4.0)许可协议的约束: https://creativecommons.org/licenses/by/4.0/legalcode。此数据的相应引用如下: 陈,大清 (2019). 在线零售 II. UCI 机器学习 仓库 (https://doi.org/10.24432/C5CG6D).

我下载并修改了如下 笔记本中的数据:https://github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch09-featuretools/prepare-retail-dataset.ipynb

你可以在附带的 GitHub 仓库中找到修改后的数据集: https://github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch09-featuretools/retail.csv

设置实体集和自动创建特征

关系型数据集或数据库包含分散在多个表中的数据,表之间的关系由一个唯一标识符决定,它告诉我们如何连接这些表。 为了使用<st c="4265">featuretools</st>自动创建特征,我们首先需要输入不同的数据表,并在所谓的 <st c="4427">featuretools</st>中建立它们之间的关系,以便库可以自动根据这些关系创建特征。

我们将使用一个包含客户、发票和产品信息的数据集进行工作。 首先,我们将设置一个实体集,突出这三个项目之间的关系。这个实体集将是本章剩余菜谱的起点。 接下来,我们将通过聚合客户、发票和产品层次的数据来自动创建特征,利用来自<st c="4981">featuretools</st>的默认参数。

在本菜谱中,你将学习如何正确设置实体集并为每个实体自动提取一系列特征。 在接下来的菜谱中,我们将深入了解我们可以使用<st c="5232">featuretoo</st><st c="5242">ls</st>创建的不同类型的特征。

准备工作

在本菜谱中,我们将使用来自 UCI 机器学习仓库的 Online Retail II 数据集。 在这个表中,有客户,他们是向零售公司大批量购买的企业。 客户通过一个 <st c="5491">customer_id</st> 唯一标识符来识别。 每个客户会进行一次或多次购买,这些购买通过一个 <st c="5589">invoice</st> 唯一标识符标记,包含发票号码。 在每一张发票中,都有客户购买的一个或多个项目。 公司销售的产品或每个项目也通过一个唯一的 <st c="5805">stock code</st> 来识别。

因此,数据有以下 关系

图 9.1 – 显示数据关系的图

图 9.1 – 显示数据关系的图

每位客户都进行了一笔或多笔购买,这些购买通过发票号来识别。 每张发票包含一个或多个项目,这些项目通过库存代码来识别。 每个项目可以被一个或多个客户购买,因此出现在多张发票中。 考虑到这些关系,让我们继续进行 下一步。

如何操作...

在这个菜谱中,我们将 使用数据设置一个实体集,然后 突出显示数据集中的不同关系。 最后,我们将通过在客户、发票和 产品级别上汇总数据集中的信息来创建特征:

  1. 让我们导入所需的库:

     import pandas as pd
    import featuretools as ft
    from woodwork.logical_types import Categorical
    
  2. 让我们加载在“准备”部分中描述的零售数据集,并显示其前五行:

     df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    df.head()
    

    在下面的屏幕截图中,我们可以看到客户(<st c="6965">customer_id</st>)和发票(<st c="6994">invoice</st>)的唯一标识符,以及关于每张发票中购买的商品的附加信息,例如商品的代码(<st c="7099">stock_code</st>)、描述、数量、单价,以及发票的日期:

图 9.2 – 在线零售 II 数据集

图 9.2 – 在线零售 II 数据集

注意

使用 <st c="7699">pandas</st> <st c="7708">unique()</st> 函数 来识别独特项目、客户和发票的数量 – 例如,通过 执行 <st c="7818">df["customer_id"].nunique()</st>.

  1. 让我们用一个任意的名称初始化一个实体集,例如 如下 <st c="7910">data</st>:

     es = ft.EntitySet(id="data")
    
  2. 让我们向实体集添加一个 DataFrame;我们给 DataFrame 起个名字(<st c="8017">data</st>)。 我们需要为每一行添加一个唯一标识符,我们称之为 <st c="8089">rows</st>,由于在这个数据集中我们没有唯一的行标识符,我们将通过设置 <st c="8214">make_index=True</st>创建它作为额外的列。最后,我们指出 <st c="8257">invoice_date</st> <st c="8280">datetime</st> 类型,并且 <st c="8298">customer_id</st> 应该被处理 <st c="8331">Categorical</st>类型:

     es = es.add_dataframe(
        dataframe=df,
        dataframe_name=»data»,
        index="rows",
        make_index=True,
        time_index=»invoice_date»,
        logical_types={ «customer_id»: Categorical},
    )
    
  3. 接下来,我们添加原始数据 DataFrame <st c="8562">invoices</st>之间的关系。为此,我们指出原始或基础 DataFrame,我们在 步骤 4中称之为 <st c="8664">data</st> ,我们给新的 DataFrame 起个名字, <st c="8714">invoices</st>,我们添加发票的唯一标识符,并将包含 <st c="8800">customer_id</st> 的列添加到这个 DataFrame 中:

     es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»invoices»,
        index="invoice",
        copy_columns=[«customer_id»],
    )
    

注意

我们将 <st c="8980">customer_id</st> 变量复制到 <st c="9008">invoices</st> 数据中,因为我们想创建客户和发票之间的后续关系。

  1. 现在,我们添加第二个关系,即客户和发票之间的关系。 为此,我们指出基础 DataFrame,我们在 步骤 5中称之为 <st c="9241">invoices</st> ,然后我们给新的 DataFrame 起个名字, <st c="9300">customers</st>,并添加唯一的 客户标识符:

     es.normalize_dataframe(
        base_dataframe_name=»invoices»,
        new_dataframe_name=»customers»,
        index=»customer_id», )
    
  2. 我们可以 添加一个第三种关系,在原始数据 产品 之间:

     es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»items»,
        index=»stock_code»,
    )
    
  3. 让我们显示实体集中的信息: 实体集:

     es
    

    在以下输出中,我们看到实体集包含四个 DataFrame:原始数据, <st c="9793">invoices</st> DataFrame, <st c="9817">customers</st> DataFrame,以及产品或 <st c="9857">items</st> DataFrame。 实体还包含发票或项目与原始数据之间的关系,以及客户 和发票 之间的关系:

    <st c="10009">Entityset: data</st>
     <st c="10025">DataFrames:</st>
     <st c="10037">data [Rows: 741301, Columns: 8]</st>
     <st c="10069">invoices [Rows: 40505, Columns: 3]</st>
     <st c="10104">customers [Rows: 5410, Columns: 2]</st>
     <st c="10139">items [Rows: 4631, Columns: 2]</st>
     <st c="10170">Relationships:</st>
     <st c="10185">data.invoice -> invoices.invoice</st>
     <st c="10218">invoices.customer_id -> customers.customer_id</st>
    <st c="10319">invoices</st> DataFrame:
    
    

    es["invoices"].head()

    
    <st c="10361">We see in</st> <st c="10372">the following output that</st> `<st c="10398">featuretools</st>` <st c="10410">au</st><st c="10413">tomatically created a DataFrame containing the invoice’s unique</st> <st c="10478">identifier, followed by the customer’s unique identifier and the first date registered for</st> <st c="10569">ea</st><st c="10571">ch invoice:</st>
    

图 9.3 – 发票级别的信息 DataFrame

图 9.3 – 发票级别的信息 DataFrame

  1. 现在让我们显示 <st c="10913">客户</st> DataFrame:

     es["customers"].head()
    

    在以下输出中,我们可以看到 <st c="10997">featuretools</st> 自动创建了一个包含客户唯一标识符的 DataFrame,随后是此客户的首次发票日期:

图 9.4 – 客户级别的信息 DataFrame

图 9.4 – 客户级别的信息 DataFrame

注意

执行 <st c="11501">es["items"].head()</st> 来显示包含产品的 DataFrame。您还可以使用 <st c="11586">pandas</st> <st c="11595">shape</st> 函数来评估不同 DataFrame 的大小。 您将注意到每个 DataFrame 中的行数与唯一发票、客户和产品的数量相匹配。

  1. 我们还可以以下方式显示这些数据表之间的 关系:

     es.plot()
    

注意

为了可视化数据关系,您需要安装 <st c="11887">graphviz</st> 。如果您还没有安装,请按照 featuretools 文档中的说明进行安装: https://featuretools.alteryx.com/en/stable/install.html#installing-graphviz

在以下输出中,我们可以看到关系数据集及其 关系:

图 9.5 – 包含发票、客户和产品表的表之间的关系

图 9.5 – 包含发票、客户和产品表的表之间的关系

在输入 数据和它们的关系 后,我们可以开始 自动为我们的新 DataFrame 中的每一个创建特征——即客户、发票和产品——使用来自 <st c="13035">featuretools</st> 的默认参数。

  1. 让我们通过聚合客户级别的数据来创建特征。 为此,我们设置了 <st c="13185">featuretools</st> ,将 <st c="13210">客户</st> 指定为目标 DataFrame。 在创建特征时,我们希望忽略具有唯一标识符的两个列:

     feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»customers»,
        ignore_columns={
            «invoices»:[«invoice»],
            «invoices»:[«customer_id»],
        }
    )
    

注意

来自步骤 12 的命令触发了在客户级别对数据进行不同聚合的 114 个特征的创建。<st c="13621">feature_matrix</st> 变量是一个包含特征值的 DataFrame,而<st c="13689">feature_defs</st> 是一个包含新特征名称的列表。请继续执行<st c="13769">feature_defs</st> 或访问我们的配套 GitHub 仓库(https://github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch09-featuretools/Recipe1-Setting-up-an-entitity-set.ipynb)以检查创建的特征名称。您将在它如何工作… 部分中找到更多关于这些特征的信息。

  1. 由于篇幅原因,我们无法在书中打印出所有特征,因此,让我们显示五个创建特征的名称:

     feature_defs[5:10]
    

    在以下输出中,我们看到由<st c="14331">featuretools</st>`创建的 114 个特征中的 5 个

    <st c="14345">[<Feature: MIN(data.price)>,</st>
    <st c="14373"><Feature: MIN(data.quantity)>,</st>
    <st c="14403"><Feature: MODE(data.description)>,</st>
    <st c="14437"><Feature: MODE(data.stock_code)>,</st>
    <st c="14470"><Feature: NUM_UNIQUE(data.description)>]</st>
    

注意

<st c="14521">featuretools</st> 库使用创建它们的函数来命名新特征,然后是用于执行聚合的 DataFrame,最后是聚合变量的名称。因此,<st c="14720">MIN(data.quantity)</st> <st c="14756">df.groupby(["customer_id"])["quantity"].min()</st>等价,如果您熟悉<st c="14828">pandas</st>。我们将在它如何工作… 部分中提供更多详细信息。

  1. 让我们显示包含五个创建特征的DataFrame 的前五行:

     feature_matrix[feature_matrix.columns[5:10]].head()
    

    在以下输出中,我们可以看到包含五个新特征值的五个新特征的前五行:

图 9.6 – 通过在客户级别聚合数据创建的具有五个特征的 DataFrame

图 9.6 – 通过在客户级别聚合数据创建的具有五个特征的 DataFrame

  1. 同样,我们可以通过在发票级别聚合信息来自动创建特征:

     feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»invoices»,
        ignore_columns = {«data»: [«customer_id»]},
        max_depth = 1,
    )
    
  2. 上一步返回了 24 个特征 - 让我们显示它们的名称:

     feature_defs
    

    我们可以在以下输出中看到特征的名称: 如下:

    <st c="16024">[<Feature: customer_id>,</st>
    <st c="16049"><Feature: COUNT(data)>,</st>
    <st c="16072"><Feature: MAX(data.price)>,</st>
    <st c="16099"><Feature: MAX(data.quantity)>,</st>
    <st c="16129"><Feature: MEAN(data.price)>,</st>
    <st c="16157"><Feature: MEAN(data.quantity)>,</st>
    <st c="16188"><Feature: MIN(data.price)>,</st>
    <st c="16215"><Feature: MIN(data.quantity)>,</st>
    <st c="16245"><Feature: MODE(data.description)>,</st>
    <st c="16279"><Feature: MODE(data.stock_code)>,</st>
    <st c="16312"><Feature: NUM_UNIQUE(data.description)>,</st>
    <st c="16352"><Feature: NUM_UNIQUE(data.stock_code)>,</st>
    <st c="16391"><Feature: SKEW(data.price)>,</st>
    <st c="16419"><Feature: SKEW(data.quantity)>,</st>
    <st c="16450"><Feature: STD(data.price)>,</st>
    <st c="16477"><Feature: STD(data.quantity)>,</st>
    <st c="16507"><Feature: SUM(data.price)>,</st>
    <st c="16534"><Feature: SUM(data.quantity)>,</st>
    <st c="16564"><Feature: DAY(first_data_time)>,</st>
    <st c="16596"><Feature: MONTH(first_data_time)>,</st>
    <st c="16630"><Feature: WEEKDAY(first_data_time)>,</st>
    <st c="16666"><Feature: YEAR(first_data_time)>]</st>
    

注意

通过执行<st c="16782">feature_matrix.head()</st> 来显示包含新特征的 DataFrame,或者检查我们附带的 GitHub 仓库以查看结果。

总结一下,通过使用步骤 16 中的代码,并将目标 DataFrame 的名称从<st c="16952">invoices</st> 更改为<st c="16964">items</st>,然后在产品级别自动创建特征。

它是如何工作的...

在这个菜谱中,我们设置了一个包含数据和某些变量(唯一标识符)之间关系的实体集。 之后,我们通过聚合每个唯一标识符的数据集中的信息来自动创建特征。 我们使用了<st c="17344">featuretools</st>中的两个主要类,<st c="17358">EntitySet</st> <st c="17372">dfs</st>,来创建特征。 让我们更详细地讨论这些。

<st c="17449">EntitySet</st> 类存储了数据,变量的逻辑类型以及变量之间的关系。 变量类型(是否为数值或分类)由<st c="17645">featuretools</st>自动分配。我们也可以在将 DataFrame 添加到实体集时设置特定的变量类型。 步骤 4中,我们将数据添加到实体集,并将<st c="17820">customer_id</st> 的逻辑类型设置为<st c="17835">Categorical</st>

注意

要检查<st c="17890">featuretools</st>推断的数据类型,可以执行<st c="17920">es["data"].ww</st>,其中<st c="17941">es</st> 是实体集,<st c="17966">data</st> 是 DataFrame 的名称。

<st c="18000">The</st> <st c="18005">EntitySet</st> <st c="18014">类具有</st> <st c="18029">add_dataframe</st> <st c="18042">方法,我们在</st> *<st c="18068">步骤 4</st>* 中使用了该方法来添加一个新的 DataFrame。 <st c="18099">在使用此方法时,我们需要指定唯一标识符,如果没有,则需要创建一个,就像我们在</st> *<st c="18228">步骤 4</st>* 中做的那样,通过将 <st c="18247">make_index</st> <st c="18257">设置为</st> True 注意index 参数中,从 <st c="18305">add_dataframe</st> <st c="18318">传递了</st> "rows" 字符串。 使用此配置, EntitySet 向 DataFrame 添加了一个 <st c="18392">rows</st> <st c="18396">列,其中包含每行的唯一标识符,这是一个从 0 开始的新整数序列。</st>

<st c="18518">注意</st>

<st c="18523">而不是使用</st> <st c="18545">add_dataframe</st> <st c="18558">方法将 DataFrame 添加到实体集中,我们可以通过执行</st> <st c="18630">es["df_name"]=df</st> <st c="18646">来添加,其中</st> <st c="18654">"df_name"</st> <st c="18663">是我们想要给 DataFrame 的名字,</st> <st c="18713">df</st> <st c="18715">是我们想要</st> <st c="18741">添加的 DataFrame。</st>

<st c="18748">EntitySet</st> <st c="18753">类具有</st> <st c="18777">normalize_dataframe</st> <st c="18796">方法,该方法用于从现有列的唯一值创建一个新的 DataFrame 和关系。</st> <st c="18907">该方法接受新 DataFrame 将关联的 DataFrame 的名称以及</st> <st c="18998">新 DataFrame 的名称。</st> <st c="19028">我们</st> <st c="19030">还需要在</st> <st c="19057">index</st> <st c="19062">参数中指示新 DataFrame 的唯一标识符。</st> <st c="19121">默认情况下,此方法创建一个包含唯一标识符的新 DataFrame,后面跟着一个</st> <st c="19217">datetime</st> <st c="19225">列,包含每个唯一标识符首次注册的日期。</st> <st c="19298">我们可以通过使用</st> <st c="19353">copy_columns</st> <st c="19365">参数来向此 DataFrame 添加更多列,就像我们在</st> 步骤 5 <st c="19397">中做的那样。</st> <st c="19391">添加更多列到新 DataFrame 对于我们想要跟踪与此新 DataFrame 的关系是有用的,就像我们在</st> 步骤 6 <st c="19535">中做的那样。</st>

<st c="19541">实体集</st> 类还具有 <st c="19570">plot()</st> 方法,该方法显示实体集中的现有关系。 *图 9.5**中, <st c="19655">我们看到了我们的数据表之间的关系;</st> 发票<st c="19719">和</st>项目<st c="19729">(产品) 表与原始数据相关,而</st>客户<st c="19804">表与</st>发票` 表相关,该表反过来又与 原始数据 相关。

注意

表之间的关系决定了如何创建特征。 <st c="19980">发票</st> <st c="19993">项目</st> 表与原始数据相关。 因此,我们只能创建深度为 1 的特征。 另一方面, <st c="20092">客户</st> 表与发票相关,而发票与数据相关。 因此,我们可以创建深度为 2 的特征。 这意味着新特征将包括整个数据集的聚合或首先对发票进行聚合,然后对客户进行后续聚合。 我们可以使用 <st c="20445">max_depth</st> 参数 <st c="20468">dfs</st>来调整要创建的特征。

在设置数据和关系之后,我们使用了 <st c="20530">dfs</st> <st c="20539">featuretools</st> 来自动创建特征。 在创建特征时使用 <st c="20614">dfs</st>,我们需要设置目标 DataFrame - 即为创建特征的数据表。 <st c="20727">dfs</st> 类通过 转换 *聚合 现有变量,通过所谓的 转换 聚合 原始操作来创建特征。

一个转换 原始转换变量。 例如,从日期时间变量,使用转换原始操作, <st c="20977">dfs</st> 提取 <st c="20994">月份</st> <st c="21001">年份</st> <st c="21007">日</st>,和 <st c="21016">周</st>

聚合原语 用于对唯一标识符的信息进行聚合。 它使用数学运算,如均值、标准差、最大值和最小值、总和以及偏度系数来处理数值变量。 对于分类变量,聚合原语使用众数和唯一项的计数。 对于唯一标识符,它们计算出现的次数。

考虑到转换和聚合原语的功能,让我们尝试理解在这个菜谱中创建的特性。我们使用了 <st c="21585">dfs</st> 的默认参数来创建 `默认特性

注意

有关 <st c="21679">featuretools</st> 返回的默认特性的更多详细信息,请访问 https://featuretools.alteryx.com/en/stable/generated/featuretools.dfs.html#featuretools.dfs

首先 为每个客户创建了特性。 <st c="21837">featuretools</st> 为每个客户返回了 114 个特性。 由于 <st c="21903">客户</st> 数据与 <st c="21936">发票</st> 数据相关,而发票数据又与整个数据集相关,因此 <st c="21995">特性是通过在两个级别上聚合数据创建的。</st> <st c="22052">首先,使用整个数据集对每个客户的数据进行了聚合。</st> <st c="22127">接下来,首先对每个发票的数据进行了聚合,然后对预聚合的数据再次进行聚合以生成</st> 每个客户的特性

The <st c="22252">featuretools</st> 库使用用于聚合数据的函数来命名新特征 – 例如, <st c="22356">COUNT</st>, <st c="22363">MEAN</st>, <st c="22369">STD</st>, 和 <st c="22378">SKEW</st>, 等等。 接下来,它使用用于聚合的数据,并跟随聚合的变量。 例如, <st c="22526">MEAN(data.quantity)</st> 特征包含从整个数据集中计算出的客户购买物品的平均数量,这相当于 <st c="22676">df.groupby("customer_id"])["quantity"].mean()</st>,如果你熟悉 <st c="22748">pandas</st>。另一方面, <st c="22779">MEAN(invoices.MEAN(data.quantity))</st> 特征首先获取每个发票的物品平均数量 – 即, <st c="22889">df.groupby("invoice"])["quantity"].mean()</st> – 然后从结果序列中获取平均值,考虑特定客户的发票。

对于分类 特征, <st c="23067">featuretools</st> 确定众数和 唯一值。 例如,从 <st c="23145">description</st> 变量中,我们得到了 <st c="23181">NUM_UNIQUE(data.description)</st> <st c="23214">MODE(data.descripti</st><st c="23233">on)</st> 特征。 描述只是物品的名称。 因此,这些特征突出了客户购买的独特物品数量和客户购买次数最多的物品。

注意一些有趣的事情

The <st c="23453">NUM_UNIQUE(data.description)</st> <st c="23486">MODE(data.description)</st> 变量在分类特征聚合后是数值型的。 The <st c="23586">featuretools</st> 库通过使用这些新创建变量的数值聚合来创建更多特征。 In this way, the <st c="23712">MAX(invoices.NUM_UNIQUE(data.description)</st> feature first finds the number of unique items per invoice and then returns the maximum from those values for a particular customer, considering all the customer’s invoices.

从日期时间特征中, <st c="23952">featuretools</st> 默认提取日期组成部分。 请记住, <st c="24020">customers</st> DataFrame 包含了 <st c="24053">customer_id</st> 变量以及每个客户的首次发票日期,正如我们在 步骤 10的输出中看到的。 从这个日期时间特征中, <st c="24191">featuretools</st> 创建了 <st c="24212">DAY(first_invoices_time)</st>, <st c="24238">MONTH(first_invoices_time)</st>, <st c="24266">WEEKDAY(first_invoices_time)</st>, 和 <st c="24300">YEAR(first_invoices_time)</st> 特征,它们包含了不同的 日期部分。

最后, <st c="24381">featuretools</st> 还返回了每个客户的发票总数(<st c="24451">COUNT(invoices)</st>)和每客户的行总数(<st c="24497">COUNT(data)</st>)

另请参阅

要了解更多关于什么启发了 <st c="24575">featuretools</st>的信息,请查看原文 深度特征合成:自动化数据科学努力的途径 由 Kanter 和 Veeramachaneni 所著 https://www.jmaxkanter.com/papers/DSAA_DSM_2015.pdf

使用通用和累积操作创建特征

The <st c="24829">f</st><st c="24830">eaturetools</st> 库使用所谓的 转换原语 来创建特征。 转换原语接受一个或多个数据集的列作为输入,并 in 返回一个或多个列作为输出。 它们应用于一个 *单个 DataFrame。

The <st c="25066">featuretools</st> 库根据它们执行的操作类型或修改的变量类型将它的变换原语分为各种类别。例如, 通用变换原语 应用数学运算,如平方根、正弦和余弦。 累积变换原语 通过比较一行值与上一行值来创建新特征。 例如,累积总和、累积平均值、累积最小值和最大值以及行值之间的差异都属于这一类别。 还有一个可以应用于日期时间变量的累积变换,即 自上次以来 变换,它确定两个连续时间戳之间的时间流逝。

在这个食谱中,我们将使用来自 <st c="25931">featuretools</st>的通用和累积变换原语来创建特征。

准备就绪

变量变换,如平方根或对数变换,在我们想要改变变量的分布时很有用,正如我们在 第三章, 变换数值变量中看到的。其他数学推导,如正弦和余弦,有助于捕捉潜在的数据模式,正如我们在 从周期性变量创建周期特征 食谱 第八章, 创建新特征中描述的。从这些章节中描述的变换中, <st c="26422">featuretools</st> 支持平方根和对数变换以及正弦和余弦(但不包括在 0 和 2π之间的归一化)。

使用累积变换,例如,我们可以通过将发票级别的每一行的项目数量加起来来获取每张发票购买的项目总数。 为了理解我们将在这个食谱中创建的特征,让我们先用 <st c="26824">pandas</st> 创建它们:

  1. 让我们导入 <st c="26851">pandas</st> <st c="26862">numpy</st>:

     import numpy as np
    import pandas as pd
    
  2. 让我们加载在 技术 要求 部分中描述的零售数据集:

     df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    
  3. 让我们 将两个数值变量, <st c="27093">价格</st> <st c="27103">数量</st>,捕获到一个列表中:

     numeric_vars = ["quantity", "price"]
    
  4. 让我们 将累积函数的名称捕获到一个列表中:

     func = ["cumsum", "cummax", "diff"]
    
  5. 让我们创建一个包含我们将要创建的变量的新名称的列表:

     new_names = [f"{var}_{function}"
        for function in func for var in numeric_vars]
    
  6. 让我们使用第 4 步中的累积函数创建新变量,并将其应用到第 3 步中的变量上,然后将它们添加到 DataFrame 中:

     df[new_names] = df.groupby(
        "invoice")[numeric_vars].agg(func)
    

    上一步返回了每个发票内的累积和、累积最大值以及行之间的差异。 一旦遇到新的发票号码,它 就重新开始。

  7. 让我们显示一个特定发票的原始和新特征:

     df[df["invoice"] == "489434" ][
        numeric_vars + new_names]
    

    在以下输出中,我们可以看到 <st c="27980">quantity_cumsum</st> 是数量变量的累积和,而 <st c="28048">price_diff</st> 是行 之后的行价格差异:

图 9.7 – 显示对单个实体(发票)中的数值特征应用累积函数的 DataFrame

图 9.7 – 显示对单个实体(发票)中的数值特征应用累积函数的 DataFrame

现在 将正弦 和余弦变换应用到整个 DataFrame。

  1. 让我们创建一个包含新变量名称的列表:

     new_names = [
        f"{var}_{function}"
        for function in ["sin", "cos"]
        for var in numeric_vars]
    
  2. 让我们使用正弦 和余弦将价格和数量进行转换:

     df[new_names] = df[numeric_vars].agg(
        [np.sin, np.cos])
    

    第 9 步中的转换已应用于整个数据集,无论发票号码如何,这是可以的,因为它将一行映射到同一行,而不是像累积函数那样从一行映射到下一行。 您可以通过执行<st c="29094">df[new_names].head()</st>来检查结果。

现在我们了解了我们想要创建的特征类型,让我们使用<st c="29213">featuretools</st>`来自动化这个过程。

如何做...

我们将 对每个发票应用累积变换 并对整个数据集应用一般变换:

  1. 首先,我们将导入 <st c="29372">pandas</st>, <st c="29380">featuretools</st>, 以及 <st c="29402">Categorical</st> 逻辑类型:

     import pandas as pd
    import featuretools as ft
    from woodwork.logical_types import Categorical
    
  2. 让我们加载在技术 要求 部分 中描述的数据集:

     df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    
  3. 让我们设置一个 实体集:

     es = ft.EntitySet(id="data")
    
  4. 让我们将 DataFrame 添加到 实体集:

     es = es.add_dataframe(
        dataframe=df,
        dataframe_name=»data»,
        index="rows",
        make_index=True,
        time_index=»invoice_date»,
        logical_types={
            "customer_id": Categorical,
            "invoice": Categorical,
        }
    )
    

注意

默认情况下, <st c="29962">featuretools</st> 仅保留在创建新特征后生成的特征矩阵中的分类、数值和布尔特征。 发票 变量的类型无法准确推断,因此我们需要通过将其逻辑类型设置为我们在步骤 4 中所做的,来强制将其作为分类变量,如果我们希望 <st c="30260">featuretools</st> 将其保留在新特征数据集中。 要了解 <st c="30365">featuretools</st>推断的数据类型,您可以执行 <st c="30387">es["data"].</st><st c="30406">ww</st>

  1. 让我们创建一个与步骤 4 的 DataFrame 相关的新 DataFrame:

     es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»invoices»,
        index="invoice",
        copy_columns=[«customer_id»],
    )
    

注意

有关步骤 4 和 步骤 5 的更多详细信息,请访问 设置实体集和自动创建特征 *的食谱。

  1. 让我们列出我们将用于创建特征的累积转换:

     cum_primitives = [
        "cum_sum",
        "cum_max",
        "diff", "time_since_previous"]
    

注意

您可以在以下链接找到 <st c="30924">featuretools</st>支持的累积转换: https://featuretools.alteryx.com/en/stable/api_reference.html#cumulative-transform-primitives

  1. 让我们列出要执行的一般转换:

     general_primitives = ["sine", " cosine "]
    

注意

您可以在以下链接找到 <st c="31207">featuretools</st>支持的一般转换: https://featuretools.alteryx.com/en/stable/api_reference.html#general-transform-primitives

  1. 最后,让我们 创建特征。 我们使用 <st c="31409">dfs</st> 类,将原始 DataFrame 设置为目标 DataFrame – 即,我们将使用其变量作为新特征的模板。 请注意,我们向 <st c="31595">agg_primitives</st> 参数传递一个空列表;这是为了避免返回默认的聚合原语。 我们从 步骤 7 将通用原语传递到 <st c="31734">trans_primitives</st> 参数,并将累积原语从 步骤 6 传递到 <st c="31810">groupby_trans_primitives</st> 参数:

     feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»data»,
        agg_primitives=[],
        trans_primitives=general_primitives,
        groupby_trans_primitives = cum_primitives,
        ignore_dataframes = [«invoices»],
    )
    

注意

步骤 8 触发特征的创建,这可能会花费一些时间,具体取决于数据的大小、聚合级别以及要创建的特征数量。 您可以通过将 *在创建它们之前 检查输出特征,通过设置 <st c="32319">features_only</st> 参数为 <st c="32346">True</st>。这将仅返回特征名称;您可以检查它们,确保它们显示您需要的内容,然后通过将该参数重新设置为 <st c="32527">False</st>来触发特征合成。

  1. 现在让我们 显示 创建的特征 名称:

     feature_defs
    

    在以下输出中,我们可以看到我们创建的特征名称,包括价格和数量的正弦和余弦,以及将这些变量按发票号分组后的累积变换:

    <st c="32828">[<Feature: customer_id>,</st>
    <st c="32853"><Feature: invoice>,</st>
    <st c="32872"><Feature: stock_code>,</st>
    <st c="32894"><Feature: description>,</st>
    <st c="32917"><Feature: quantity>,</st>
    <st c="32937"><Feature: price>,</st>
    <st c="32954"><Feature: COSINE(price)>,</st>
    <st c="32979"><Feature: COSINE(quantity)>,</st>
    <st c="33007"><Feature: SINE(price)>,</st>
    <st c="33030"><Feature: SINE(quantity)>,</st>
    <st c="33056"><Feature: CUM_MAX(price) by invoice>,</st>
    <st c="33093"><Feature: CUM_MAX(quantity) by invoice>,</st>
    <st c="33133"><Feature: CUM_SUM(price) by invoice>,</st>
    <st c="33170"><Feature: CUM_SUM(quantity) by invoice>,</st>
    <st c="33210"><Feature: DIFF(price) by invoice>,</st>
    <st c="33244"><Feature: DIFF(quantity) by invoice>,</st>
    <st c="33281"><Feature: TIME_SINCE_PREVIOUS(invoice_date) by invoice>]</st>
    

注意

价格和数量的正弦和余弦变换可能不会增加太多价值,因为这些不是周期性特征。 我保留了这些变换在配方中,以向您展示如何一般地应用变换原语,如果您需要的话。

如您从 前面的列表中看到,新特征被附加为新的列到原始 DataFrame 中。 您可以通过执行feature_matrix.head() 来显示 最终的 DataFrame:

图 9.8 – 深度特征合成得到的 DataFrame,包含原始变量和新特征

图 9.8 – 深度特征合成得到的 DataFrame,包含 原始变量和新特征

关于创建功能的更多详细信息,请查看 如何工作… *部分。

如何工作…

要使用 <st c="35087">featuretools</st>创建一般和累积转换的功能,我们首先需要设置一个包含数据的实体集并定义其变量之间的关系。 我们在 设置实体集和自动创建功能 *配方 中描述了如何设置实体集。

为了应用累积和一般转换,我们使用了 <st c="35378">dfs</st> <st c="35393">featuretools</st>。一般转换应用于整个 DataFrame,而不按特定变量进行分组。 为了执行一般转换,我们将包含转换名称的字符串列表传递给 <st c="35610">trans_primitives</st> 参数 from <st c="35643">dfs</st>

我们在按 <st c="35703">invoice</st>分组后应用累积转换。为此,我们将包含累积转换名称的字符串列表传递给 <st c="35807">groupby_trans_primitives</st> 参数<st c="35847">dfs</st>featuretools 库知道应该按发票分组,因为我们通过使用 <st c="35969">normalize_dataframe</st> 方法从 <st c="36001">EntitySet</st> 步骤 5中建立了这个唯一标识符。

最后,我们不想从 <st c="36090">invoices</st> DataFrame 中的变量创建功能;因此,我们将 <st c="36123">dfs</st> 设置为通过设置 <st c="36163">ignore_dataframes = ["</st>``<st c="36185">invoices"]</st>

<st c="36197">dfs</st> 类返回了两个变量,包含原始和新的 DataFrame 以及特征名称的列表。 新特征以创建它们的操作命名,例如 <st c="36405">SINE</st>, <st c="36411">COSINE</st>, <st c="36419">CUM_MAX</st>, 或 <st c="36431">DIFF</st>,后跟应用转换的变量,以及当对应时,用于分组的变量。

请注意 <st c="36578">featuretools</st> 会自动识别并选择应用转换的变量。 <st c="36692">正弦、余弦、累计总和、最大值和差值被应用于数值变量,而</st> time_since_previous<st c="36818">转换被应用于</st>datetime 变量`。

结合数值特征

在第 第八章创建新特征,我们了解到我们可以通过结合 变量和数学运算来创建新特征。 <st c="37035">featuretools</st> 库支持多种组合变量的操作,包括加法、除法、取模和乘法。 在本例中,我们将学习如何使用 <st c="37233">featuretools</st>来组合这些特征。

如何操作...

让我们首先导入库并准备数据集:

  1. 首先,我们将导入 <st c="37353">pandas</st><st c="37361">featuretools</st>,以及 <st c="37383">Categorical</st> 逻辑类型:

     import pandas as pd
    import featuretools as ft
    from woodwork.logical_types import Categorical
    
  2. 让我们加载在 *技术 * *要求 * 部分中描述的数据集:

     df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    
  3. 让我们设置一个 <st c="37657">实体集</st>

     es = ft.EntitySet(id="data")
    
  4. 让我们将 DataFrame 添加到 实体集:

     es = es.add_dataframe(
        dataframe=df,
        dataframe_name=»data»,
        index="rows",
        make_index=True,
        time_index=»invoice_date»,
        logical_types={«customer_id»: Categorical},
    )
    
  5. 让我们创建一个新的 DataFrame,它与从 步骤 4中的 DataFrame 相关:

     es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»invoices»,
        index="invoice",
        copy_columns=[«customer_id»],
    )
    

注意

有关 *步骤 4 * 步骤 5的更多详细信息,请访问 *设置实体集和自动创建特征 * *食谱 *。

  1. 我们将乘以 <st c="38260">数量</st> <st c="38273">价格</st> 变量,分别反映购买的商品数量和单价,以获得总 支付金额:

     feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»data»,
        agg_primitives=[],
        trans_primitives=[«multiply_numeric»],
        primitive_options={
            («multiply_numeric»): {
                ‹include_columns›: {
                    'data': ["quantity", "price"]
                }
            }
        },
        ignore_dataframes=[«invoices»],
    )
    

注意

我们将 <st c="38687">agg_primitives</st> 设置为空列表,以避免创建 <st c="38744">默认原语</st>

  1. 现在 让我们显示新特征的名称:

     feature_defs
    

    在以下输出中,我们看到特征名称,其中最后一个对应于 <st c="38936">价格</st> <st c="38946">数量</st> 变量的组合:

    <st c="38965">[<Feature: customer_id>,</st>
    <st c="38990"><Feature: stock_code>,</st>
    <st c="39012"><Feature: description>,</st>
    <st c="39035"><Feature: quantity>,</st>
    <st c="39055"><Feature: price>,</st>
    <st c="39072"><Feature: price * quantity>]</st>
    
  2. 最后,让我们检查在 步骤 6中创建的新 DataFrame:

     feature_matrix.head()
    

    在以下输出中,我们可以看到新特征被附加到原始 DataFrame 的右侧:

图 9.9 – 由价格与数量的乘积产生的新特征的 DataFrame

图 9.9 – 由价格与数量的乘积产生的新特征的 DataFrame

<st c="39776">featuretools</st> 结合特征可能比 <st c="39833">df["price"].mul(df["quantity"])</st> pandas 函数更复杂。 真正的力量在于我们以这种方式创建新特征并随后在发票或客户级别进行聚合时。 我们将在 使用聚合原始操作创建特征 食谱 中讨论聚合函数。

接下来...

要乘积特征,我们使用了来自 <st c="40201">featuretools</st> <st c="40170">MultiplyNumeric</st> 原始操作,它可以通过 <st c="40242">dfs</st> 使用 <st c="40256">multiply_numeric</st> 字符串访问。 我们将前面的字符串传递给 <st c="40316">trans_primitive</st> 参数,然后使用 <st c="40360">primitive_options</st> 参数指定要乘积的变量。 请注意,此外,我们还向 <st c="40482">agg_primitives</st> 参数传递了一个空列表,以避免返回默认的聚合原始操作,并且我们忽略了来自 <st c="40606">invoices</st> DataFrame 的特征。

要查看其他允许您组合变量的函数,请访问 https://featuretools.alteryx.com/en/stable/api_reference.html#binary-transform-primitives。在撰写本文时,我发现 <st c="40828">MultiplyNumeric</st> <st c="40848">DivideNumeric</st> 未在文档中列出。 您可以通过检查源代码始终双重检查哪些函数受支持: https://github.com/alteryx/featuretools/tree/main/featuretools/primitives/standard/transform/binary。您还可以通过在设置实体集及其关系后运行以下命令来检查您可以在数据上执行哪些操作: <st c="41239">ft.get_valid_primitives(es, target_dataframe_name="data", max_dep</st><st c="41304">th=2)</st>。在这里,es 是第 步骤 5产生的实体集。

从日期和时间中提取特征

在第 第六章从日期和时间中提取特征 变量,我们讨论了如何通过从日期和时间变量(如年份、月份、星期几、小时等)中提取特征来丰富我们的 数据集。我们可以利用 <st c="41713">featuretools</st> 自动提取这些特征。

featuretools 支持使用其 日期时间转换原语创建来自日期时间变量的各种特征。这些原语包括常见的变量,如年份、月份和日期,以及其他特征,如 是否是午餐时间 是否是工作日。此外,我们可以提取表示日期是否为联邦假日或银行假日(如英国所称)的特征,或者确定到特定日期的时间距离的特征。 对于一家零售公司来说,圣诞节、黑色星期五或节礼日等日期的邻近性通常预示着销售额的增加,如果他们正在预测需求,这些将是有用的变量。

注意

有关从 datetime 变量创建的特征的更多详细信息,请访问 https://featuretools.alteryx.com/en/stable/api_reference.html#datetime-transform-primitives

在这个配方中,我们将使用 <st c="42656">featuretools</st>自动从 datetime 变量创建多个特征:

如何做到这一点...

让我们开始 通过导入库并准备 数据集:

  1. 首先,我们将导入 <st c="42776">pandas</st><st c="42784">featuretools</st>,以及一些特殊的 datetime 原始设置:

     import pandas as pd
    import featuretools as ft
    from featuretools.primitives import (
        IsFederalHoliday, DistanceToHoliday)
    from woodwork.logical_types import Categorical
    
  2. 让我们加载 技术 要求 部分中描述的数据集:

     df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    
  3. 让我们设置一个实体集:

     es = ft.EntitySet(id="data")
    
  4. 让我们将 DataFrame 添加到 实体集:

     es = es.add_dataframe(
        dataframe=df,
        dataframe_name=»data»,
        index="rows",
        make_index=True,
        time_index=»invoice_date»,
        logical_types={«customer_id»: Categorical},
    )
    
  5. 让我们创建一个新的 DataFrame,它与 步骤 4中的 DataFrame 有关系:

     es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»invoices»,
        index="invoice",
        copy_columns=[«customer_id»],
    )
    

注意

有关 步骤 4 步骤 5的更多详细信息,请访问 设置实体集和自动创建特征 *的配方。

  1. 让我们创建一个 返回一个布尔向量,指示日期是否与英国的银行假日(即非工作日)相符的原始设置:

     is_bank_hol = IsFederalHoliday(country="UK")
    

注意

在设置用于确定银行假日的原始设置时,选择正确的国家非常重要。 有关支持国家的列表, 请访问 https://github.com/dr-prodigy/python-holidays#available-countries

  1. 让我们检查这个原始设置中包含哪些银行假日:

     hols = is_bank_hol.holidayUtil.federal_holidays.values()
    available_hols = list(set(hols))
    

    如果我们 执行 <st c="44311">available_hols</st>`,我们将看到支持英国银行假日的列表:

    <st c="44382">['May Day',</st>
     <st c="44394">'Good Friday',</st>
     <st c="44409">'Wedding of William and Catherine',</st>
     <st c="44445">'Coronation of Charles III',</st>
     <st c="44474">'Christmas Day',</st>
     <st c="44491">'Wedding of Charles and Diana',</st>
     <st c="44523">'Christmas Day (observed)',</st>
     <st c="44551">'State Funeral of Queen Elizabeth II',</st>
     <st c="44590">'Silver Jubilee of Elizabeth II',</st>
     <st c="44624">'Spring Bank Holiday',</st>
     <st c="44647">'Diamond Jubilee of Elizabeth II',</st>
     <st c="44682">'Boxing Day (observed)',</st>
     <st c="44707">'Platinum Jubilee of Elizabeth II',</st>
     <st c="44743">"New Year's Day (observed)",</st>
     <st c="44772">'Boxing Day',</st>
     <st c="44786">'Golden Jubilee of Elizabeth II',</st>
     <st c="44820">'Millennium Celebrations',</st>
     <st c="44847">"New Year's Day"]</st>
    
  2. 让我们创建另一个原始设置,用于确定到特定日期的天数——在这种情况下,到节礼日的距离:

     days_to_boxing = DistanceToHoliday(
        holiday="Boxing Day", country="UK")
    
  3. 现在,让我们创建一个包含字符串的列表,这些字符串可以识别我们从 <st c="45149">datetime</st> 和包括从 步骤 6 步骤 8中获取的原始设置:

     date_primitives = [
        "day", "year", "month", "weekday",
        "days_in_month", "part_of_day",
        "hour", "minute",
        is_bank_hol,
        days_to_boxing
    ]
    
  4. 现在让我们 第 9 步 创建基于 <st c="45406">invoice_date</st> 日期变量的日期和时间特征:

     feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»invoices»,
        agg_primitives=[],
        trans_primitives=date_primitives,
    )
    

注意

在第 4 步中,我们输入了 <st c="45607">invoice_date</st> 变量作为时间变量。 因此, <st c="45655">featuretools</st> 将使用此变量来创建日期和 时间相关的特征。

  1. 让我们显示创建的 特征名称:

     feature_defs
    

    在以下输出中,我们看到原始特征和 时间特征的名称:

    <st c="45872">[<Feature: customer_id>,</st>
    <st c="45897"><Feature: DAY(first_data_time)>,</st>
    <st c="45929"><Feature: DAYS_IN_MONTH(first_data_time)>,</st>
    <st c="45971"><Feature: DISTANCE_TO_HOLIDAY(</st>
     <st c="46002">first_data_time, holiday=Boxing Day, country=UK)>,</st>
    <st c="46053"><Feature: HOUR(first_data_time)>,</st>
    <st c="46086"><Feature: IS_FEDERAL_HOLIDAY(</st>
     <st c="46116">first_data_time, , country=UK)>,</st>
    <st c="46149"><Feature: MINUTE(first_data_time)>,</st>
    <st c="46184"><Feature: MONTH(first_data_time)>,</st>
    <st c="46218"><Feature: PART_OF_DAY(first_data_time)>,</st>
    <st c="46258"><Feature: WEEKDAY(first_data_time)>,</st>
    <st c="46350">feature_matrix.head()</st> to take a look at the resulting DataFrame with the features created from the invoice date. The DataFrame is quite big, so for reasons of space, we’ll only display a few columns in the book.
    
  2. 让我们显示包含三个新特征的 结果 DataFrame:

     columns = [
        "DISTANCE_TO_HOLIDAY(first_data_time,
            holiday=Boxing Day, country=UK)",
        "HOUR(first_data_time)",
        "IS_FEDERAL_HOLIDAY(first_data_time,
            country=UK)",
    ]
    feature_matrix[columns].head()
    

    在以下输出中,我们看到包含新特征的 DataFrame:

图 9.10 – 包含从 datetime 派生的一些特征的 DataFrame

图 9.10 – 包含从 datetime 派生的一些特征的 DataFrame

请注意, 一些创建的特征是数值型的,例如 <st c="47281">HOUR</st> <st c="47289">DAY</st>,一些是布尔型的,例如 <st c="47321">IS_FEDERAL_HOLIDAY</st>,还有一些是分类型的,例如 <st c="47375">PART_OF_DAY</st>。要查看 <st c="47420">PART_OF_DAY</st>的值, <st c="47441">feature_matrix["PAR</st><st c="47460">T_OF_DAY(first_data_time)"].unique()</st>

它是如何工作的...

要从 datetime 变量创建特征,我们使用了来自 <st c="47602">featuretools</st> (https://featuretools.alteryx.com/en/stable/api_reference.html#datetime-transform-primitives)的 datetime 转换原语。 这些原语可以通过 <st c="47749">dfs</st> 使用我们在 第 6 步 第 9 步 中指定的字符串和函数通过 <st c="47826">trans_primitive</st> 参数访问。 注意,此外,我们还向 <st c="47907">agg_primitives</st> 参数传递了一个空列表,以避免返回应用于我们的 datetime 特征的默认聚合原语。 我们还忽略了来自 <st c="48091">invoices</st> DataFrame 的特征。

注意

我们将agg_primitives设置为一个空列表,并忽略了invoices DataFrame,以保持输出简单并能够专注于日期时间特征。然而,请注意,featuretools的真正威力在于从datetim中创建原始数据,然后在不同实体级别进一步聚合它们。

从文本中提取特征

在第十一章中,从文本变量中提取特征,我们将讨论我们可以从文本片段中利用pandasscikit-learn提取的各种特征。我们还可以通过利用featuretools自动从文本中提取多个特征。

<st c="48704">featuretools</st>库支持创建几个基本特征作为其默认功能的一部分,例如文本中的字符数、单词数、每个单词的平均字符数以及文本中的中位词长等。

注意

要获取默认文本原始数据的完整列表,请访问featuretools.alteryx.com/en/stable/api_reference.html#naturallanguage-transform-primitives

此外,还有一个配套的 Python 库nlp_primitives,它包含额外的原始数据,用于创建基于 NLP 的更高级特征。在这些函数中,我们发现了一些用于确定多样性得分、极性得分或停用词计数的原始数据。

注意

在编写本文时,没有关于nlp_primitives库支持的原始数据的文档,因此要了解更多信息,您需要检查源代码:github.com/alteryx/nlp_primitives/tree/6243ef2379501bfec2c3f19e35a30b5954605e57/nlp_primitives

在这个食谱中,我们将首先利用<st c="49788">featuretools</st>的默认功能以及n突出显示如何使用来自<st c="49877">nlp_primitives</st> 库的 <st c="49857">p</st>原语。

准备就绪

为了跟随这个食谱,您需要安装<st c="49973">nlp_primitives</st> 库,您可以使用<st c="50014">pip</st>来完成此操作:

 pip install nlp_primitives

否则,您可以使用 conda`:

 conda install -c conda-forge nlp-primitives

注意

有关更多详细信息,请访问<st c="50160">nlp_primi</st><st c="50169">tives</st> GitHub 存储库: https://github.com/alteryx/nlp_primitives

如何操作...

让我们首先导入库并准备数据集:

  1. 首先,我们将导入 <st c="50343">pandas</st><st c="50351">featuretools</st>,以及 逻辑类型:

     import pandas as pd
    import featuretools as ft
    from woodwork.logical_types import (
       Categorical, NaturalLanguage)
    
  2. 让我们加载在<st c="50541">技术</st> *要求 * 部分中描述的数据集:

     df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    
  3. 让我们设置一个 实体集:

     es = ft.EntitySet(id="data")
    
  4. 让我们将 DataFrame 添加到实体集中,突出显示 <st c="50757">描述</st> 变量是一个 文本变量:

     es = es.add_dataframe(
        dataframe=df,
        dataframe_name=»data»,
        index="rows",
        make_index=True,
        time_index=»invoice_date»,
        logical_types={
            «customer_id»: Categorical,
            "invoice": Categorical,
            «description»: NaturalLanguage,
        }
    )
    

注意

为了使<st c="51033">featuretools</st> 库的文本原语正常工作,我们需要使用<st c="51139">NaturalLanguage</st> 逻辑类型来指示哪些变量是文本。

  1. 让我们创建 一个新的 DataFrame,它与<st c="51240">步骤 4</st>中的 DataFrame 有关系:

     es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»invoices»,
        index="invoice",
        copy_columns=[«customer_id»],
    )
    

注意

有关 *步骤 4 * 步骤 5的更多详细信息,请访问 *设置实体集并创建特征 * *自动 * 食谱。

  1. 让我们创建一个与我们要创建的文本特征对应的字符串列表:

     text_primitives = [
        "num_words",
        "num_characters",
        "MeanCharactersPerWord" ,
        "PunctuationCount"]
    
  2. 现在让我们 <st c="51730">描述</st> 变量中提取文本特征:

     feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»data»,
        agg_primitives=[],
        trans_primitives=text_primitives,
        ignore_dataframes=[«invoices»],
    )
    
  3. 让我们显示已创建的特征的名称:

     feature_defs
    

    在以下输出中,我们看到原始特征的名称,然后是来自 <st c="52087">description</st> 变量的特征:

    <st c="52108">[<Feature: customer_id>,</st>
    <st c="52133"><Feature: invoice>,</st>
    <st c="52152"><Feature: stock_code>,</st>
    <st c="52174"><Feature: quantity>,</st>
    <st c="52194"><Feature: price>,</st>
    <st c="52211"><Feature: MEAN_CHARACTERS_PER_WORD(description)>,</st>
    <st c="52260"><Feature: NUM_CHARACTERS(description)>,</st>
    <st c="52299"><Feature: NUM_WORDS(description)>,</st>
    <st c="52422">feature_matrix.head()</st>.
    
  4. 让我们显示包含从文本派生特征的 DataFrame 的一个片段:

    <st c="52521">text_f = [</st>
     <st c="52532">"NUM_CHARACTERS(description)",</st>
     <st c="52563">"NUM_WORDS(description)",</st>
     <st c="52589">"PUNCTUATION_COUNT(description)",</st>
    <st c="52623">]</st>
    <st c="52625">feature_matrix[text_f].head()</st>
    

    在以下输出中,我们看到一个从文本中创建的特征 DataFrame:

图 9.11 – 从文本创建的特征 DataFrame

图 9.11 – 从文本创建的特征 DataFrame

注意

<st c="52943">featuretools</st> 库移除了原始文本变量 <st c="53000">description</st>,并返回了 新的特征。

要使用来自 <st c="53109">nlp_primitives</st> 包的原始函数创建特征,您首先需要导入它们 – 例如,通过执行 <st c="53196">from nlp_primitives import DiversityScore</st> – 然后将原始函数添加到我们在 步骤 6中创建的文本原始函数列表中。请注意,这些是 复杂函数,因此创建特征可能需要一些时间。

它是如何工作的...

要从文本变量创建特征,我们使用了来自 <st c="53504">featuretools</st>的默认文本原始函数。这些原始函数可以通过传递一个包含与原始函数名称对应的字符串列表到 <st c="53663">trans_primitives</st> 参数中从 <st c="53556">dfs</st> 访问,例如来自 步骤 6的,到 <st c="53663">trans_primitives</st> 参数。

对于更高级的原始函数,您需要从 <st c="53773">nlp_primitives</st> 库中导入原始函数,然后将它们传递给来自 <st c="53857">dfs</st> <st c="53825">trans_primitives</st> 参数。有了这个, <st c="53873">dfs</st> 就可以利用这些原始函数的功能从文本中创建新的特征。 <st c="53966">nlp_primitives</st> 库在底层使用 <st c="54002">nltk</st> Python 库。

使用聚合原始函数创建特征

在本章中,我们通过将现有变量映射到 新特征中,通过各种函数自动创建了特征。 例如,我们从日期时间变量中提取日期和时间部分,计算文本中的单词、 字符和标点符号的数量,将数值特征组合成新变量,并使用正弦和余弦等函数转换特征。 为了创建这些特征,我们使用了 转换原语。

<st c="54540">featuretools</st> 库还 支持 <st c="54773">价格</st>,与发票相关,聚合原语将取单个发票的所有价格观测值并返回单个值,例如平均价格或总和(即支付的总金额),对于 该发票。

注意

<st c="55009">featuretools</st> 的聚合功能相当于 <st c="55069">groupby</st> <st c="55080">pandas</st>中,随后是 <st c="55100">pandas</st> functions,如 <st c="55126">mean</st> <st c="55132">sum</st> <st c="55137">std</st>,和 <st c="55146">count</st>,以及其他函数。

一些聚合原语与数值变量一起工作,例如平均值、总和或最大值 和最小值。 其他聚合原语特定于分类变量,例如唯一值的数量和最频繁的 值(众数)。

注意

要获取 支持的聚合原语的完整列表, 请访问 https://featuretools.alteryx.com/en/stable/api_reference.html#aggregation-primitives

在本菜谱中,我们将首先通过聚合现有变量来创建多个特征。 之后,我们将结合使用转换和聚合原语,以突出<st c="55772">featuretools</st>的真正威力。

准备就绪

在这个菜谱中,我们将使用来自 UCI 机器学习仓库的 在线零售 II 数据集。 这个数据集包含有关产品(项目)、发票和客户的信息。 为了跟随这个菜谱,了解这些实体的性质和它们之间的关系,以及如何使用 <st c="56147">featuretools</st>正确设置实体集非常重要,我们在 设置实体集和自动创建特征 菜谱中描述了这一点。 在继续下一节之前,请确保您已经检查了该菜谱。

如何做到...

让我们首先导入库并准备好 数据集:

  1. 首先,我们将导入 <st c="56442">pandas</st> <st c="56450">featuretools</st>,以及逻辑类型:

     import pandas as pd
    import featuretools as ft
    from woodwork.logical_types import (
        Categorical, NaturalLanguage)
    
  2. 让我们加载 技术 要求 部分中描述的数据集:

     df = pd.read_csv(
        «retail.csv», parse_dates=[«invoice_date»])
    
  3. 让我们设置一个 实体集:

     es = ft.EntitySet(id="data")
    
  4. 让我们将 DataFrame 添加到实体集中,并强调以下内容: <st c="56856">描述</st> 变量是一个文本变量, <st c="56897">customer_id</st> 是分类变量, <st c="56929">invoice_date</st> 是一个 日期时间特征:

     es = es.add_dataframe(
        dataframe=df,
        dataframe_name=»data»,
        index="rows",
        make_index=True,
        time_index=»invoice_date»,
        logical_types={
            «customer_id»: Categorical,
            «description»: NaturalLanguage,
        }
    )
    
  5. 让我们创建一个新的 DataFrame,它与来自 步骤 4的 DataFrame 有关系:

     es.normalize_dataframe(
        base_dataframe_name=»data»,
        new_dataframe_name=»invoices»,
        index="invoice",
        copy_columns=[«customer_id»],
    )
    
  6. 现在,我们添加第二个关系,即客户与发票之间的关系。 为此,我们指示基础 DataFrame,我们在 步骤 5中将其称为 <st c="57514">invoices</st> ,我们给新的 DataFrame 起名, <st c="57568">customers</st>,并添加一个唯一的 客户标识符:

     es.normalize_dataframe(
        base_dataframe_name=»invoices»,
        new_dataframe_name=»customers»,
        index=»customer_id»,
    )
    

注意

有关 步骤 4 5的更多详细信息,请访问 设置实体集和自动创建特征 自动 菜谱。

  1. 让我们创建一个包含字符串名称的列表,这些名称标识了我们想要使用的聚合原语: 要使用:

     agg_primitives = ["mean", "max", "min", "sum"]
    
  2. 让我们通过在客户级别聚合数据来创建特征。 为此,我们设置了 <st c="58087">dfs</st> 类从 <st c="58102">featuretools</st>,表示 <st c="58127">customers</st> 作为目标 DataFrame,并将来自 步骤 7 的聚合原语传递给 <st c="58237">trans_primitives</st> 参数,以防止 <st c="58275">dfs</st> 返回默认转换:

     feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»customers»,
        agg_primitives=agg_primitives,
        trans_primitives=[],
    )
    
  3. 让我们显示 创建的特征的 名称:

     feature_defs
    

    在以下输出中,我们看到在 客户级别 聚合的名称特征:

    <st c="58621">[<Feature: MAX(data.price)>,</st>
    <st c="58650"><Feature: MAX(data.quantity)>,</st>
    <st c="58680"><Feature: MEAN(data.price)>,</st>
    <st c="58708"><Feature: MEAN(data.quantity)>,</st>
    <st c="58739"><Feature: MIN(data.price)>,</st>
    <st c="58766"><Feature: MIN(data.quantity)>,</st>
    <st c="58796"><Feature: SUM(data.price)>,</st>
    <st c="58823"><Feature: SUM(data.quantity)>,</st>
    <st c="58853"><Feature: MAX(invoices.MEAN(data.price))>,</st>
    <st c="58895"><Feature: MAX(invoices.MEAN(data.quantity))>,</st>
    <st c="58940"><Feature: MAX(invoices.MIN(data.price))>,</st>
    <st c="58981"><Feature: MAX(invoices.MIN(data.quantity))>,</st>
    <st c="59025"><Feature: MAX(invoices.SUM(data.price))>,</st>
    <st c="59066"><Feature: MAX(invoices.SUM(data.quantity))>,</st>
    <st c="59110"><Feature: MEAN(invoices.MAX(data.price))>,</st>
    <st c="59152"><Feature: MEAN(invoices.MAX(data.quantity))>,</st>
    <st c="59197"><Feature: MEAN(invoices.MEAN(data.price))>,</st>
    <st c="59240"><Feature: MEAN(invoices.MEAN(data.quantity))>,</st>
    <st c="59286"><Feature: MEAN(invoices.MIN(data.price))>,</st>
    <st c="59328"><Feature: MEAN(invoices.MIN(data.quantity))>,</st>
    <st c="59373"><Feature: MEAN(invoices.SUM(data.price))>,</st>
    <st c="59415"><Feature: MEAN(invoices.SUM(data.quantity))>,</st>
    <st c="59460"><Feature: MIN(invoices.MAX(data.price))>,</st>
    <st c="59501"><Feature: MIN(invoices.MAX(data.quantity))>,</st>
    <st c="59545"><Feature: MIN(invoices.MEAN(data.price))>,</st>
    <st c="59587"><Feature: MIN(invoices.MEAN(data.quantity))>,</st>
    <st c="59632"><Feature: MIN(invoices.SUM(data.price))>,</st>
    <st c="59673"><Feature: MIN(invoices.SUM(data.quantity))>,</st>
    <st c="59717"><Feature: SUM(invoices.MAX(data.price))>,</st>
    <st c="59758"><Feature: SUM(invoices.MAX(data.quantity))>,</st>
    <st c="59802"><Feature: SUM(invoices.MEAN(data.price))>,</st>
    <st c="59844"><Feature: SUM(invoices.MEAN(data.quantity))>,</st>
    <st c="59889"><Feature: SUM(invoices.MIN(data.price))>,</st>
    <st c="59930"><Feature: SUM(invoices.MIN(data.quantity))>]</st>
    

注意

请记住, <st c="59995">featuretools</st> 使用创建它们的函数命名特征,然后是用于计算的 DataFrame,最后是用于计算的变量。 因此, <st c="60187">MAX(data.price)</st> 是每个客户在数据集中看到的最高价格。 另一方面, <st c="60282">MEAN(invoices.MAX(data.price))</st> 是特定客户在每张发票中观察到的所有最大价格的均值。 也就是说,如果一个客户有六张发票,我们首先找到六张发票中的每张发票的最高价格,然后取这些值的平均值。

  1. 现在让我们 显示包含原始数据和 新特征的 结果 DataFrame:

     feature_matrix.head()
    

    在以下输出中,我们看到 一些 <st c="60742">dfs</st> 返回的 DataFrame 中的变量:

图 9.12 – 包含客户级别聚合的一些特征的 DataFrame

图 9.12 – 包含客户级别聚合的一些特征的 DataFrame

由于空间限制,我们无法显示 步骤 10 的全部输出,所以请确保您在自己的计算机上执行它或访问我们的配套 GitHub 仓库以获取更多 详细信息: https://github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch09-featuretools/Recipe6-Creating-features-with-aggregation-primitives.ipynb

为了跟进,让我们将使用转换原语从菜谱中学到的知识与这个菜谱中的聚合 函数结合起来。 首先,我们将从现有的日期时间和文本变量中创建新特征;然后,我们将在 客户级别对这些特征以及数值变量进行聚合。

  1. 让我们使用日期和 文本原语来创建列表:

     trans_primitives = ["month", "weekday", "num_words"]
    
  2. 让我们使用聚合原语创建一个列表:

     agg_primitives = ["mean"]
    
  3. 现在让我们通过转换然后 自动创建特征,并聚合变量:

     feature_matrix, feature_defs = ft.dfs(
        entityset=es,
        target_dataframe_name=»customers»,
        agg_primitives=agg_primitives,
        trans_primitives=trans_primitives,
        max_depth=3,
    )
    

    步骤 13 触发的代码会创建功能,并在 客户级别进行后续聚合。

  4. 让我们显示新功能的 名称:

     feature_defs
    

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

    <st c="62965">[<Feature: MEAN(data.price)>,</st>
    <st c="62995"><Feature: MEAN(data.quantity)>,</st>
    <st c="63026"><Feature: MONTH(first_invoices_time)>,</st>
    <st c="63064"><Feature: WEEKDAY(first_invoices_time)>,</st>
    <st c="63104"><Feature: MEAN(invoices.MEAN(data.price))>,</st>
    <st c="63147"><Feature: MEAN(invoices.MEAN(data.quantity))>,</st>
    <st c="63193"><Feature: MEAN(data.NUM_WORDS(description))>,</st>
    <st c="63238"><Feature: MEAN(invoi</st><st c="63259">ces.MEAN(data.NUM_</st>
     <st c="63278">WORDS(description)))>] WORDS(description)))>]</st>
    

请注意,由于空间限制,在我们的菜谱中我们将特征的创建保持到最小,但你可以创建尽可能多的特征,并使用 内置 <st c="63544">featuretools</st>的功能显著丰富你的数据集。

它的工作原理...

在这个菜谱中,我们将使用转换原语创建功能的方法,这是我们 在本章中讨论的,与使用 聚合原语创建功能的方法结合起来。

要使用 <st c="63790">featuretools</st> 自动创建特征,我们首先需要将数据输入到实体集中,并建立数据之间的关系。 我们在 设置实体集和自动创建特征 菜谱 中讨论了如何设置实体集。

为了聚合现有的特征,我们使用了 <st c="64080">dfs</st> 类。 我们创建了一个包含对应聚合原语的字符串的列表,并将其传递给 <st c="64188">agg_primitives</st> 参数,来自 <st c="64218">dfs</st>。为了在不创建新特征的情况下聚合现有变量,我们将一个空列表传递给 <st c="64317">trans_primitives</st> 参数,来自 <st c="64347">dfs</st>

<st c="64356">customers</st> DataFrame<st c="64396">invoice</st> DataFrame 的子集,而 <st c="64396">invoice</st> DataFrame 又是原始数据的子集。 因此, <st c="64472">dfs</st> 从原始数据和每个发票的预聚合数据中创建了聚合。 因此, <st c="64576">MEAN(data.price)</st> 特征由客户购买的商品的总数据的均值组成,而 <st c="64702">MEAN(invoices.MEAN(data.price))</st> 首先计算每张发票的平均价格,然后取这些值的均值。 因此,如果一个客户有五张发票, <st c="64873">featuretools</st> 首先计算每张发票支付的平均价格,然后取这些值的均值。 因此, <st c="65000">MEAN(data.price)</st> <st c="65021">MEAN(invoices.MEAN(data.price))</st> 不是同一个特征。

注意

聚合原语对唯一标识符的信息进行聚合。 聚合原语使用数学运算,如均值、标准差、最大值和最小值、总和以及偏度系数来处理数值变量。 对于分类变量,聚合原语使用众数和唯一项的数量。 对于唯一标识符,聚合原语计算出现的次数。

接下来,我们将 从日期和文本变量中创建新特征与聚合相结合。 为此,我们将对应于转换原语的一组字符串传递给 <st c="65683">trans_primitives</st> 参数,并将对应于聚合原语的一组字符串传递给 <st c="65790">agg_primitives</st> 参数 <st c="65818">dfs</st>

步骤 13 的输出之一 是一个新特征列表。 从这些特征中,我们可以识别出由每个客户的首次发票日期创建的特征,例如 <st c="65984">MONTH(first_invoices_time)</st> <st c="66015">WEEKDAY(first_invoices_time)</st>。我们还可以看到从文本特征创建的聚合特征,例如 <st c="66132">MEAN(data.NUM_WORDS(description))</st> <st c="66170">MEAN(invoices.MEAN(data.NUM_WORDS(description)))</st>。最后,我们还可以看到现有数值变量的聚合,例如 <st c="66299">MEAN(data.price)</st> <st c="66320">MEAN(invoices.MEAN(data.price))</st>

注意

如果您想将转换和聚合原语应用于特定变量,您可以通过指定原语选项来实现,具体讨论如下 此处: https://docs.featuretools.com/en/stable/guides/specifying_primitive_options.html

第十一章:10

使用 tsfresh 从时间序列创建特征

在这本书的整个过程中,我们讨论了针对表格和关系数据集定制的特征工程方法和工具。 在本章中,我们将把我们的重点转向时间序列数据。 时间序列是在时间上按顺序进行观察的一系列数据。 例如,包括能源生产和需求、温度、空气污染物浓度、股价和销售收入。 这些例子代表了一个变量,并且它们的值随时间变化。

可负担的传感器的大量可用性,能够测量运动、湿度、葡萄糖和其他参数,显著增加了时间标注数据的数量。 这些时间序列可以用于各种分类任务。 例如,通过分析在特定时间间隔内家庭的电力使用模式,我们可以推断出是否使用了特定的电器。 同样,超声波传感器的信号可以帮助确定(气体)管道故障的概率,而声音波长的特征可以帮助预测听众是否会喜欢一首歌。 时间序列数据对于回归任务也非常有价值。 例如,机械设备传感器的信号可以用来预测设备的剩余使用寿命。

为了使用时间序列与传统的监督机器学习模型,如线性回归、逻辑回归或基于决策树的算法,我们需要将每个时间序列映射到一个定义良好的特征向量,以捕捉其特征。 时间序列模式,包括趋势、季节性和周期性等,可以通过简单和复杂的数学运算的组合来捕捉。 简单的计算包括,例如,计算时间序列的平均值和标准差。 更复杂的方法包括确定相关性或熵,例如。 此外,我们可以应用非线性时间序列分析函数来分解时间序列信号,例如傅里叶变换或小波变换,并使用这些函数的参数作为监督模型的特征。

从时间序列创建特征可能非常耗时;我们需要应用各种信号处理和时间序列分析算法来识别和提取有意义的特征。 <st c="2339">tsfresh</st> Python 包,代表 <st c="2597">tsfresh</st> 包括一个特征选择算法,该算法可以识别给定时间序列的最具预测性的特征。 <st c="2714">通过自动化应用复杂的时间序列方法,</st> tsfresh` 弥合了信号处理专家和机器学习实践者之间的差距,使得从时间序列数据中提取有价值的特征变得更加容易。

在本章中,我们将学习如何通过利用tsfresh自动从时间序列数据中创建数百个特征。随后,我们将讨论如何通过选择最相关的特征、从不同的时间序列中提取不同的特征以及将特征创建过程集成到 scikit-learn 管道中来对时间序列数据进行分类。

在本章中,我们将介绍以下食谱:以下内容:

  • 时间序列自动提取数百个特征

  • 时间序列数据自动创建和选择预测特征

  • 从不同的时间序列中提取不同的特征

  • 通过特征选择创建特征子集

  • 将特征创建嵌入到scikit-learn 管道

技术要求

在本章中,我们将使用开源的tsfresh Python 库。 您可以通过执行 tsfresh 安装 pip 来安装tsfresh pip install tsfresh

注意

如果您使用的是旧版的 Microsoft 操作系统,您可能需要更新 Microsoft C++构建工具才能继续安装tsfresh 包。 请按照此线程中的步骤操作: https://stackoverflow.com/questions/64261546/how-to-solve-error-microsoft-visual-c-14-0-or-greater-is-required-when-inst

我们将使用来自 UCI 机器学习仓库的占用检测数据集,该数据集可在http://archive.ics.uci.edu/ml/datasets/Occupancy+Detection找到,并受 Creative Commons Attribution 4.0 International (CC BY 4.0)许可协议的许可:https://creativecommons.org/licenses/by/4.0/legalcode。该数据的相应引用如下:

Candanedo, Luis. (2016). 占用检测. UCI 机器学习 仓库. https://doi.org/10.24432/C5X01N.

我下载并修改了如本笔记本所示的数据:https://github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch10-tsfresh/prepare-occupancy-dataset.ipynb

若要获取修改后的数据集和目标变量,请查看以下链接中的文件<st c="4971">occupancy.csv</st> <st c="4989">occupancy_target.csv</st>,可在以下链接找到:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch10-tsfresh

占用检测数据集包含在 135 小时内每分钟间隔采集的时间序列数据。变量测量办公室的温度、湿度、 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miC</mml:mi>mml:miO</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> 水平,以及照明消耗。 使用摄像头录像来确定是否有人在办公室。 目标变量显示办公室是否在任何一小时被占用。 如果目标变量取值为 <st c="5529">1</st>,则表示该小时办公室被占用;否则,它取值为 <st c="5612">0</st>

包含时间序列和目标变量的数据集行数不同。 时间序列数据集包含以一分钟间隔记录的 135 小时记录,即 8,100 行。 目标变量只有 135 行,每行都有一个标签,表示在 135 小时中的每个小时办公室是否被占用。

注意

查看本书 GitHub 仓库中的笔记本,以熟悉不同时间序列的绘图: 数据集: https://github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch10-tsfresh/prepare-occupancy-dataset.ipynb

从时间序列自动提取数百个特征

时间序列 按时间顺序索引的数据点 分析时间序列序列使我们能够做出各种预测。 例如,传感器数据可以用来预测管道故障,声音数据可以帮助识别音乐类型,健康历史或个人测量,如血糖水平,可以表明一个人是否生病,正如我们将在这个菜谱中展示的,光照模式、湿度和 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miC</mml:mi>mml:miO</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> 水平可以确定办公室 是否被占用。

为了训练回归和分类模型,使用传统的机器学习算法,如线性回归或随机森林,我们需要一个大小为 M x N的数据集,其中 M 是行数,N 是特征数或列数。 然而,对于时间序列数据,我们拥有的是一系列 M 时间序列,每个时间序列都有多个按时间顺序索引的行。 要在监督学习模型中使用时间序列,每个时间序列都需要映射到一个定义良好的特征向量, N,如图所示 以下

图 10.1 – 显示从时间序列创建特征过程(用于分类或回归)的图表

图 10.1 – 显示从时间序列创建特征过程(用于分类或回归)的图表

这些特征向量,表示为 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub></mml:math>, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math>, 和 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mn3</mml:mn></mml:mrow></mml:msub></mml:math> 图 10.1**.1中,应该能够捕捉时间序列的特征。 例如, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn></mml:mrow></mml:msub></mml:math>可能是时间序列的均值, <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:mix</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math>及其方差。 我们可以创建许多特征来描述时间序列,包括数据点的分布、相关性、平稳性或熵等。 因此,特征向量 N 可以通过应用一系列 特征化方法 that 取时间序列作为输入并返回一个或多个标量作为输出。 均值或总和将时间序列序列作为输入,并返回一个标量作为输出,该标量是时间序列的均值或其值的总和。 我们还可以将线性趋势拟合到时间序列序列中,这将返回两个标量——一个表示斜率,另一个表示 截距。

<st c="8267">tsfresh</st> 对时间序列应用 63 种特征化方法,每种方法返回一个或多个标量,因此对于任何给定的时间序列,都会产生超过 750 个特征。 在这个菜谱中,我们将使用 <st c="8474">tsfresh</st> 将时间序列数据转换为 M x N 特征 表,然后 我们将使用它来预测 办公室占用情况。

准备就绪

在这个菜谱中,我们将使用《技术要求》 部分中描述的 占用检测数据集。 该数据集包含在办公室内每分钟间隔测量的温度、湿度、 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miC</mml:mi>mml:miO</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> 级别和照明消耗。 有 135 小时的测量数据,每小时都有一个唯一的标识符。 还有一个包含目标变量的数据集,该变量指示在 135 小时中的哪个小时办公室被占用。 让我们加载数据并绘制一些图表来理解其模式:

  1. 让我们 <st c="9138">pandas</st> <st c="9149">matplotlib</st>:

     import matplotlib.pyplot as plt
    import pandas as pd
    
  2. 加载数据集并显示前五行: :

     X = pd.read_csv(
        "occupancy.csv", parse_dates=["date"])
    X.head()
    

    在下面的图中,我们可以看到包含唯一标识符的数据集,随后是测量日期和时间以及五个时间序列的值,这些时间序列捕捉了温度、湿度、照明和 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miC</mml:mi>mml:miO</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> 办公室的 级别:

图 10.2 – 包含时间序列数据的 DataFrame

图 10.2 – 包含时间序列数据的 DataFrame

  1. 让我们创建 一个函数来 绘制从 步骤 2 在给定小时的时间序列(记录的 135 小时中的每一小时都有一个唯一的标识符<st c="10066">id</st> 列):

     def plot_timeseries(n_id):
        fig, axes = plt.subplots(nrows=2, ncols=3,
        figsize=(20, 10))
        X[X[«id»] == n_id]["temperature"].plot(
            ax=axes[0, 0], title="temperature")
        X[X[«id»] == n_id]["humidity"].plot(
            ax=axes[0, 1], title="humidity")
        X[X[«id»] == n_id]["light"].plot(
            ax=axes[0, 2], title="light")
        X[X[«id»] == n_id]["co2"].plot(
        ax=axes[1, 0], title="co2")
        X[X[«id»] == n_id]["humidity_ratio"].plot(
            ax=axes[1,1], title="humidity_ratio")
        plt.show()
    
  2. 让我们 绘制办公室未被占用的小时对应的时间序列:

     plot_timeseries(2)
    

    在下面的图中,我们可以看到记录的第二个小时的时间序列值,当时办公室 是空的:

图 10.3 – 数据收集的第二个小时的时间序列值,当时办公室是空的

图 10.3 – 数据收集的第二个小时的时间序列值,当时办公室是空的

注意灯光是关闭的,这就是为什么我们在右上角的光消耗图中看到 0 处的水平线。

  1. 现在,让我们绘制出办公室被占用的小时对应的时间序列数据:

     plot_timeseries(15)
    

    在下面的图中,我们可以看到记录的第十五个小时的时间序列值,当时办公室 被占用:

图 10.4 – 数据收集的第十五个小时的时间序列值,当时办公室被占用

图 10.4 – 数据收集的第十五个小时的时间序列值,当时办公室被占用

注意这次灯光是亮着的(右上角面板)。

在这个菜谱中,我们将从这些时间序列数据的每一小时窗口中提取特征,捕捉 它们的 特征的各个方面。 从这些 60 分钟的时间序列段中,我们将使用 <st c="12453">tsfresh</st>自动生成超过 750 个特征,确保对 数据属性的全面表示。

如何做到...

我们将从一条时间序列 <st c="12628">灯光</st>中自动创建数百个特征,然后使用这些特征来预测办公室是否在任何 给定的小时被占用:

  1. 让我们导入所需的 Python 库 和函数:

     import pandas as pd
    from sklearn.linear_model import LogisticRegression
    from sklearn.metrics import classification_report
    from sklearn.model_selection import train_test_split
    from tsfresh import extract_features
    from tsfresh.utilities.dataframe_functions import (
        impute
    )
    
  2. 加载在 技术 要求 部分中描述的数据集:

     X = pd.read_csv("occupancy.csv", parse_dates=["date"])
    
  3. L将目标变量加载到一个 <st c="13210">pandas</st> 序列中:

     y = pd.read_csv("occupancy_target.csv",
        index_col="id")["occupancy"]
    
  4. 让我们使用 <st c="13379">tsfresh</st>自动为每小时的灯光记录创建数百个特征。要从 <st c="13416">light</st> 变量中创建特征,我们将包含此变量和每个时间序列的唯一标识符的 DataFrame 传递给 <st c="13533">extract_features</st> 函数 来自 <st c="13564">tsfresh</st>

     features = extract_features(
        X[[«id», «light»]], column_id="id")
    

    如果我们执行 <st c="13653">features.shape</st>,我们将获得 <st c="13682">(135, 789)</st> ,这对应于结果 DataFrame 的大小 ,其中每一行代表一个小时的记录,每一列代表由 <st c="13839">tsfresh</st>创建的一个特征。有 789 个特征可以描述任何给定小时的光消耗。 继续执行 <st c="13947">features.head()</st> 以查看结果 DataFrame。 由于篇幅原因,我们无法在书中显示整个 DataFrame。 因此,我们将探索一些 特征。

  5. 让我们将创建的五个特征名称存储在 一个数组中:

     feats = features.columns[10:15]
    

    如果我们执行 <st c="14241">feats</st>,我们将看到与平均数、长度、标准差、变异系数和光消耗的方差相对应的五个特征名称 每小时:

    <st c="14413">Index(['light__mean', 'light__length',</st> <st c="14453">'light__standard_deviation',</st> <st c="14482">'light__variation_coefficient',</st> <st c="14514">'light__variance'], dtype='object')</st>
    
  6. 现在,让我们显示从 步骤 5 开始的第一个 五个小时的特征值:

     features[feats].head()
    

    在以下 DataFrame 中,我们看到从 前五个小时的 消耗 的时间序列中提取的特征:

图 10.5 – 为每小时的光消耗创建的特征

图 10.5 – 为每小时的光消耗创建的特征

查看图 10.4中的光消耗平均值,我们可以看到前一个小时灯光是开启的,然后在接下来的四个小时里关闭了。 时间序列的长度是 60,因为我们每小时有 60 分钟的记录。

注意

<st c="15383">tsfresh</st> 对时间序列应用 63 种特征创建方法。 根据时间序列的特征,如其长度或其变异性,某些方法可能会返回缺失值或无穷大值。 例如,在 图 10**.4中,我们可以看到在光照消耗恒定的那些小时,无法计算变异系数。 并且在这些情况下,方差也是 <st c="15775">0</st> 实际上,对于我们的数据集,许多生成的特征只包含 <st c="15863">NaN</st> 值,或者像长度一样是恒定的,因此对于训练机器 学习模型来说没有用。

  1. <st c="15975">tsfresh</st> 包含一个用于填充包含 <st c="16048">NaN</st> 值的特征的填充函数。 让我们继续填充 我们的特征:

     impute(features)
    

    The <st c="16121">impute</st> 函数来自 <st c="16142">tsfresh</st> ,它将 <st c="16159">NaN</st><st c="16164">-Inf</st> <st c="16174">Inf</st> 值分别替换为变量的中位数、最小值或最大值。

    让我们使用这些特征来训练一个逻辑回归模型,并预测办公室 是否被占用。

  2. 让我们开始 数据集 分为训练集和 测试集:

     X_train, X_test, y_train, y_test = train_test_split(
        features,
        y,
        test_size=0.1,
        random_state=42,
    )
    
  3. 现在,让我们设置并训练一个逻辑回归模型,然后评估 其性能:

     cls = LogisticRegression(random_state=10, C=0.01)
    cls.fit(X_train, y_train)
    print(classification_report(
         y_test, cls.predict(X_test)))
    

    在以下输出中,我们看到用于分类分析的常用评估指标的值,这表明创建的特征对于预测 办公室占用情况是有用的:

     <st c="16959">precision     recall</st> <st c="16976">f1-score   support</st>
     <st c="16993">0         1.00        1.00        1.00           11</st>
     <st c="17013">1         1.00</st> <st c="17021">1.00        1.00            3</st>
     <st c="17032">accuracy                                       1.00           14</st>
     <st c="17049">macro avg         1.00        1.00        1.00</st> <st c="17074">14</st>
    <st c="17077">weighted avg         1.00        1.00        1.00           14</st>
    

注意

为了保持方法简单,我没有优化模型超参数或调整概率阈值——这些是我们通常为了确保模型 准确度所做的事情。

  1. 最后,让我们 提取每个时间序列的特征,即 <st c="17358">光照</st><st c="17365">温度</st><st c="17378">湿度</st> <st c="17392">二氧化碳</st>,这次,我们将在提取后立即填充特征:

     features = extract_features(
        X,
        column_id="id",
        impute_function=impute,
        column_sort="date",
    )
    

注意

步骤 10中,我们指出我们想要根据包含测量时间和日期的时间戳对时间序列进行排序,通过将 <st c="17716">date</st> 变量传递给 <st c="17737">column_sort</st> 参数。 这在我们的时间序列不是等距或不是按时间顺序排列时很有用。 如果我们将此参数设置为 <st c="17882">None</st> <st c="17888">tsfresh</st> 将假设时间序列是有序的且等距的。

步骤 10 的输出是一个包含 135 行的 DataFrame,包含 3,945 个特征(执行 <st c="18051">features.shape</st> 以检查这一点),这些特征描述了五个原始时间序列——温度、光照、湿度及其比率,以及 <mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">mml:msubmml:mrowmml:miC</mml:mi>mml:miO</mml:mi></mml:mrow>mml:mrowmml:mn2</mml:mn></mml:mrow></mml:msub></mml:math> 办公室。 这些 特征在 步骤 10中被插补,因此您可以继续使用此 DataFrame 来训练另一个逻辑 回归 模型以预测 办公室的占用情况。

它是如何工作的...

在这个菜谱中,我们使用了 <st c="18397">tsfresh</st> 来自动从五个时间序列中创建数百个特征,然后使用这些特征来训练一个逻辑回归模型以预测办公室 是否被占用。

注意

要使用 <st c="18612">tsfresh</st>创建特征,我们想要从中提取特征的时序间隔必须用一个 <st c="18700">id</st> 变量</st c="18757">标记。

为了从时间序列创建特征,我们使用了 <st c="18817">extract_features</st> 函数,该函数来自 <st c="18848">tsfresh</st>。此函数接受包含时间序列和唯一标识符的 DataFrame 作为输入,并返回一个包含提取特征的 DataFrame 作为输出。 (执行 <st c="18051">features.shape</st> 以检查这一点)。

<extract_features>有三个关键参数:column_idcolumn_sortimpute_functioncolumn_id接收用于提取特征时每个序列的唯一标识符的列名。column_sort用于在提取特征之前重新排序时间序列。当column_sort被设置为None时,tsfresh假设数据是按时间顺序排列的,并且时间戳是等距的。在步骤 10中,我们传递了date变量作为排序变量,这通知tsfresh在提取特征之前如何排序数据。

注意

在我们的数据集中,将column_sort设置为None或传递date变量没有区别,因为我们的时间序列已经是按时间顺序排列的,并且时间戳是等距的。如果您的时序不是这种情况,请使用此参数正确创建特征。

最后extract_features也接受通过impute_function参数的impute函数,以自动从创建的特征中移除无限和NaN值。将在后续菜谱中讨论extract_features的附加参数。

注意

更多关于extract_features函数的细节

<st c="20313">impute</st> 函数,它可以独立使用,就像我们在步骤 7中做的那样,或者可以在<st c="20404">extract_features</st> 函数中使用,就像我们在步骤 10中做的那样,用变量的中位数、最小值或最大值分别替换了<st c="20462">NAN</st><st c="20465">-Inf</st><st c="20477">Inf</st>值。如果特征只包含 <st c="20587">NaN</st> 值,它们将被零替换。 插补是在原地发生的——也就是说,在同一个正在被插补的 DataFrame 中。

<st c="20714">extract_features</st> 函数返回一个 DataFrame,它包含与数据中唯一标识符一样多的行。 在我们的例子中,它返回了一个包含 135 行的 DataFrame。 生成的 DataFrame 的列对应于应用于每个 135 个 60 分钟时间序列的 63 个表征方法返回的 789 个值。

步骤 5中,我们探索了一些生成的特征,这些特征捕捉了时间序列的均值、方差和变异系数,以及它们的长度。让我们再探索一些生成的特征。

其中一些创建的变量是自解释的。例如,'light__skewness''light__kurtosis'变量包含偏度和峰度系数,这些系数表征了数据分布。<st c="21466">'light__has_duplicate_max'</st><st c="21494">'light__has_duplicate_min'</st>,以及'light__has_duplicate'<st c="21548">变量指示时间序列在时间间隔内是否有重复值或重复的最小值或最大值。</st><st c="21680">'light__quantile__q_0.1'<st c="21708">,'light__quantile__q_0.2'<st c="21734">,以及</st><st c="21740">'light__quantile__q_0.3'</st>变量显示了时间序列的不同分位数。最后,'light__autocorrelation__lag_0'<st c="21877">,'light__autocorrelation__lag_1'<st c="21910">,以及</st><st c="21916">'light__autocorrelation__lag_2'</st>变量显示了时间序列与其过去值的自相关,滞后 0,1 或 2 步——这些信息通常在预测中很有用。in预测。

其他特征描述方法返回来自信号处理算法的特征,例如对于 Ricker 小波,它返回以下特征,即<st c="22281">'light__cwt_coefficients__coeff_0__w_2__widths_(2, 5, 10, 20)'</st><st c="22345">'light__cwt_coefficients__coeff_0__w_5__widths_(2, 5, 10, 20)'</st><st c="22409">'light__cwt_coefficients__coeff_0__w_10__widths_(2, 5, 10, 20)'</st>,以及'light__cwt_coefficients__coeff_0__w_20__widths_(2, 5, 10, 20)'`等。其中一些。

注意

由于方法众多,本书无法详细讨论每种特征描述方法及其输出。您可以在https://tsfresh.readthedocs.io/en/latest/api/tsfresh.feature_extraction.html找到有关<st c="22768">tsfresh</st>支持的转换及其公式的更多详细信息。

<st c="22934">tsfresh</st> 自动创建的一些特征可能对某些时间序列没有意义,甚至无法计算,因为它们需要一定的长度或数据变化性,或者时间序列必须满足某些分布假设。 因此,特征的可适用性将取决于时间序列的性质。

注意

您可以根据领域知识决定从您的时序数据中提取哪些特征,或者通过创建所有可能的特征,然后应用特征选择算法或进行数据分析。 实际上,从我们的数据集中,许多生成的特征要么是恒定的,要么只包含缺失数据。 因此,我们可以通过从数据中去除这些特征来减少特征空间到信息特征。

另请参阅

有关 <st c="23690">tsfresh</st> 的更多 详细信息,请参阅文章 Christ M.,Braun N.,,Neuffer J.,和 Kempa-Liehr A. (2018)。 基于可扩展假设检验的时间序列特征提取 (tsfresh – 一个 Python 包)。 神经计算 307 (2018)。 72-77 页。 https://dl.acm.org/doi/10.1016/j.neucom.2018.03.067

自动从时间序列数据中创建和选择预测特征

在上一个菜谱 中,我们 使用 <st c="24166">tsfresh</st>自动从时间序列变量中提取了数百个特征。如果我们有多个时间序列变量,我们很容易得到一个包含数千个特征的数据库。 此外,许多生成的特征只有缺失数据或恒定值,因此对训练机器学习模型没有用。学习 模型。

当我们创建分类和回归模型来解决现实生活中的问题时,我们通常希望我们的模型只输入少量相关特征以产生可解释的机器学习输出。 简单的模型有很多优点。 首先,它们的输出更容易解释。 其次,简单的模型存储成本低,训练速度快。 它们也能更快地返回输出。

<st c="24838">tsfresh</st> 包含一个基于非参数统计假设检验的高度可并行化的特征选择算法,该算法可以在特征创建过程之后执行,以快速去除不相关的特征。特征选择过程利用不同的测试针对不同的特征。

<st c="25149">tsfresh</st> 使用以下测试来 <st c="25186">选择特征</st>

  • 如果特征和目标 <st c="25275">都是二进制</st><st c="25202">费舍尔精确检验独立性</st>

  • 如果特征或目标 <st c="25347">是二进制</st><st c="25285">科尔莫哥洛夫-斯米尔诺夫检验</st>

  • 如果特征或目标 <st c="25414">都不是二进制</st><st c="25356">肯德尔秩次检验</st>

这些测试的优势在于它们是非参数的,因此不对被测试变量的潜在分布做出假设。

这些测试的结果是一个衡量每个特征与目标之间关联显著性的 p 值向量。然后根据 <st c="25756">本雅明-叶库蒂埃</st> 程序对这些 p 值进行评估,以决定保留哪些特征。

<st c="25824">注意</st>

关于 <st c="25854">tsfresh</st> 的特征选择过程的更多详细信息,请参阅 Christ, Kempa-Liehr, 和 Feindt 的文章,分布式和并行时间序列特征提取用于工业大数据应用。亚洲机器学习会议 (ACML) 2016,大数据学习研讨会 (WLBD),汉密尔顿(新西兰),arXiv,https://arxiv.org/abs/1610.07717v1

在这个食谱中,我们将自动从各种时间序列中创建数百个特征,然后通过使用 <st c="26348">tsfresh</st> 来选择最相关的特征。

<st c="26356">如何做到...</st>

我们将首先自动从单个时间序列 <st c="26458">lights</st> 中创建和选择特征,然后我们将自动化针对多个 <st c="26519">时间序列</st> 的过程:

  1. 让我们导入所需的 Python 库和函数:

     import pandas as pd
    from sklearn.linear_model import LogisticRegression
    from sklearn.metrics import classification_report
    from sklearn.model_selection import train_test_split
    from tsfresh import (
        extract_features,
        extract_relevant_features,
        select_features,
    )
    from tsfresh.utilities.dataframe_functions import impute
    
  2. 加载在 技术 要求 部分中描述的 <st c="26925">数据集</st> 和目标变量:

     X = pd.read_csv("occupancy.csv", parse_dates=["date"])
    y = pd.read_csv("occupancy_target.csv",
        index_col="id")["occupancy"]
    
  3. 让我们为每个小时的使用记录自动创建数百个特征,并填补结果特征:

     features = extract_features(
        X[[«id», «light»]],
        column_id="id",
        impute_function=impute,
    )
    

    上一步的输出是一个包含 135 行和 789 列的 DataFrame,对应于从每个小时的能耗中创建的特征。

注意

关于步骤 3或占用检测数据集的更多详细信息,请查看从时间序列中自动提取数百个特征的食谱。

  1. 现在,让我们根据我们在本食谱介绍中提到的非参数测试来选择特征:

     features = select_features(features, y)
    

    如果我们执行 <st c="27818">len(features)</st>,我们将看到值 <st c="27853">135</st>,这意味着从步骤 3中创建的 789 个特征中,只有 135 个具有统计学意义。继续执行 <st c="27977">features.head()</st>以显示结果 DataFrame 的前五行。

  2. 由于空间原因,我们只将显示前个特征:

     feats = features.columns[0:5]
    features[feats].head()
    

    在以下 DataFrame 中,我们可以看到前五个小时的能耗的前五个特征的值:

图 10.6 – 从每个小时的能耗中创建的五个选定特征的 DataFrame

图 10.6 – 从每个小时的能耗中创建的五个选定特征的 DataFrame

如何工作…部分查看讨论,以获取关于步骤 4产生的 DataFrame 的更详细分析。

  1. 现在,我们将使用步骤 4中的特征来训练一个逻辑回归模型,并预测办公室是否被占用。首先,我们将数据集分为训练集和测试集:

     X_train, X_test, y_train, y_test = train_test_split(
        features,
        y,
        test_size=0.1,
        random_state=42,
    )
    
  2. 让我们设置并训练一个逻辑回归模型,然后评估其性能:

     cls = LogisticRegression(
        random_state=10, C=0.1, max_iter=1000)
    cls.fit(X_train, y_train)
    print(classification_report(
        y_test, cls.predict(X_test)))
    

    在以下输出中,我们可以看到用于分类分析的常用评估指标值。这些指标表明,所选特征对预测办公室占用情况很有用:

     <st c="29491">precision     recall  f1-score   support</st>
     <st c="29525">0</st> <st c="29527">1.00        0.91        0.95           11</st>
     <st c="29545">1         0.75        1.00        0.86            3</st>
    **<st c="29564">accuracy                                       0.93           14</st>**
     **<st c="29581">macro avg         0.88        0.95        0.90           14</st>**
    `<st c="29980">extract_relevant_features</st>`<st c="30005">, and, like this, combine</st> *<st c="30031">steps 3</st>* <st c="30038">and</st> *<st c="30043">4</st>*<st c="30044">. We’ll do that to create and select features automatically for the five time series in</st> <st c="30132">our dataset:</st>
    
    

    features = extract_relevant_features(

    X,
    
    y,
    
    column_id="id",
    
    column_sort="date",
    

    )

注意

extract_relevant_features 的参数与 extract_features 的参数非常相似。 然而,请注意,前者将自动执行插补以能够进行特征选择。 我们在 *从时间序列中自动提取数百个特征 *食谱 中讨论了 extract_features 的参数。

步骤 8 的 输出 由一个包含 135 行和 968 个特征的 DataFrame 组成,这些特征是从默认返回的 3,945 个原始特征中提取的,由 tsfresh 返回(你可以通过执行 features.shape 来检查这一点)。 继续使用这个 DataFrame 来训练另一个逻辑回归模型以预测办公室占用率。

它是如何工作的...

在这个 食谱 中,我们 从时间序列中创建了数百个特征,然后根据非参数统计测试选择了最相关的特征。 特征创建和选择过程是通过 tsfresh 自动执行的。

为了创建特征,我们使用了 tsfreshextract_features 函数,我们在 *从时间序列中自动提取数百个特征 *食谱 中对其进行了详细描述。

为了选择特征,我们使用了来自 tsfreshselect_features 函数。这个函数根据特征的性质和目标应用不同的统计测试。 简而言之,如果特征和目标是二元的,它通过费舍尔精确测试测试它们之间的关系。 如果特征或目标是二元的,而另一个变量是连续的,它通过使用柯尔莫哥洛夫-斯米尔诺夫测试来测试它们之间的关系。 如果特征和目标都不是二元的,它使用肯德尔秩测试。

这些测试的结果是一个向量,每个特征都有一个 p 值。 接下来, <st c="31899">tsfresh</st> 应用 Benjamini-Yekutieli 程序,旨在降低错误发现率,根据 p 值选择要保留的特征。 这种特征选择程序有一些优点,主要优点是统计测试计算速度快,因此选择算法可扩展且可并行化。 另一个优点是测试是非参数的,因此适用于线性和非线性模型。

然而,评估每个特征单独的特征选择方法无法去除冗余特征。事实上,许多由 <st c="32524">tsfresh</st> 自动创建的特征将高度相关,例如那些捕捉到不同消费分位数的光照。 因此,它们将显示相似的 p 值并被保留。 但在实践中,我们只需要一个或少数几个来捕捉时间序列的信息。 我建议使用能够识别 <st c="32811">tsfresh</st> 选择程序中特征交互的替代特征选择方法。

最后,在 步骤 8中,我们通过使用 <st c="33054">extract_relevant_features</st> 函数将特征创建步骤(步骤 3)与特征选择步骤(步骤 4)结合起来。 <st c="33090">extract_relevant_features</st> <st c="33128">extract_features</st> 函数应用于从每个时间序列中创建特征并进行插补。 接下来,它应用 <st c="33238">select_features</st> 函数以返回一个 DataFrame,其中每行都有一个唯一的标识符,以及为每个时间序列选择的特征。 请注意,不同的特征可以为不同的 时间序列 选择。

另请参阅

<st c="33495">tsfresh</st> 的选择算法提供了一个快速的方法来去除无关特征。 然而,它并不能找到分类或回归任务的最佳特征子集。 可以在 <st c="33711">tsfresh</st>算法之后应用其他特征选择方法来进一步减少特征 空间。

有关特征选择算法的更多详细信息,请参阅 Soledad Galli 所著的书籍 《Python 机器学习中的特征选择》 在 Leanpub 上的内容: https://leanpub.com/feature-selection-in-machine-learning/

从不同的时间序列中提取不同的特征

<st c="34035">tsfresh</st> 基于时间序列的特征和分布提取许多特征,例如它们的关联属性、平稳性和熵。 它还应用非线性时间序列分析函数,例如通过傅里叶或小波变换分解时间序列信号。 根据时间序列的性质,这些变换中的一些比其他的有意义。 例如,波长分解方法对于由信号或传感器产生的时间序列是有意义的,但并不总是对表示销售或 股价的时间序列有用。

在这个菜谱中,我们将讨论如何优化特征提取过程以从每个时间序列中提取特定特征,然后使用这些特征来预测 办公室占用率。

如何做到这一点...

<st c="34846">tsfresh</st> 通过包含方法名称作为键的字典访问将用于创建特征的方法,如果它们需要一个参数,它将参数作为值。 <st c="35037">tsfresh</st> 还包括一些预定义的字典。 我们将首先探索这些预定义的字典,这些字典可以通过 <st c="35177">settings</st> 模块访问:

  1. 让我们导入所需的 Python 库、函数和 <st c="35259">settings</st> 模块:

     import pandas as pd
    from sklearn.linear_model import LogisticRegression
    from sklearn.metrics import classification_report
    from sklearn.model_selection import train_test_split
    from tsfresh.feature_extraction import (
        extract_features
    )
    from tsfresh.feature_extraction import settings
    
  2. 加载文档中描述的数据集和目标变量

     X = pd.read_csv("occupancy.csv", parse_dates=["date"])
    y = pd.read_csv("occupancy_target.csv",
        index_col="id")["occupancy"]
    

    <st c="35773">tsfresh</st> 包括 三个主要字典,这些字典控制特征创建输出: <st c="35857">settings.ComprehensiveFCParameters</st>, <st c="35893">settings.EfficientFCParameters</st>, 和 <st c="35929">settings.MinimalFCParameters</st>。在这里,我们将探索返回特征最少的字典。 您可以重复这些步骤来探索 其他字典。

  3. 显示在使用返回最少特征的字典时将应用的特征创建方法: 最少特征:

     minimal_feat = settings.MinimalFCParameters()
    minimal_feat.items()
    

    在输出中的 步骤 3,我们看到一个以特征提取方法名称为键,如果有的话,使用这些方法的参数,作为值的字典: 作为值:

    <st c="36433">ItemsView({'sum_values': None, 'median': None, 'mean': None, 'length': None, 'standard_deviation': None, 'variance': None, 'root_mean_square': None, 'maximum': None, 'absolute_maximum': None, 'minimum': None})</st>
    

注意

继续探索其他两个预定义的字典, <st c="36709">settings.ComprehensiveFCParameters</st> <st c="36748">settings.EfficientFCParameters</st>,通过调整来自 步骤 3的代码。

  1. 现在,让我们使用来自 步骤 3 的字典来提取仅从 <st c="36897">灯光</st> 时间 序列中提取的特征,然后显示结果 DataFrame 的形状:

     features = extract_features(
        X[[«id», «light»]],
        column_id="id", <st c="37035">default_fc_parameters=minimal_feat,</st> )
    features.shape
    

    步骤 4 的输出是 <st c="37112">(135, 10)</st>,这意味着为 135 个小时的灯光消耗数据中的每一个都创建了 10 个特征。

  2. 让我们显示 结果 DataFrame:

     features.head()
    

    我们在以下 DataFrame 中看到前五个小时灯光消耗的结果特征值: 在以下 DataFrame 中:

图 10.7 – 每小时灯光消耗创建的特征 DataFrame

图 10.7 – 每小时灯光消耗创建的特征 DataFrame

现在,我们将使用这些特征来训练一个逻辑回归模型,以预测办公室 是否被占用。

  1. 让我们首先将数据集分为训练集和 测试集:

     X_train, X_test, y_train, y_test = train_test_split(
        features,
        y,
        test_size=0.1,
        random_state=42,
    )
    
  2. 现在,让我们设置并训练一个逻辑回归模型,然后评估 其性能:

     cls = LogisticRegression(random_state=10, C=0.01)
    cls.fit(X_train, y_train)
    print(classification_report(
        y_test, cls.predict(X_test)))
    

    在以下输出中,我们看到用于分类分析的常用评估指标。 这些指标表明所选特征对预测办公室占用是有用的:

     <st c="38730">precision     recall  f1-score   support</st>
    **<st c="38764">0         1.00        0.91        0.95           11</st>**
     **<st c="38784">1         0.75        1.00        0.86            3</st>**
     **<st c="38803">accuracy                                       0.93           14</st>**
     **<st c="38820">macro avg         0.88        0.95        0.90           14</st>**
    **<st c="38848">weighted avg         0.95        0.93</st>** **<st c="38871">0.93           14</st>**
    

**注意

由于灯光消耗是办公室占用非常好的指标,具有非常简单的特征,我们可以获得一个预测逻辑 回归模型。

现在,让我们 学习如何为不同的时间序列指定不同特征的创建。 时间序列。

  1. 让我们创建一个字典,包含我们想要从 <st c="39236">光</st> 时间序列 中创建特征的方法的名称。 我们将方法的名称作为键,如果方法需要参数,我们将其作为附加字典传递给相应的键;否则,我们传递 <st c="39413">None</st> 作为 值:

     light_feat = {
        «sum_values": None,
        "median": None,
        «standard_deviation": None,
        "quantile": [{"q": 0.2}, {"q": 0.7}],
    }
    
  2. 现在,让我们创建一个字典,包含我们想要从 <st c="39634">co2</st> 时间序列 中创建的特征:

     co2_feat = {
        «root_mean_square": None,
        «number_peaks": [{"n": 1}, {"n": 2}],
    }
    
  3. 让我们 将这些字典合并成一个 新的字典:

     kind_to_fc_parameters = {
        «light»: light_feat,
        "co2": co2_feat,
    }
    
  4. 最后,让我们使用步骤 10 中的字典来创建两个时间序列的特征:

     features = extract_features(
        X[[«id», «light», «co2»]],
        column_id="id",
        kind_to_fc_parameters=kind_to_fc_parameters,
    )
    

    步骤 11 的输出由一个包含 135 行和 8 个特征的 DataFrame 组成。 如果我们执行 <st c="40154">features.columns</st>,我们将看到创建的特征的名称:

    <st c="40218">Index(['light__sum_values', 'light__median',</st>
     <st c="40263">'light__standard_deviation',</st>
     <st c="40292">'light__quantile__q_0.2',</st>
     <st c="40318">'light__quantile__q_0.7',</st>
     <st c="40344">'co2__root_mean_square',</st>
     <st c="40369">'co2__number_peaks__n_1',</st>
     <st c="40395">'co2__numb</st><st c="40406">er_peaks__n_2'],</st>
     <st c="40423">dtype='object')</st>
    

注意在 步骤 11的输出中,已经从每个 <st c="40534">光</st> <st c="40544">co2</st> 时间序列 中创建了不同的变量。

它是如何工作的...

在这个 菜谱中,我们从我们的时间序列数据中提取了特定的特征。 首先,我们根据<st c="40727">tsfresh</st>附带的一个预定义字典创建了特征。接下来,我们创建了自己的字典,指定为不同的 时间序列 创建不同的特征。

<st c="40850">tsfresh</st> 包附带一些预定义的字典,可以通过 <st c="40939">settings</st> 模块 访问。 <st c="40960">MinimalFCParameters</st> 字典用于根据时间序列分布的基本统计参数(如平均值、中位数、标准差、方差、值的总和、计数(或长度)以及最小值和最大值)创建 10 个简单特征。 步骤 3中,我们展示了该字典,方法名称作为键,由于这些方法不需要额外的参数,每个键都有 <st c="41369">None</st> 作为值。

<st c="41387">tsfresh</st> 有两个额外的预定义字典。 <st c="41440">EfficientFCParameters</st> 用于应用计算快速的算法,而 <st c="41521">ComprehensiveFCParameters</st> 返回所有可能的特征,并且是extract_</st><st c="41615">features</st> 函数默认使用的。

注意

有关预定义字典的更多详细信息,请查看<st c="41710">tsfresh</st> 文档: https://tsfresh.readthedocs.io/en/latest/text/feature_extraction_settings.html

通过在<st c="41863">default_fc_parameters</st> 参数中使用这些预定义字典,我们可以从一条或多条时间序列中创建特定的特征,就像我们在 步骤 4中做的那样。请注意, <st c="42029">default_fc_parameters</st> 指示 <st c="42061">extract_features</st> 所有 时间序列中创建相同的特征。 如果我们想从不同的时间序列中提取不同的特征怎么办呢?

为了为不同的时间序列创建不同的特征,我们可以使用<st c="42277">kind_to_fc_parameters</st> 参数,这是<st c="42312">tsfresh</st> <st c="42323">extract_features</st> 函数的。 此参数接受一个字典的字典,指定应用于每个 时间序列的算法。

第 8 步中,我们创建了一个字典来指定从<st c="42543">light</st>时间序列创建特定特征。请注意,<st c="42576">"sum_values"</st><st c="42593">"mean"</st>方法将<st c="42613">None</st>作为值,但<st c="42637">quantile</st>方法需要额外的参数,这些参数对应于应从时间序列返回的分位数。在第 9 步中,我们创建了一个字典来指定从<st c="42838">co2</st>时间序列创建特征。在第 10 步中,我们将这两个字典合并为一个,以时间序列的名称作为键,特征创建字典作为值。然后,我们将这个字典传递给<st c="43079">tsfresh</st><st c="43090">extract_features</st>函数的<st c="43044">kind_to_fc_parameters</st>参数。如果我们使用领域知识来创建特征,或者只创建少量特征,这种方式指定特征是合适的。

如果我们想要为各种时间序列创建多个特征,是否需要手动将每个方法键入字典中?实际上并不需要。在下面的配方中,我们将学习如何根据 Lasso 选择的特征指定要创建哪些特征。

通过特征选择创建特征子集

在《自动从时间序列数据中创建和选择预测特征》的配方中,我们学习了如何使用<st c="43720">tsfresh</st>来选择相关特征。我们还讨论了<st c="43766">tsfresh</st>选择程序的局限性,并建议采用替代特征选择方法来识别预测特征,同时避免冗余。

在这个配方中,我们将使用<st c="43985">tsfresh</st>创建和选择特征。之后,我们将通过利用 Lasso 正则化进一步减少特征空间。然后,我们将学习如何从选定的特征名称创建字典,以触发仅从未来的时间序列创建这些特征。

...如何做到这一点...

让我们首先导入必要的库并准备 数据集:

  1. 让我们导入 所需的库和函数:

     import pandas as pd
    from sklearn.feature_selection import SelectFromModel
    from sklearn.linear_model import LogisticRegression
    from tsfresh import (
        extract_features,
        extract_relevant_features,
    )
    from tsfresh.feature_extraction import settings
    
  2. 加载在 技术 要求 部分中描述的 占用检测数据集:

     X = pd.read_csv("occupancy.csv", parse_dates=["date"])
    y = pd.read_csv(
        "occupancy_target.csv",
        index_col="id")["occupancy"]
    
  3. 从我们的五个时间序列创建并选择特征,然后显示结果的 DataFrame 的形状:

     features = extract_relevant_features(
        X,
        y,
        column_id="id",
        column_sort="date",
    )
    features.shape
    

    步骤 3 *的输出是 <st c="45068">(135, 968)</st>,表示从五个原始时间序列中返回了 968 个特征,对于每小时的记录。

注意

我们在 步骤 3 的 *自动从时间序列数据创建和选择预测特征食谱 *中讨论了该函数。

让我们通过选择具有 Lasso 正则化的特征进一步减少特征空间。

  1. 设置具有 Lasso 正则化的逻辑回归,这是 <st c="45478">"l1"</st> 惩罚。 我还任意设置了 一些额外的参数:

     cls = LogisticRegression(
        penalty="l1",
        solver=»liblinear",
        random_state=10,
        C=0.05,
        max_iter=1000,
    )
    
  2. 让我们设置一个转换器来保留那些逻辑回归系数 与 0 不同的特征:

     selector = SelectFromModel(cls)
    
  3. 训练逻辑回归模型并选择 特征:

     selector.fit(features, y)
    
  4. 现在,将所选特征存储在 一个变量中:

     features = selector.get_feature_names_out()
    

    如果我们 执行 <st c="45985">特征</st>,我们将 看到所选特征的 名称:

    array([
    'light__sum_of_reoccurring_data_points',
    'co2__fft_coefficient__attr_"abs"__coeff_0',
    'co2__spkt_welch_density__coeff_2', 'co2__variance',
    'temperature__c3__lag_1', 'temperature__abs_energy',
    'temperature__c3__lag_2', 'temperature__c3__lag_3',
    'co2__sum_of_reoccurring_data_points',
    'light__spkt_welch_density__coeff_8',
    'light__agg_linear_trend__attr_"intercept"__chunk_len_50__f_agg_"var"',
             'light__agg_linear_trend__attr_"slope"__chunk_len_50__f_agg_"var"',  'light__agg_linear_trend__attr_"intercept"__chunk_len_10__f_agg_"var"'],
    dtype=object)
    
  5. 要从时间序列中提取仅来自 步骤 6 的特征,我们需要在字典中捕获特征创建方法名称和相应的参数。 我们可以通过使用 <st c="46815">tsfresh</st>从特征名称 自动完成此操作:

     kind_to_fc_parameters = settings.from_columns(
        selector.get_feature_names_out(),
    )
    

    如果我们执行 <st c="46922">kind_to_fc_parameters</st>,我们将看到从 步骤 6 的特征名称 *创建的字典:

    {'light':
        {‹sum_of_reoccurring_data_points': None,
        ‹spkt_welch_density': [{'coeff': 8}],
        'variance': None,
        ‹agg_linear_trend': [
            {‹attr': 'slope','chunk_len': 50,
                'f_agg': 'var'},
            {‹attr': 'intercept',
                'chunk_len': 10,'f_agg':'var'}
            ]
        },
    'co2':
        {‹spkt_welch_density': [{'coeff': 2}],
        'variance': None,
        ‹sum_of_reoccurring_data_points': None
        },
        'temperature': {
            'c3': [{'lag': 1}, {'lag': 2}, {'lag':3}],
            'abs_energy': None}
    }
    
  6. 现在,我们可以使用 来自 步骤 8 的字典以及 <st c="47519">extract_features</st> 函数来仅从 我们的数据集中创建那些特征:

     features = extract_features(
        X,
        column_id="id",
        column_sort="date",
        kind_to_fc_parameters=kind_to_fc_parameters,
    )
    

新的 DataFrame 可以通过执行 <st c="47763">features.head()</st>来显示,它只包含由 Las选择的 12 个特征。 请在您的计算机上进一步验证结果。

它是如何工作的...

在这个食谱中,我们 从 5 个时间序列中创建了 968 个特征 接下来,我们使用 Lasso 正则化将特征空间减少到 12 个特征。 最后,我们将所选特征的规格捕获到一个字典中,以便在未来的操作中,我们只从我们的 时间序列中创建这些特征。

要使用 <st c="48261">tsfresh</st>自动创建和选择特征,我们使用了 <st c="48282">extract_relevant_features</st> 函数,我们在 *从时间序列数据自动创建和选择预测特征 * *食谱 * 中详细描述了它。

Lasso 正则化具有将逻辑回归模型的一些系数降低到 0 的内禀能力。 系数为 0 的特征对办公室占用预测的贡献为零,因此可以将其移除。 使用 <st c="48698">SelectFromModel()</st> 类可以识别并移除这些特征。 我们设置了一个使用 Lasso 正则化的逻辑回归模型的 <st c="48787">SelectFromModel()</st> 实例来找到模型系数。 通过 <st c="48906">fit()</st> <st c="48913">SelectFromModel()</st> 使用我们从时间序列创建的 968 个特征训练了逻辑回归模型,并识别了那些系数与 0 不同的特征。 然后,使用 <st c="49099">get_feature_names_out()</st> 方法,我们将所选特征的名称捕获到一个 新变量中。

为了仅创建由 Lasso 正则化选择的 12 个特征,我们使用来自 <st c="49323">from_columns()</st> 函数的 <st c="49352">tsfresh</st>来创建一个字典。此函数返回一个字典,其变量是作为键从其中选择特征的。 值是包含用于创建特征的键的额外字典,以及如果有的话,作为值的参数。 为了创建新特征,我们使用这个 字典 以及 <st c="49670">extract_features</st> 函数。

注意

步骤 9中,我们将整个数据集 传递给 <st c="49749">extract_features</st> 函数。 生成的特征只包含从五个时间序列中的三个提取的特征。 另外两个时间序列 被忽略。

将特征创建集成到 scikit-learn 管道中

在本章中,我们讨论了如何通过利用 <st c="50096">tsfresh</st>自动创建和选择时间序列数据中的特征。然后,我们使用这些特征来训练一个分类模型,以预测在任何给定的小时内办公室是否被占用。

<st c="50226">tsfresh</st> 包括围绕其主函数的包装类, <st c="50244">wrapper</st> <st c="50287">extract_features</st> <st c="50308">extract_relevant_features</st>,以便使特征创建和选择与 <st c="50402">scikit-learn 管道</st>兼容。

在这个菜谱中,我们将设置一个 scikit-learn 管道,使用 <st c="50526">tsfresh</st> 从时间序列中提取特征,然后用这些特征训练一个逻辑回归模型来预测 办公室的占用情况。

如何操作...

让我们首先导入必要的库并准备好数据集:

  1. 让我们导入所需的库 和函数:

     import pandas as pd
    from sklearn.pipeline import Pipeline
    from sklearn.linear_model import LogisticRegression
    from sklearn.model_selection import train_test_split
    from sklearn.metrics import classification_report
    from tsfresh.transformers import (
        RelevantFeatureAugmenter)
    
  2. 加载在技术 要求 部分中描述的占用检测数据集:

     X = pd.read_csv("occupancy.csv", parse_dates=["date"])
    y = pd.read_csv(
        "occupancy_target.csv",
        index_col="id")["occupancy"]
    
  3. 创建一个空的 DataFrame,其中包含目标变量的索引:

     tmp = pd.DataFrame(index=y.index)
    
  4. 现在,让我们将 步骤 3 和目标从步骤 2 分成训练集和 测试集:

     X_train, X_test, y_train, y_test = train_test_split(
        tmp, y, random_state=0)
    

注意

<st c="51548">X_train</st> <st c="51561">X_test</st> 将用作容器来存储由 <st c="51628">tsfresh</st> 创建的特征。它们对于 <st c="51678">RelevantFe</st><st c="51688">atureAugmenter()</st> 的功能是必需的,我们将在接下来的步骤中讨论。

  1. 让我们创建一个字典,指定从每个时间序列中提取的特征(我任意定义了以下 特征):

     kind_to_fc_parameters = {
        "light": {
            "c3": [{"lag": 3}, {"lag": 2}, {"lag": 1}],
            «abs_energy": None,
            «sum_values": None,
            «fft_coefficient": [
                {«attr": "real", "coeff": 0},
                {«attr": "abs", "coeff": 0}],
            «spkt_welch_density": [
                {«coeff": 2}, {"coeff":5}, {"coeff": 8}
            ],
            «agg_linear_trend": [
                {«attr": "intercept",
                „chunk_len": 50, „f_agg": „var"},
                {"attr": "slope",
                «chunk_len": 50, "f_agg":"var"},
            ],
            «change_quantiles": [
                {«f_agg": "var", "isabs": False,
                «qh": 1.0,"ql": 0.8},
                {«f_agg": "var", "isabs": True,
                «qh": 1.0,"ql": 0.8},
            ],
        },
    "co2": {
        «fft_coefficient": [
            {«attr": "real", "coeff": 0},
            {«attr": "abs", "coeff": 0}],
        "c3": [{"lag": 3}, {"lag": 2}, {"lag": 1}],
        «sum_values": None,
        «abs_energy": None,
        «sum_of_reoccurring_data_points": None,
        «sum_of_reoccurring_values": None,
        },
    "temperature": {"c3": [{"lag": 1},
        {«lag»: 2},{«lag»: 3}], «abs_energy": None},
    }
    

    我们在《从不同时间序列中提取不同特征的方法》中讨论了该字典的参数。

  2. 让我们设置<st c="52885">RelevantFeatureAugmenter()</st>,它是对<st c="52943">extract_relevant_features</st>函数的包装,以创建在第 5 步中指定的特征:

     augmenter = RelevantFeatureAugmenter(
        column_id="id",
        column_sort="date",
        kind_to_fc_parameters=kind_to_fc_parameters,
    )
    

注意

要创建所有可能的特征,请在第 6 步中使用<st c="53191">FeatureAugmenter()</st>类。

  1. 让我们将第 6 步中的特征创建实例与一个逻辑回归模型结合到一个 scikit-learn 流水线中:

     pipe = Pipeline(
        [
            ("augmenter", augmenter),
            («classifier», LogisticRegression(
        random_state=10, C=0.01)),
        ]
    )
    
  2. 现在,让我们告诉<st c="53479">RelevantFeatureAugmenter()</st>它需要使用哪个数据集来创建特征:

     pipe.set_params(augmenter__timeseries_container=X)
    
  3. 让我们拟合流水线,这将触发特征创建过程,然后是逻辑回归模型的训练:

     pipe.fit(X_train, y_train)
    
  4. 现在让我们使用测试集中的时间序列来获取预测,并通过以下分类报告来评估模型的性能:

     print(classification_report(
        y_test, pipe.predict(X_test)))
    

    我们可以看到第 10 步的输出如下:

     <st c="54007">precision     recall  f1-score   support</st>
     <st c="54041">0         1.00        0.96        0.98</st> <st c="54058">28</st>
     <st c="54061">1         0.86        1.00        0.92            6</st>
     <st c="54080">accuracy                                       0.97           34</st>
    **<st c="54097">macro avg         0.93        0.98        0.95           34</st>**
    **<st c="54125">weighted avg         0.97        0.97        0.97           34</st>**
    

分类报告的值表明,提取的特征适合预测在任何给定小时办公室是否被占用

它是如何工作的...

在这个方法中,我们将从时间序列中创建特征与<st c="54398">tsfresh</st>结合,并在一条流水线上训练来自 scikit-learn 库的机器学习算法。

<st c="54500">tsfresh</st>库包括两个包装类,围绕其主函数,以使特征创建过程与 scikit-learn 流水线兼容。在这个方法中,我们使用了<st c="54679">RelevantFeatureAugmenter()</st>类,它包装了<st c="54729">extract_relevant_features</st>函数,用于从时间序列中创建和选择特征。

<st c="54818">RelevantFeatureAugmenter()</st> 的工作原理如下;使用 <st c="54869">fit()</st>,它通过使用 <st c="54917">extract_relevant_features</st>来创建和选择特征。所选特征的名称随后存储在 transformer 内部。 使用 <st c="55031">transform()</st> <st c="55044">RelevantFeatureAugmenter()</st> 从时间序列中创建所选特征。

<st c="55122">我们通过传递一个包含我们想要创建的特征的字典到其</st> RelevantFeatureAugmenter()<st c="55190">的</st>kind_to_fc_parameters<st c="55281">参数中,覆盖了</st>RelevantFeatureAugmenter()<st c="55190">的默认</st>功能。</st> <st c="55293">因此,使用</st> transform()<st c="55320">,</st> RelevantFeatureAugmenter()` 从时间序列中创建了指定的特征。

<st c="55401">为了从时间序列创建所有特征,</st> tsfresh<st c="55454">包含了</st>FeatureAugmenter()<st c="55486">类,它具有与</st>RelevantFeatureAugmenter()<st c="55556">相同的函数,但没有特征</st> 选择步骤`。

<st c="55597">RelevantFeatureAugmenter()</st> <st c="55629">FeatureAugmenter()</st> 都需要两个 DataFrame 来工作。 第一个 DataFrame 包含时间序列数据和唯一标识符(我们在 步骤 2中加载了这个 DataFrame)。 第二个 DataFrame 应该是空的,并包含唯一标识符 在其索引中 (我们在 步骤 3中创建了此 DataFrame)。 特征是从包含时间序列的第一个 DataFrame 中提取的(在应用 <st c="56011">transform()</st>时)并随后添加到第二个 DataFrame 中,然后用于训练逻辑回归或获取 <st c="56135">其预测</st>

<st c="56151">注意</st>

空 DataFrame 的索引被 <st c="56201">RelevantFeatureAugmenter()</st><st c="56232">FeatureAugmenter()</st> 用于识别从中提取特征的时间序列。因此,当传递 <st c="56356">X_train</st> 并应用 <st c="56336">fit()</st> 时,从其 <st c="56412">id</st> 值在训练集中的时间序列中提取了特征。之后,通过观察使用测试集做出的预测来评估模型,这触发了从其 <st c="56596">id</st> 值在 <st c="56612">X_test</st>》中的时间序列中创建特征的过程。

当我们在管道上使用 <st c="56633">fit()时,我们从原始时间序列中创建了特征,并使用这些特征训练了一个逻辑回归模型。使用predict()</st> 方法,我们从测试集中创建了特征,并基于这些特征获得了逻辑回归的预测结果。

参见:

关于本食谱中使用的课程和程序的更多详细信息,请访问以下链接:链接 链接 链接

  • <st c="57027">tsfresh</st> 文档:文档

  • 一个包含演示的 Jupyter 笔记本:演示

第十二章:11

从文本变量中提取特征

文本可以是我们的数据集中的一个变量。 例如,在保险领域,描述事故情况的详细信息可能来自表格中的自由文本字段。 如果一家公司收集客户评价,这些信息将以用户提供的短文本片段的形式收集。 文本数据不显示 表格 模式,这是我们在这本书中一直处理的数据集。 相反,文本中的信息在长度、内容和写作风格上可能有所不同。 我们可以从文本变量中提取大量信息,用作机器学习模型中的预测特征。 本章将涵盖的技术属于 自然语言处理 (NLP)。 自然语言处理是语言学和计算机科学的一个子领域。 它关注计算机和人类语言之间的交互,换句话说,就是如何编程让计算机理解人类语言。 自然语言处理包括多种技术来理解文本的句法、语义和语篇。 因此,要公正地对待这个领域,就需要一本 完整的书。

在本章中,我们将讨论允许我们快速从短文本片段中提取特征以补充我们的预测模型的方法。 具体来说,我们将讨论如何通过查看文本的一些统计参数来捕捉文本的复杂性,例如单词长度和计数、使用的单词和唯一单词的数量、句子数量等。 我们将使用 <st c="1537">pandas</st> 和</st c="1548">scikit-learn` 库,并且我们将简要介绍一个非常有用的 Python NLP 工具包,称为 自然语言 工具包 (NLTK)。

本章包括以下食谱:

  • 统计字符、单词和词汇

  • 通过统计句子来估计文本的复杂性

  • 使用词袋模型和 n-gram 创建特征

  • 实现词频-逆文档频率

  • 清理和词干提取文本变量

技术要求

在本章中,我们将使用pandasmatplotlibscikit-learn Python 库。我们还将使用NLTK,这是一个用于 NLP 和文本分析的综合性 Python 库。您可以在http://www.nltk.org/install.html找到安装NLTK的说明。

如果您使用的是 Python Anaconda 发行版,请按照说明在https://anaconda.org/anaconda/nltk安装NLTK

在您安装了NLTK之后,打开一个 Python 控制台并执行以下操作:

 import nltk
nltk.download('punkt')
nltk.download('stopwords')

这些命令将为您下载运行本章中菜谱所需的数据。

注意

如果您尚未下载这些或其他对NLTK功能必要的源数据,NLTK将引发错误。仔细阅读错误消息,因为它将指导您下载运行您尝试执行的命令所需的数据。

计算字符、单词和词汇

文本的一个显著特征是其复杂性。长描述比短描述更有可能包含更多信息。包含不同、独特词汇的文本比反复重复相同词汇的文本更可能包含更丰富的细节。同样,当我们说话时,我们使用许多短词,如冠词和介词来构建句子结构,而主要概念通常来自我们使用的名词和形容词,这些往往是较长的词。所以,正如您所看到的,即使不阅读文本,我们也可以通过确定单词数量、唯一单词数量(单词的非重复出现)、词汇多样性和这些单词的长度来开始推断文本提供的信息量。在本菜谱中,我们将学习如何使用pandas从文本变量中提取这些特征。

准备工作

我们将使用 scikit-learn,它包含大约 18,000 篇关于 20 个不同主题的新闻帖子。 有关此数据集的更多详细信息可以在以下网站上找到:

在深入菜谱之前,让我们讨论一下我们将从这些 文本 片段 中提取的特征。 我们提到,较长的描述、文章中的更多单词、更多唯一单词的多样性以及更长的单词往往与文章提供的信息量相关。 因此,我们可以通过提取以下关于 文本 的信息来捕捉文本复杂性:

  • 字符总数

  • 单词总数

  • 唯一单词的总数

  • 词汇多样性(总单词数除以唯一单词数)

  • 单词平均长度(字符数除以单词数)

在这个菜谱中,我们将使用 <st c="4932">pandas</st>来提取这些数值特征,它具有广泛的字符串处理功能,可以通过 <st c="5023">str</st> 向量化的字符串函数 来访问 序列。

如何做到这一点...

让我们首先加载 <st c="5106">pandas</st> 并准备好 数据集:

  1. Load <st c="5149">pandas</st> and the dataset from <st c="5177">scikit-learn</st>:

     import pandas as pd
    from sklearn.datasets import fetch_20newsgroups
    
  2. Let’s load the train set part of the 20 Newsgroup dataset into a <st c="5326">pandas</st> DataFrame:

     data = fetch_20newsgroups(subset='train')
    df = pd.DataFrame(data.data, columns=['text'])
    

提示

您可以通过执行print(df['text'][1])来打印 DataFrame 中的文本示例。更改[ <st c="5553">]<st c="5559">之间的数字以显示不同的文本。</st>注意,每个文本描述都是一个由字母、数字、标点和空格组成的单个字符串。 <st c="5694">您可以通过执行type(df["text"][1])`来检查数据类型。

现在我们已经将文本变量放入了一个pandas DataFrame 中,我们就可以准备提取特征了。

  1. 让我们在新列中捕获每个文本片段中的字符数:

     df['num_char'] = df['text'].str.len()
    

提示

您可以在计数字符数之前,通过在len()方法之前添加strip()方法来移除字符串末尾的空白字符,包括换行符,如下所示:df['num_char'] = df['text'].str.strip().str.len()

  1. 让我们在新列中捕获每个文本中的单词数:

     df['num_words'] = df['text'].str.split().str.len()
    

    要计算单词数,我们使用pandas库的split()方法,该方法在空白处分割文本。通过执行,例如,split()来查看split()的输出,df["text"].loc[1].split()以分离 DataFrame 中第二个文本的单词。

  2. 让我们在新列中捕获每个文本中的唯一单词数:

     df['num_vocab']df[
        'text'].str.lower().str.split().apply(
            set).str.len()
    

注意

如果一个单词有一个大写字母,Python 会将其解释为两个不同的单词。为了避免这种行为,我们可以在split()方法之前应用lower()方法。

  1. 让我们创建一个捕获词汇多样性的特征——也就是说,总单词数(步骤 4)与唯一单词数(步骤 5)的比较:

     df['lexical_div'] = df['num_words'] / df['num_vocab']
    
  2. 让我们通过将字符数(步骤 3)除以单词数(步骤 4)来计算平均单词长度:

     df['ave_word_length'] = df[
        'num_char'] / df['num_words']
    

    如果我们执行 <st c="7292">df.head()</st>,我们将看到包含文本和刚刚 建的特征的 前五行数据:

图 11.1 – 包含文本变量和总结文本特征的 DataFrame

图 11.1 – 包含文本变量和总结文本特征的 DataFrame

有了这些,我们已经提取了五个不同的特征来捕捉文本复杂性,我们可以将这些特征用作 我们的机器 学习 算法的 输入。

注意

在这个菜谱中,我们从原始数据中直接创建了新特征,而没有进行任何数据清理、删除标点符号,甚至没有进行词干提取。 请注意,这些是在大多数标准 NLP 程序之前执行的步骤。 要了解更多信息,请访问本章末尾的 清洗和词干提取文本变量 菜谱。

它是如何工作的...

在这个菜谱中,我们创建了 五个新的特征,通过利用 pandas 的 <st c="8578">str</st> 来访问内置的 <st c="8605">pandas</st> 功能来处理字符串。 我们处理了 20 个新闻组数据集的 <st c="8686">train</st> 子集的文本列,该数据集包含在 <st c="8743">scikit-learn</st>中。 该数据集中的每一行都由一个包含文本的字符串组成。

我们使用了 pandas 的 <st c="8833">str</st>,然后是 <st c="8850">len()</st>,来计算每个字符串中的字符数——即字母、数字、符号和空格的总数。 我们还结合了 <st c="8993">str.len()</st> <st c="9008">str.strip()</st> 来移除字符串开头和结尾的空白字符以及换行符中的空白字符,在计算字符数之前。

要计算单词数,我们使用了 pandas 的 <st c="9197">str</st>,然后是 <st c="9214">split()</st>,将字符串分割成单词列表。 <st c="9270">split()</st> 方法通过在单词之间的空白处断开字符串来创建单词列表。 接下来,我们使用 <st c="9401">str.len()</st>来计算这些单词,得到每个字符串的单词数。

注意

我们可以通过传递一个字符串或字符来改变 <st c="9489">str.split()</st> 的行为,我们希望用它来分割字符串。 例如, <st c="9594">df['text'].str.split(';')</st> <st c="9659">;</st>的每个出现处分割字符串。

为了确定唯一单词的数量,我们使用了 pandas 的 <st c="9719">str.split()</st> 函数将字符串分割成单词列表。 接下来,我们在 pandas 的 <st c="9848">apply()</st> 方法中应用了内置的 Python <st c="9820">set()</st> 方法,以返回一组单词。 记住,集合包含 唯一出现 的元素列表中的元素 – 那就是唯一单词。 接下来,我们使用 pandas 的 <st c="10023">str.len()</st> 函数来计数这些单词,并返回 <st c="10253">lower()</st> 函数,在分割字符串并计数唯一单词数量之前将所有字符转换为小写。

为了创建 词汇 多样性和平均单词长度 特征, 我们 只是执行了两个 <st c="10495">pandas</st> 序列的向量化除法。 就是这样;我们创建了五个新的特征,其中包含有关文本复杂性的信息。

还有更多...

我们可以通过使用可视化来检查数据集中每个 20 个不同新闻主题中提取的文本特征的分布。

要创建新创建特征的直方图,在运行本食谱中 如何工作... 部分的全部步骤之后,遵循以下步骤:

  1. 导入 <st c="10936">matplotlib</st>:

     import matplotlib.pyplot as plt
    
  2. 将带有新闻主题的目标添加到 20 Newsgroup DataFrame:

     df['target'] = data.target
    
  3. 创建一个函数,用于显示每个新闻主题所选特征的直方图:

     def plot_features(df, text_var):
        nb_rows = 5
        nb_cols = 4
        fig, axs = plt.subplots(
            nb_rows, nb_cols,figsize=(12, 12))
        plt.subplots_adjust(wspace=None, hspace=0.4)
        n = 0
        for i in range(0, nb_rows):
            for j in range(0, nb_cols):
                axs[i, j].hist(
                    df[df.target==n][text_var], bins=30)
                axs[i, j].set_title(
                    text_var + ' | ' + str(n))
                     n += 1
        plt.show()
    
  4. 为单词特征运行该函数 的数量 次:

     plot_features(df, 'num_words')
    

    前面的命令返回以下图表,其中你可以看到每个 20 个新闻主题中单词数量的分布,编号从 0 到 19,在 图表标题中:

图 11.2 – 每个文本单词数量的直方图,按每个文本中讨论的主题进行细分

图 11.2 – 每个文本单词数量的直方图,按每个文本中讨论的主题进行细分

单词数量 在不同新闻主题中 显示出不同的 分布。因此,这个特征在分类算法中预测文本的主题时可能很有用。

另请参阅

要了解更多关于 pandas 内置字符串处理功能的信息,请访问 https://pandas.pydata.org/pandas-docs/stable/user_guide/text.html#method-summary

通过计算句子数量来估计文本复杂度

我们可以捕获的文本的一个方面是其复杂性。 通常,包含多个句子并分布在几个段落中的较长的描述,比包含非常少句子的描述提供的信息更多。 因此,捕获句子的数量可能有助于了解文本提供的信息量。 这个过程被称为 <st c="14027">NLTK</st> Python 库,它提供了这个功能。

准备工作

在这个示例中,我们将使用 <st c="14130">NLTK</st> Python 库。 有关如何安装 <st c="14184">NLTK</st>的指南,请参阅本章的 技术要求部分

如何操作...

让我们首先导入所需的库和数据集:

  1. 让我们加载 <st c="14341">pandas</st>,从 <st c="14377">NLTK</st>中获取句子分词器,以及来自 <st c="14399">scikit-learn</st>的数据集

     import pandas as pd
    from nltk.tokenize import sent_tokenize
    from sklearn.datasets import fetch_20newsgroups
    
  2. 为了理解 <st c="14590">NLTK</st>中的句子分词器的功能,让我们创建一个包含多个句子的字符串变量:

     text = """
    The alarm rang at 7 in the morning as it usually did on Tuesdays. She rolled over, stretched her arm, and stumbled to the button till she finally managed to switch it off. Reluctantly, she got up and went for a shower. The water was cold as the day before the engineers did not manage to get the boiler working. Good thing it was still summer. Upstairs, her cat waited eagerly for his morning snack. Miaow! He voiced with excitement as he saw her climb the stairs. """
    
  3. 现在,让我们 使用 <st c="15185">步骤 2</st> 中的字符串,通过 <st c="15213">NLTK</st> 库的 句子分词器 将其分割成句子:

     sent_tokenize(text)
    

提示

如果你在 <st c="15301">步骤 3</st>中遇到错误,请仔细阅读错误消息,并下载 <st c="15383">NLTK</st>所需的所需数据源,如错误消息中所述。 有关更多详细信息,请查看 技术 要求 部分。

句子分词器返回以下输出中显示的句子列表:

<st c="15571">['\nThe alarm rang at 7 in the morning as it usually did on Tuesdays.',</st>
 <st c="15643">'She rolled over,\nstretched her arm, and stumbled to the button till she finally managed to switch it off.',</st>
 <st c="15753">'Reluctantly, she got up and went for a shower.',</st>
 <st c="15803">'The water was cold as the day before the engineers\ndid not manage to get the boiler working.',</st>
 <st c="15900">'Good thing it was still summer.',</st>
 <st c="15935">'Upstairs, her cat waited eagerly for his morning snack.',</st>
 <st c="15994">'Miaow!',</st>
 <st c="16004">'He voiced with excitement\nas he saw her climb the stairs.']</st>

<st c="16066">注意</st>

跟随字母的转义字符,<st c="16117">\n</st>,表示一个新行。

  1. <st c="16143">让我们计算文本变量中的句子数量:</st>

     len(sent_tokenize(text))
    

    之前的命令返回了 <st c="16257">8</st>,这是我们文本变量中的句子数量。 现在,让我们确定整个 DataFrame 中的句子数量。

  2. <st c="16383">让我们将 20 个新闻组数据集的 训练 <st c="16404">子集加载到 pandas DataFrame 中:</st>

     data = fetch_20newsgroups(subset='train')
    df = pd.DataFrame(data.data, columns=['text'])
    
  3. 为了加快以下步骤,我们只需处理 DataFrame 的前 <st c="16620">10</st> `行:

     df = df.loc[1:10]
    
  4. 让我们也删除文本的第一部分,这部分包含有关电子邮件发送者、主题和其他我们不感兴趣的信息。 大部分信息都在单词 <st c="16866">Lines</st> 之后,后面跟着 <st c="16884">:</st>,所以让我们在 <st c="16916">Lines:</st> 处拆分字符串,并捕获字符串的第二部分:

     df['text'] = df['text'].str.split('Lines:').apply(
        lambda x: x[1])
    
  5. `最终,让我们创建一个包含每段文本中句子数量的变量:

     df['num_sent'] = df['text'].apply(
        sent_tokenize).apply(len)
    

    <st c="17174">使用</st> <st c="17184">df</st> <st c="17186">命令,您可以显示包含 text 变量和包含每段文本中句子数量的新特征的整个 DataFrame:`

图 11.3 – 包含文本变量和每段文本中句子数量的 DataFrame

图 11.3 – 包含文本变量和每段文本中句子数量的 DataFrame

<st c="17926">现在,我们可以将这个新功能作为输入提供给</st> 机器学习算法。

<st c="18000">它的工作原理...</st>

在这个示例中,我们使用 <st c="18086">sent_tokenizer</st><st c="18110">NLTK</st> <st c="18114">库</st> 中将包含文本的字符串分割成句子。<st c="18124">sent_tokenizer</st> 已经预先训练以识别大写字母和不同类型的标点符号,这些符号标志着句子的开始和结束。

首先,我们将 <st c="18294">sent_tokenizer</st> 应用到一个手动创建的字符串上,以便熟悉其功能。分词器将文本分割成八个句子的列表。我们结合了分词器和内置的 Python <st c="18495">len()</st> 方法来计算字符串中的句子数量。

接下来,我们加载了一个包含文本的数据集,为了加快计算速度,我们只保留了 DataFrame 的前 10 行,使用 pandas 的 <st c="18692">loc[]</st> 函数。接下来,我们移除了文本的第一部分,这部分包含了关于电子邮件发送者和主题的信息。为此,我们使用 pandas 的 <st c="18873">str.split("Lines:")</st> 函数在 <st c="18852">Lines:</st> 处分割字符串,该函数返回一个包含两个元素的列表:<st c="18973">Lines:</st> 前后的字符串。通过在 <st c="19016">apply()</st> 中使用 lambda 函数,我们保留了文本的第二部分——即列表中由 <st c="19118">split()</st> 返回的第二个字符串。

最后,我们使用 pandas 的 <st c="19207">apply()</st> 方法将 <st c="19126">sent_tokenizer</st> 应用到 DataFrame 的每一行上,将字符串分割成句子,然后使用内置的 Python <st c="19300">len()</st> 方法对句子列表进行操作,以返回每个字符串的句子数量。这样,我们创建了一个包含每个文本句子数量的新特征。

There’s more...

<st c="19485">NLTK</st> <st c="19490">具有单词分词等有用功能,我们可以使用这些功能而不是 pandas来计数和返回单词数量。</st>您可以在以下链接中了解更多关于 <st c="19666">NLTK</st> <st c="19670">功能的信息:</st>

  • 《Python 3 Text Processing with NLTK 3 Cookbook》,作者 Jacob Perkkins,Packt Publishing

  • The <st c="19781">NLTK</st> 文档 http://www.nltk.org/

使用词袋和 n-gram 创建特征

A 词袋 (BoW) 是一种简化的文本表示,它捕捉了文本中存在的单词以及每个单词在文本中出现的次数。 所以,对于文本字符串Dogs like cats, but cats do not like dogs,得到的 BoW 如下: 如下:

图 11.4 – 从句子“Dogs like cats, but cats do not like dogs”派生的 BoW

图 11.4 – 从句子“Dogs like cats, but cats do not”派生的 BoWlike dogs

在这里,每个单词都成为一个变量,而这个变量的值表示该单词在字符串中出现的次数。 正如你所看到的,BoW 捕捉了多重性,但并不保留单词顺序或语法。 这就是为什么它是一种简单但有用的提取特征和捕捉我们所处理文本的一些信息的方法。 这就是为什么它是一种简单但有用的提取特征和捕捉我们所处理文本的一些信息的方法。

为了捕捉一些语法,BoW 可以与n-gram一起使用。n-gram 是一个在给定文本中连续的n项的序列。 继续使用句子Dogs like cats, but cats do not like dogs,得到的 2-gram 如下: 如下:

  • Dogs like

  • like cats

  • cats but

  • 但是做

  • do not

  • like dogs

我们可以与一个 BoW(词袋)一起创建一个 n-gram 词袋,其中额外的变量由 2-gram 提供,每个 2-gram 的值是它们在每个字符串中出现的次数;在这个例子中,值是 1。 因此,我们的最终 BoW(词袋)包含 2-gram 将看起来像这样: 如下:

图 11.5 – 包含 2-gram 的 BoW

图 11.5 – 包含 2-gram 的 BoW

在这个菜谱中,我们将学习如何使用<st c="21419">scikit-learn</st>创建带有或不带 n-gram 的 BoW。

准备就绪

在跳入这个 食谱 之前,让我们熟悉一些 BoW 的参数,以便我们可以调整以使 BoW 更全面。 在创建多个文本的 BoW 时,对于我们在分析的文本片段中至少出现一次的每个独特单词,都会创建一个新特征。 任何 一个 文本片段中出现的单词,如果它只出现在一个文本片段中,那么它在该特定文本中的值将为 1,而在其他所有文本中的值为 0。 因此,BoWs 往往是稀疏矩阵,其中大部分值 都是零。

如果我们处理的是巨大的文本语料库,那么 BoW 中的列数——即单词数——可能相当大,如果我们还包括 n-gram,那么列数会更大。 为了限制列数和返回矩阵的稀疏性,我们可以保留在多个文本中出现的单词;或者换句话说,我们可以保留至少在某个百分比 的文本中出现的单词。

为了减少 BoW 的列数和稀疏性,我们还应该使用与单词相同的 大小写 ——例如,小写——因为 Python 将不同大小写的单词识别为不同的单词。 我们还可以通过 删除 停用词来减少列数和稀疏性。停用词是使用频率非常高的单词,使句子流畅,但本身并不携带任何有用的信息。 停用词的例子包括代词,如我、你和他,以及介词和冠词。

在这个食谱中,我们将学习如何设置单词为小写,删除停用词,保留具有最低可接受频率的单词,并使用来自 <st c="23042">scikit-learn</st>` 的单个转换器 CountVectorizer() 一起捕获 n-gram:

如何做...

让我们首先加载必要的库并准备好 数据集:

  1. 加载 <st c="23175">pandas</st><st c="23183">CountVectorizer</st>,以及来自 <st c="23221">scikit-learn</st>` 的数据集

     import pandas as pd
    from sklearn.datasets import fetch_20newsgroups
    from sklearn.feature_extraction.text import ( CountVectorizer
    )
    
  2. 让我们将 20 个新闻组数据集的 训练集部分 加载到一个 pandas DataFrame 中:

     data = fetch_20newsgroups(subset='train')
    df = pd.DataFrame(data.data, columns=['text'])
    
  3. 为了使结果更容易解释,让我们从 文本变量 中删除标点符号和数字:

     df['text'] = df['text'].str.replace(
        ‹[^\w\s]›,››, regex=True).str.replace(
        ‹\d+›,››, regex=True)
    

注意

要了解更多关于 Python 中的正则表达式的信息,请点击此 链接: https://docs.python.org/3/howto/regex.html

  1. 现在,让我们 设置 <st c="23864">CountVectorizer()</st> ,以便在创建 BoW 之前,它将文本转换为小写,删除停用词,并保留至少出现在 5%的 文本片段中的单词:

     vectorizer = CountVectorizer(
        lowercase=True,
        stop_words='english',
        ngram_range=(1, 1),
        min_df=0.05)
    

注意

为了将 n-gram 作为返回列的一部分引入,我们可以将<st c="24221">ngrams_range</st> 的值更改为,例如,<st c="24251">(1,2)</st>。这个元组提供了不同 n-gram 的 n 值范围的上下边界。 <st c="24371">(1,2)</st>的情况下,<st c="24378">CountVectorizer()</st> 将返回单个单词和两个连续单词的数组。

  1. 让我们拟合<st c="24468">CountVectorizer()</st> ,以便它学习在 BoW 中应该使用哪些单词:

     vectorizer.fit(df['text'])
    
  2. 现在,让我们创建 BoW:

     X = vectorizer.transform(df['text'])
    
  3. 最后,让我们将 BoW 捕获到一个具有相应 特征名称 的 DataFrame 中:

     bagofwords = pd.DataFrame(
        X.toarray(),
        columns = vectorizer.get_feature_names_out()
    )
    

    有了 我们 已经 创建 了一个包含单词作为列和每个文本中它们出现的次数作为值的<st c="24835">pandas</st> DataFrame。 您可以通过执行<st c="24986">bagofwords.head()</st>来查看结果:

图 11.6 – 由 20 个新闻组数据集生成的 BoW 的 DataFrame

图 11.6 – 由 20 个新闻组数据集生成的 BoW 的 DataFrame

我们可以使用 这个 BoW 作为机器 学习模型的输入。

它是如何工作的...

scikit-learn 的<st c="25536">CountVectorizer()</st> 将一组文本文档转换为标记计数的矩阵。 这些标记可以是单个单词或两个或更多连续单词的数组——即 n-gram。 在这个配方中,我们从一个 DataFrame 中的文本变量创建了一个 BoW。

我们从<st c="25843">scikit-l</st><st c="25851">earn</st> 加载了 20 个新闻组的文本数据集,并使用 pandas 的<st c="25926">replace()</st> 函数从文本行中删除了标点符号和数字,该函数可以通过 pandas 的<st c="25984">str</st> 模块访问,以替换数字,<st c="26015">'\d+'</st>,或符号,<st c="26034">'[^\w\s]'</st>,为空字符串,<st c="26065">''</st>。然后,我们使用了<st c="26083">CountVectorizer()</st> 来创建 BoW。 我们将<st c="26131">lowercase</st> 参数设置为<st c="26154">True</st> ,在提取 BoW 之前将单词转换为小写。 我们将<st c="26227">stop_words</st> 参数设置为<st c="26250">english</st> ,以忽略停用词——也就是说,避免 BoW 中的停用词。 我们将<st c="26329">ngram_range</st> 设置为<st c="26348">(1,1)</st> 元组,以仅返回作为列的单个单词。 最后,我们将<st c="26416">min_df</st> 设置为<st c="26426">0.05</st> ,以返回至少出现在 5%的文本中的单词,换句话说,在 DataFrame 中的 5%的行中。

设置 变压器 之后,我们使用了<st c="26592">fit()</st> 方法来允许变压器找到满足先前标准的单词。 最后,使用<st c="26704">transform()</st> 方法,变压器返回了一个包含具有其特征名称的 BoW 的对象,我们将它捕获在一个<st c="26826">pandas</st> DataFrame中。

另请参阅

有关<st c="26877">CountVectorizer()</st> 的更多详细信息,请访问 <st c="26906">scikit-learn</st> 库的文档 https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html

实现词频-逆文档频率

词频-逆文档频率(TF-IDF) 是一个数值统计量,它捕捉了在考虑整个文档集合的情况下,一个词在文档中的相关性。这是什么意思?一些词在文本文档中以及跨文档中都会出现很多次,例如英语单词 theais,例如。 这些词通常对文档的实际内容传达的信息很少,并且不会使文本脱颖而出。TF-IDF 提供了一种方法,通过考虑一个词在文档中出现的次数以及它在文档中出现的频率来 权衡 该词的重要性。因此,像 theais 这样的常用词将具有较低的权重,而像 leopard 这样更具体于某个主题的词将具有更高的权重。

TF-IDF 是两个统计量的乘积词频tf)和逆文档频率idf),表示如下:tf-idf = tf × idf。tf 在其最简单形式中,是一个词在单个文本中的计数。因此,对于术语 t,tf 的计算为 tf(t) = count(t),并且基于文本进行确定。idf 是衡量一个词在 所有 文档中的普遍性的度量,通常在对数尺度上计算。以下是一个常见的实现方法:

idft=log(n1+df(t))

在这里, n 是文档的总数, df(t) 是包含术语 t 的文档数量。 df(t)的值越大,该术语的权重越低。 如果一个词在文本中频繁出现(高 tf),或者在不同文本中很少出现( idf),那么这个词的重要性就会很高。

注意

TF-IDF 可以与 n-gram 一起使用。 同样,为了权衡一个 n-gram,我们将某个文档中的 n-gram 频率与文档间 n-gram 的频率相乘。

在这个菜谱中,我们将学习如何使用scikit-learn 提取特征,使用或不使用 n-gram。

准备就绪

<st c="29031">scikit-learn</st> 使用一种稍微不同的方式来计算 IDF 统计量:

<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" display="block">mml:mii</mml:mi>mml:mid</mml:mi>mml:mif</mml:mi><mml:mfenced separators="|">mml:mrowmml:mit</mml:mi></mml:mrow></mml:mfenced>mml:mo=</mml:mo>mml:mrowmml:mrow<mml:mi mathvariant="normal">log</mml:mi></mml:mrow>mml:mo⁡</mml:mo>mml:mrow<mml:mfenced separators="|">mml:mrowmml:mfracmml:mrowmml:mn1</mml:mn>mml:mo+</mml:mo>mml:min</mml:mi></mml:mrow>mml:mrowmml:mn1</mml:mn>mml:mo+</mml:mo>mml:mid</mml:mi>mml:mif</mml:mi>mml:mo(</mml:mo>mml:mit</mml:mi>mml:mo)</mml:mo></mml:mrow></mml:mfrac></mml:mrow></mml:mfenced></mml:mrow></mml:mrow>mml:mo+</mml:mo>mml:mn1</mml:mn></mml:math>

这个公式确保了出现在所有文本中的词都得到最低的权重 1。 此外,在计算每个词的 TF-IDF 之后, <st c="29288">scikit-learn</st> 将(包含所有词的)特征向量归一化到其欧几里得范数。 有关确切公式的更多详细信息,请访问 <st c="29430">scikit-learn</st> 文档 https://scikit-learn.org/stable/modules/feature_extraction.html#tfidf-term-weighting

TF-IDF 在创建术语矩阵时具有与 BoW 相同的特征——即,高特征空间和稀疏性。 为了减少特征数量和稀疏性,我们可以删除停用词,将字符转换为小写,并保留在至少 观察值中出现的单词的最低百分比。 如果您不熟悉这些术语,请访问本章中的 使用词袋和 n-gram 创建特征 菜谱进行复习。

在这个菜谱中,我们将学习如何将单词转换为小写,删除停用词,保留具有最低可接受频率的单词,捕获 n-gram,然后使用来自 scikit-learn 的单个转换器 <st c="30217">TfidfVectorizer()</st>)返回单词的 TF-IDF 统计信息:

如何做到这一点...

让我们首先加载必要的库并准备 数据集:

  1. 加载 <st c="30335">pandas</st><st c="30343">TfidfVectorizer()</st>和来自 <st c="30378">scikit-learn</st>的数据集:

     import pandas as pd
    from sklearn.datasets import fetch_20newsgroups
    from sklearn.feature_extraction.text import (
        TfidfVectorizer
    )
    
  2. 让我们将 20 个新闻组数据集的训练集部分加载到一个 pandas DataFrame 中:

     data = fetch_20newsgroups(subset='train')
    df = pd.DataFrame(data.data, columns=['text'])
    
  3. 为了使结果更容易解释,让我们从 文本变量中删除标点符号和数字:

     df['text'] = df['text'].str.replace(
        ‹[^\w\s]›,››, regex=True).str.replace(
        '\d+','', regex=True)
    
  4. 现在,让我们设置 <st c="30921">TfidfVectorize</st><st c="30935">r()</st> <st c="30945">scikit-learn</st> 中,以便在创建 TF-IDF 度量之前,将所有文本转换为小写,删除停用词,并且 保留至少在 5%的 文本片段中出现的单词:

     vectorizer = TfidfVectorizer(
        lowercase=True,
        stop_words='english',
        ngram_range=(1, 1),
        min_df=0.05)
    

注意

为了将 n-gram 作为返回列的一部分引入,我们可以将 <st c="31306">ngrams_range</st> 的值更改为,例如, <st c="31336">(1,2)</st>。这个元组提供了不同 n-gram 的 n 值范围的上下边界。 <st c="31456">(1,2)</st>的情况下, <st c="31463">TfidfVectorizer()</st> 将返回单个单词和两个连续单词的数组作为列。

  1. 让我们拟合 <st c="31564">TfidfVectorizer()</st> ,以便它学习哪些单词应该作为 TF-IDF 矩阵的列引入,并确定 单词的 <st c="31691">idf</st>

     vectorizer.fit(df['text'])
    
  2. 现在,让我们创建 TF-IDF 矩阵:

     X = vectorizer.transform(df['text'])
    
  3. 最后,让我们将 TF-IDF 矩阵捕获到具有相应 特征名称的 DataFrame 中:

     tfidf = pd.DataFrame(
        X.toarray(),
        columns = vectorizer.get_feature_names_out()
    )
    

    有了这个,我们就创建了一个 <st c="32003">pandas</st> DataFrame,其中包含单词作为列,TF-IDF 作为值。 您可以通过执行 tfidf.head()来检查结果:

图 11.7 – 由 TF-IDF 得到的特征 DataFrame

图 11.7 – 由 TF-IDF 得到的特征 DataFrame

现在,我们可以使用这个频率 DataFrame 来训练学习模型。

它是如何工作的...

在这个菜谱中,我们通过使用 scikit-learn 的 <st c="33187">TfidfVectorizer()</st> 提取了至少出现在 5% 的文档中的单词的 TF-IDF 值。

我们从 <st c="33269">scikit-learn</st> 加载了 20 个新闻组文本数据集,然后使用 pandas 的 <st c="33356">replace()</st>从文本行中删除了标点符号和数字,这可以通过 pandas 的 <st c="33405">str</st>访问,以替换数字, <st c="33429">'\d+'</st>,或符号, <st c="33448">'[^\w\s]'</st>,用空字符串, <st c="33479">''</st>替换。 然后,我们使用了 <st c="33497">TfidfVectorizer()</st> 来为单词创建 TF-IDF 统计数据。 我们将 <st c="33565">lowercase</st> 参数设置为 <st c="33588">True</st> ,在计算之前将单词转换为小写。 我们将 <st c="33664">stop_words</st> 参数设置为 <st c="33687">english</st> ,以避免返回矩阵中的停用词。 我们将 <st c="33746">ngram_range</st> 设置为 <st c="33765">(1,1)</st> 元组,以返回单个单词作为特征。 最后,我们将 <st c="33833">min_df</st> 参数设置为 <st c="33852">0.05</st> ,以返回至少出现在 5% 的文本中或换句话说,在 5% 的 行中的单词。

在设置完转换器后,我们应用了 <st c="34001">fi</st><st c="34003">t()</st> 方法,让转换器找到最终项矩阵中要保留的单词。 使用 <st c="34098">transform()</st> 方法,转换器返回了一个包含单词及其 TF-IDF 值的对象,然后我们将其捕获在一个具有适当特征名称的 pandas DataFrame 中。 现在,我们可以在机器 学习算法中使用这些特征。

另请参阅

*有关 <st c="34365">TfidfVectorizer()</st> 的更多详细信息,请访问 scikit-learn 的文档:https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html

清理和词干提取文本变量

我们的数据集中的一些变量来自自由文本字段,这些字段是由用户手动 完成的。人们的写作风格不同,我们使用各种标点符号、大小写模式和动词变位来传达内容及其相关的情感。通过创建总结文本复杂度、关键词和文档中单词相关性的统计参数,我们可以从文本中提取(一些)信息,而无需费心阅读它。我们已经在本章前面的食谱中讨论了这些方法。然而,为了得出这些统计和聚合特征,我们首先应该清理文本变量。

文本清理或预处理包括标点符号删除、停用词消除、字符大小写设置和词干提取。 标点符号删除包括删除不是字母、数字或空格的字符;在某些情况下,我们也会删除数字。停用词消除是指删除在语言中用于句子结构和流畅性的常用词,但它们单独传达的信息很少或没有。停用词的例子包括英语中的冠词,如 theand* 和 a,以及代词,如 I, youand they,以及各种变位中的常用动词,如动词 to beto have,以及助动词 would、*and do

为了让计算机正确识别单词,也需要将所有单词设置为相同的格式,因为单词 Toytoy 由于第一个单词中的大写字母 T,会被计算机识别为不同的单词。

最后,为了专注于文本的 信息 ,我们不希望计算机在单词有不同的变形时将单词视为不同。因此,我们将使用词干提取作为预处理流程的一部分。词干提取是指将每个单词缩减到其根或基本形式,以便单词 playing, plays, 和 played 变成 play,本质上传达了相同或非常 相似的意义。

在这个菜谱中,我们将学习如何使用 pandas 和 <st c="36704">NLTK</st>. 移除标点符号和停用词,将单词转换为小写,并执行词干提取。

准备工作

我们将使用 <st c="36748">NLTK</st> 词干提取包来执行词干提取,它结合了不同的算法来从英语和其他语言中提取词干。每种方法都使用不同的算法来找到单词的 ,因此它们可能输出略微不同的结果。我建议您了解更多相关信息,尝试不同的方法,并选择适合您正在工作的项目的那个。 I recommend reading more about it, trying different methods, and choosing the one that serves the project you are working on.

关于 <st c="37135">NLTK</st>词干提取器 的更多信息可以在 https://www.nltk.org/api/nltk.stem.html. 找到。

如何做到这一点...

让我们首先加载必要的库并准备好数据集:

  1. 加载 <st c="37331">pandas</st>, <st c="37339">stopwords</st>, 和 <st c="37354">SnowballStemmer</st> <st c="37375">NLTK</st> 以及数据集从 <st c="37396">scikit-learn</st>:

     import pandas as pd
    from nltk.corpus import stopwords
    from nltk.stem.snowball import SnowballStemmer
    from sklearn.datasets import fetch_20newsgroups
    
  2. 让我们将 20 个新闻组数据集的训练集部分加载到一个 pandas DataFrame 中:

     data = fetch_20newsgroups(subset='train')
    df = pd.DataFrame(data.data, columns=['text'])
    

    现在,让我们开始进行文本清理。

注意

在执行这个菜谱中的每个命令后,通过执行例如 <st c="37888">print(df['text'][10])</st> 来打印一些示例文本,以便您可以可视化对文本所做的更改。 Go ahead and do it now, and then repeat the command after 每个步骤。

  1. 首先,让我们移除标点符号:

     df["text"] = df['text'].str.replace('[^\w\s]','')
    

小贴士

您还可以使用 Python 的内置 <st c="38191">string</st> 模块来删除标点符号。 首先,通过执行 <st c="38256">import string</st> 来导入模块,然后执行 <st c="38287">df['text'] =</st> <st c="38300">df['text'].str.replace('[{}]</st><st c="38328">'.format(string.punctuation), '')</st>

  1. 我们还可以 删除 数字字符,只留下字母, 如下所示:

     df['text'] = df['text'].str.replace(
        '\d+', '', regex=True)
    
  2. 现在,让我们将所有单词 转换为小写:

     df['text'] = df['text'].str.lower()
    

    现在,让我们开始移除 停用词的过程。

注意

步骤 6 可能会失败,如果您没有下载 NLTK 库的 <st c="38687">停用词</st>。请访问本章中的 技术要求 部分以获取更多详细信息。

  1. 让我们创建一个函数,该函数将字符串拆分为单词列表,移除停用词,并将剩余的单词重新连接成一个字符串:

     def remove_stopwords(text):
        stop = set(stopwords.words('english'))
        text = [word
        for word in text.split() if word not in stop]
        text = ‹ ‹.join(x for x in text)
        return text
    

注意

为了能够使用 <st c="39160">scikit-learn</st> 库的 <st c="39179">CountVectorizer()</st> <st c="39205">TfidfVectorizer()</st>,我们需要文本以字符串格式存在。 因此,在移除停用词后,我们需要将单词作为单个字符串返回。 我们将 NLTK 库的停用词列表转换为一个集合,因为集合比列表扫描更快。 这提高了 计算时间。

  1. 现在,让我们使用 步骤 6 中的函数来从 <st c="39572">text</st> 变量中移除停用词:

     df['text'] = df['text'].apply(remove_stopwords)
    

    如果您想知道哪些单词是 停用词s, 执行 <st c="39692">stopwords.words('english')</st>

    最后,让我们对我们的数据进行词干提取。 我们将使用 <st c="39775">SnowballStemmer</st> <st c="39796">NLTK</st> 进行此操作。

  2. 让我们为 <st c="39839">SnowballStemer</st> 创建一个针对 英语语言的实例:

     stemmer = SnowballStemmer("english")
    

提示

尝试在单个单词上使用词干提取器来查看其工作方式;例如,运行 <st c="39992">stemmer.stem('running')</st>。你应该看到 <st c="40032">run</st> 作为该命令的结果。 尝试 不同的单词!

  1. 让我们创建一个函数,该函数将字符串分割成单词列表,将 <st c="40163">stemmer</st> 应用于每个单词,并将还原的单词列表重新连接成字符串: 一个字符串:

     def stemm_words(text):
        text = [
            stemmer.stem(word) for word in text.split()
        ]
        text = ‹ ‹.join(x for x in text)
        return text
    
  2. 让我们使用 步骤 9 中的函数 来还原我们数据中的单词: 我们的数据:

     df['text'] = df['text'].apply(stemm_words)
    

    现在,我们的文本已经准备好根据字符和单词计数创建特征,以及创建 BoWs 或 TF-IDF 矩阵,正如本章前面的食谱中所述。 这一章。

    如果我们执行 <st c="40673">print(df['text'][10])</st>,我们将看到清理后的文本示例: 清理后:

    <st c="40738">irwincmptrclonestarorg irwin arnstein subject recommend duc summari what worth distribut usa expir sat may gmt organ computrac inc richardson tx keyword ducati gts much line line ducati gts model k clock run well paint bronzebrownorang fade leak bit oil pop st hard accel shop fix tran oil leak sold bike owner want think like k opinion pleas email thank would nice stabl mate beemer ill get jap bike call axi motor tuba irwin honk therefor computracrichardsontx irwincmptrclonestarorg dod r</st>
    

注意

如果你正在计算句子数,你需要在移除标点符号之前这样做,因为标点和 大写字母是定义每个句子边界的必要条件。

它是如何工作的...

在这个食谱中,我们从文本变量中移除了标点符号、数字和停用词,将单词转换为小写,最后将单词还原到词根。 我们使用 pandas 的 <st c="41653">replace()</st>从文本变量中移除了标点符号和数字,这可以通过 pandas 的 <st c="41702">str</st>访问,用于替换数字, <st c="41726">'\d+'</st>,或符号, <st c="41745">'[^\w\s]'</st>,为空字符串, <st c="41776">''</st>。或者,我们可以使用内置的 <st c="41810">punctuation</st> 模块从内置的 <st c="41847">string</st> 包中。

提示

运行 <st c="41871">string.punctuation</st> 在导入 <st c="41929">string</st> 后,在你的 Python 控制台中查看将被替换为空字符串的符号。 空字符串。

接下来,利用 pandas 的字符串 处理功能通过 <st c="42067">str</st>,我们使用 <st c="42118">lower()</st> 方法将所有单词转换为小写。 为了从文本中移除停用词,我们使用了来自 <st c="42182">stopwords</st> 模块的 <st c="42204">NLTK</st>,其中包含了一组频繁出现的单词列表——即停用词。 我们创建了一个函数,该函数接受一个字符串并将其使用 pandas 的 <st c="42388">str.split()</st>分解成单词列表,然后,使用列表推导式,我们遍历列表中的单词并保留非停用词。 最后,使用 <st c="42524">join()</st> 方法,我们将保留的单词重新连接成一个字符串。 我们使用 Python 内置的 <st c="42622">set()</st> 方法对 <st c="42644">NLTK</st> 停用词列表进行操作,以提高计算效率,因为遍历集合比遍历列表要快。 最后,使用 pandas 的 <st c="42778">apply()</st>,我们将该函数应用于我们的 文本数据的每一行。

提示

在导入 <st c="42849">stopwords.words('english')</st> 到您的 Python 控制台后运行, <st c="42915">stopwords</st> 来自 <st c="42930">NLTK</st> ,以可视化将要被移除的停用词列表。

最后,我们使用 <st c="43035">SnowballStemmer</st> <st c="43056">NLTK</st>. <st c="43062">SnowballStemmer</st> 逐个单词进行处理。 因此,我们创建了一个函数,该函数接受一个字符串并将其使用 pandas 的 <st c="43206">str.split()</st>. 在列表推导式中,我们逐个单词应用 <st c="43255">SnowballStemmer</st> ,然后将分解后的单词列表重新连接成一个字符串,使用的是 <st c="43362">join()</st> 方法。 使用 pandas 的 <st c="43390">apply()</st>,我们将该函数应用于 DataFrame 的每一行中的单词。

我们在本食谱中执行的预处理步骤产生了包含原始文本的字符串,没有标点符号或数字,全部小写,没有常用词,并且使用词根而不是其屈折形式。 返回的数据可以用来推导特征,如 计数字符、单词和词汇 食谱中所述,或者创建 BoWs 和 TI-IDF 矩阵,如 *使用词袋和 n-gram 创建特征食谱 *实现词频-逆文档频率 * *频率 * 食谱中所述。

正如我们在本食谱中所示,清理文本可能会造成数据丢失,这取决于文本的特征,如果我们希望在创建 BoW 或 TF-IDF 矩阵后解释模型,理解词根的重要性可能并不 那么直接。

posted @ 2025-09-21 12:13  绝不原创的飞龙  阅读(8)  评论(0)    收藏  举报