Python-特征工程秘籍第三版-全-
Python 特征工程秘籍第三版(全)
原文:
annas-archive.org/md5/86f7f4009ce12f06cb3f3594f063591b译者:飞龙
前言
Python 特征工程食谱涵盖了表格数据特征工程的几乎每个方面,包括缺失数据估计、分类编码、变量转换、离散化、缩放和异常值的处理。它还讨论了如何从日期和时间、文本、时间序列和关系数据集中提取特征。
本书将通过展示如何使用开源 Python 库来加速特征工程过程,通过多个实用的、动手的食谱来减轻特征工程的痛苦。在整个书中,你将利用pandas和scikit-learn转换和创建新的变量。此外,你还将学会利用四大开源特征工程库的力量——Feature-engine、Category Encoders、Featuretools 和 tsfresh。
你还将发现第二版中没有的额外食谱。这些包括在时间序列中估计缺失数据、使用决策树创建新特征以及使用中位数绝对偏差突出显示异常值。更重要的是,我们提供了指导方针,帮助你根据你的模型和数据特征决定使用哪些转换。你将确切知道每个特征转换是什么、为什么以及如何实现。
本书面向对象
本书面向机器学习和数据科学的学生和专业人员,以及参与部署机器学习模型的软件工程师,他们希望提高在数据转换和特征创建方面的技能,以改善模型训练。本书旨在为任何对特征工程感兴趣或正在从事特征工程的人提供清晰的指导,说明做什么、如何做以及为什么它很重要。本资源超越了基础知识,提供了实用的见解和详细的解释,帮助你有效地掌握特征工程。
本书涵盖内容
第一章,估计缺失数据,探讨了用合适的估计值替换数值、分类和时间序列数据中缺失值的技术。它涵盖了单次和多次估计方法,并展示了如何使用 scikit-learn 和 Feature-engine 简化估计过程。
第二章,编码分类变量,涵盖了将分类变量转换为数值特征的方法。它从常见技术如独热编码和顺序编码开始,然后探讨高基数和线性模型的适应性。本章还讨论了特定领域的编码方法,如证据权重,并展示了如何使用目标编码等方法编码高基数变量,确保你了解如何规范化过程以避免过拟合。
第三章,转换数值变量,讨论了何时以及为什么需要在机器学习模型中使用变量转换。然后展示了不同的变量转换函数,并强调每个函数最适合哪种类型的变量。到本章结束时,你将了解何时需要转换你的变量,以及为什么你应用对数或其他函数。
第四章,执行变量离散化,介绍了离散化的概念,强调了其在机器学习中的应用。该章节探讨了各种离散化方法,详细说明了它们的优缺点。它涵盖了基本的等宽和等频离散化过程,以及使用决策树和k-means 的离散化。最后,它将离散化与编码相结合,以返回与目标单调的变量。
第五章,处理异常值,介绍了识别异常值及其特征的方法。然后讨论了移除异常值或调整其值以适应可接受边界的技巧,利用 pandas 和 Feature-engine。
第六章,从日期和时间变量中提取特征,描述了如何从日期和时间变量中创建特征。它涵盖了如何从特征中提取日期和时间组件,以及如何组合日期时间变量以及如何处理不同的时区。
第七章,执行特征缩放,涵盖了将变量置于相似尺度的方法。它讨论了标准化,如何缩放到最大和最小值,以及如何执行更稳健的变量缩放形式。你还将找到关于根据你的模型和变量选择哪种方法的指南。
第八章,创建新特征,描述了将现有变量组合起来生成新特征的各种方法。它展示了基于领域知识的数学运算如何结合特征。然后讨论了如何通过正弦、余弦和样条曲线的使用来转换特征。最后,它展示了通过决策树从一个或多个变量中创建特征的价值。
第九章,使用 Featuretools 从关系数据中提取特征,介绍了关系数据集,然后解释了如何利用 Featuretools 在不同数据聚合级别上创建特征。你将学习如何从数值和分类变量、日期时间以及文本中自动创建数十个特征。
第十章,使用 tsfresh 从时间序列创建特征,讨论了如何自动从时间序列数据中创建数百个特征,用于监督分类或回归。您将利用 tsfresh 的强大功能来自动创建和选择与您的时序数据相关的特征。
第十一章,从文本变量中提取特征,探讨了有效的方法来清理和提取用于监督学习模型的短文本段特征。本章涵盖了计数单词、句子和字符以及测量词汇多样性的技术。此外,它指导您通过文本清理过程,并演示了如何通过计数单词来构建特征矩阵。
要充分利用本书
本书提供了实用的工具和技术,以简化您的特征工程流程,让您能够提高代码质量并简化流程。本书探讨了使用 Python 转换和创建特征的方法,以有效地训练机器学习模型。因此,熟悉机器学习和 Python 编程将有助于您理解和应用所提出的概念。
这些食谱已在以下库版本中进行了测试:
-
category-encoders == 2.6.3 -
Feature-engine == 1.8.0 -
featuretools == 1.31.0 -
matplotlib==3.8.3 -
nltk=3.8.1 -
numpy==1.26.4 -
pandas==2.2.1 -
scikit-learn==1.5.0 -
scipy==1.12.0 -
seaborn==0.13.2 -
tsfresh==0.20.0
| 本书中涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| Python 3.9 或更高版本 | Windows、macOS 或 Linux |
请注意,Python 库的较早或较新版本可能会阻止代码运行。如果您使用的是较新版本,请确保检查其文档以获取任何最近的更新、参数名称更改或弃用。
如果您使用的是本书的数字版,我们建议您亲自输入代码或通过 GitHub 仓库(下一节中提供链接)访问代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub(github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition)下载本书的示例代码文件。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他丰富的图书和视频的代码包,可在github.com/PacktPublishing/找到。查看它们吧!
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“我们使用year、month和quarter分别在新列中捕获年份、月份和季度。”
代码块设置如下:
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
小贴士或重要提示
看起来像这样。
部分
在这本书中,你将发现几个经常出现的标题(准备就绪,如何操作...,它是如何工作的...,还有更多...,以及也看看)。
为了清楚地说明如何完成食谱,请按照以下方式使用这些部分:
准备就绪
本节告诉您在食谱中可以期待什么,并描述如何设置任何软件或任何为食谱所需的初步设置。
如何操作…
本节包含遵循食谱所需的步骤。
它是如何工作的…
本节通常包含对前一个节中发生的事情的详细解释。
还有更多…
本节包含有关食谱的附加信息,以使您对其有更深入的了解。
也看看
本节提供了对食谱的其他有用信息的链接。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过客户关怀@packtpub.com 给我们发送电子邮件。
勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在本书中发现错误,我们将不胜感激,如果您能向我们报告,我们将非常感谢。请访问www.packtpub.com/support/errata,选择您的书,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上以任何形式遇到我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过版权@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问authors.packtpub.com。
分享您的想法
读完Python 特征工程食谱后,我们非常乐意听到您的想法!请点击此处直接进入本书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
您喜欢在路上阅读,但又无法携带您的印刷书籍到处走吗?
您的电子书购买是否与您选择的设备不兼容?
别担心,现在购买每本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止这些,您还可以获得独家折扣、时事通讯和丰富的免费内容,每天直接发送到您的收件箱
按照以下简单步骤获取福利:
- 扫描下面的二维码或访问以下链接

packt.link/free-ebook/978-1-83588-358-7
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他福利发送到您的邮箱
第一章:填充缺失数据
缺失数据——即某些观测值的值缺失——是大多数数据源中不可避免的问题。某些机器学习模型的实现可以开箱即用地处理缺失数据。为了训练其他模型,我们必须删除具有缺失数据的观测值或将它们转换为允许的值。
将缺失数据替换为其统计估计的行为称为填充。任何填充技术的目标都是生成一个完整的数据集。有多种填充方法。我们根据数据是否随机缺失、缺失值的比例以及我们打算使用的机器学习模型来选择使用哪种方法。在本章中,我们将讨论几种填充方法。
本章将涵盖以下食谱:
-
删除具有缺失数据的观测值
-
执行均值或中位数填充
-
填充分类变量
-
用任意数替换缺失值
-
为填充找到极端值
-
标记填充值
-
实施前向和后向填充
-
执行插值
-
通过链式方程进行多元填充
-
使用最近邻估计缺失数据
技术要求
在本章中,我们将使用 Python 库 Matplotlib、pandas、NumPy、scikit-learn 和 Feature-engine。如果您需要安装 Python,免费的 Anaconda Python 发行版(www.anaconda.com/)包括了大多数数值计算库。
可以使用以下方式使用pip安装feature-engine:
pip install feature-engine
如果您使用 Anaconda,可以使用conda安装feature-engine:
conda install -c conda-forge feature_engine
注意
本章中的食谱是在出版时使用 Python 库的最新版本创建的。您可以在附带的 GitHub 仓库中的requirements.txt文件中检查版本,网址为github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/requirements.txt。
我们将使用来自UCI 机器学习仓库的信用批准数据集(archive.ics.uci.edu/),该数据集根据 CC BY 4.0 创意共享许可:creativecommons.org/licenses/by/4.0/legalcode。您可以在以下链接找到数据集:archive.ics.uci.edu/dataset/27/credit+approval。
我下载并修改了如本笔记本所示的数据:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch01-missing-data-imputation/credit-approval-dataset.ipynb
我们还将使用位于 Facebook 的 Prophet GitHub 仓库中的航空乘客数据集(github.com/facebook/prophet/blob/main/examples/example_air_passengers.csv),该数据集受 MIT 许可协议保护:github.com/facebook/prophet/blob/main/LICENSE
我已经在这个笔记本中修改了数据,如所示:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch01-missing-data-imputation/air-passengers-dataset.ipynb
你可以在附带的 GitHub 仓库中找到修改后的数据集副本:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch01-missing-data-imputation/
删除含有缺失数据的观测值
完全案例分析(CCA),也称为案例的逐行删除,包括丢弃含有缺失数据的观测值。CCA 可以应用于分类和数值变量。在使用 CCA 的情况下,如果数据是随机缺失且仅在一小部分观测值中缺失,我们可以在插补后保留变量的分布。然而,如果数据在许多变量中缺失,CCA 可能会导致大量数据集的删除。
注意
只有在观测值缺失数量很少,并且你有充分的理由相信它们对你的模型不重要时,才使用 CCA。
如何操作...
让我们先进行一些导入和加载数据集的操作:
-
让我们导入
pandas、matplotlib以及来自 scikit-learn 的训练/测试分割函数:import matplotlib.pyplot as plt import pandas as pd from sklearn.model_selection import train_test_split -
让我们加载并显示在技术要求部分描述的数据集:
data = pd.read_csv("credit_approval_uci.csv") data.head()在以下图像中,我们可以看到数据的前 5 行:

图 1.1 – 数据集的前 5 行
-
如果我们正在准备数据以训练机器学习模型,我们会像通常那样继续操作;通过将数据分为训练集和测试集:
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, ) -
现在让我们制作一个条形图,显示训练集和测试集中每个变量的缺失数据比例:
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 – 每个变量的缺失数据比例
-
现在,我们将删除任何变量中含有缺失值的观测值:
train_cca = X_train.dropna() test_cca = X_test.dropna()
注意
pandas 的dropna()默认会删除任何含有缺失值的观测值。我们可以通过以下方式删除变量子集(如"A3", "A4")中的缺失数据:data.dropna(subset=["A3", "A4"])。
-
让我们打印并比较原始数据集和完整案例数据集的大小:
print(f"Total observations: {len(X_train)}") print(f"Observations without NAN: {len(train_cca)}")我们从训练集中移除了超过 200 个有缺失数据的观测值,如下所示输出:
Total observations: 483 Observations without NAN: 264 -
在从训练集和测试集中移除观测值后,我们需要对齐目标变量:
y_train_cca = y_train.loc[train_cca.index] y_test_cca = y_test.loc[test_cca.index]现在,数据集和目标变量包含没有缺失数据的行。
-
要使用
feature-engine丢弃有缺失数据的观测值,让我们导入所需的转换器:from feature_engine.imputation import DropMissingData -
让我们设置填充器以自动找到有缺失数据的变量:
cca = DropMissingData(variables=None, missing_only=True) -
让我们调整 transformer,使其找到有缺失数据的变量:
cca.fit(X_train) -
让我们检查 transformer 找到的含有 NAN 的变量:
cca.variables_前一个命令返回了有缺失数据的变量名称:
['A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9', 'A10', 'A14'] -
让我们移除训练集和测试集中有缺失数据的行:
train_cca = cca.transform(X_train) test_cca = cca.transform(X_test)使用
train_cca.isnull().sum()来证实完整案例数据集中没有缺失数据。 -
DropMissingData可以在从训练集中移除缺失数据后自动调整目标: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)
前面的代码从训练集和测试集中移除了nan行,然后重新对齐了目标变量。
注意
要从变量子集中移除有缺失数据的观测值,使用DropMissingData(variables=['A3', 'A4'])。要移除至少 5%变量中有nan的行,使用DropMissingData(threshold=0.95)。
它是如何工作的...
在这个菜谱中,我们绘制了每个变量中缺失数据的比例,然后移除了所有有缺失值的观测值。
我们使用pandas的isnull()和mean()方法来确定每个变量中缺失观测值的比例。isnull()方法为每个变量创建了一个布尔向量,其中True和False值表示值是否缺失。mean()方法取这些值的平均值,并返回缺失数据的比例。
我们使用pandas的plot.bar()创建每个变量的缺失数据分数的条形图。在图 1.2 中,我们看到了训练集和测试集中每个变量的nan分数。
要移除任何变量中的缺失值观测,我们使用了 pandas 的dropna(),从而获得了一个完整案例数据集。
最后,我们使用 Feature-engine 的DropMissingData()移除了缺失数据。这个填充器在调用fit()方法时自动识别并存储了训练集中的缺失数据变量。使用transform()方法,填充器移除了那些变量中的nan观测值。使用transform_x_y(),填充器从数据集中移除了nan行,然后重新对齐了目标变量。
参见
如果你想在管道中使用DropMissingData()与其他 Feature-engine 或 scikit-learn 转换器一起使用,请查看 Feature-engine 的Pipeline:Feature-engine.trainindata.com/en/latest/user_guide/pipeline/Pipeline.html。此管道在删除行后可以与训练集和测试集对齐目标。
执行均值或中值插补
均值或中值插补包括用变量的均值或中值值替换缺失数据。为了避免数据泄露,我们使用训练集来确定均值或中值,然后使用这些值来插补训练集、测试集以及所有未来的数据。
Scikit-learn 和 Feature-engine 从训练集中学习均值或中值,并存储这些参数以供将来使用。默认情况下。
在这个菜谱中,我们将使用pandas、scikit-learn和feature-engine执行均值和中值插补。
注意
如果变量呈正态分布,则使用均值插补;否则使用中值插补。如果缺失数据比例较高,均值和中值插补可能会扭曲变量分布。
如何做到这一点...
让我们开始这个菜谱:
-
首先,我们将导入
pandas以及从scikit-learn和feature-engine中所需的函数和类。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 -
让我们加载我们在技术要求部分准备的数据集:
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, ) -
让我们通过排除对象类型的变量来创建一个数值变量的列表:
numeric_vars = X_train.select_dtypes( exclude="O").columns.to_list()如果你执行
numeric_vars,你将看到数值变量的名称:['A2', 'A3', 'A8', 'A11', 'A14', 'A15']。 -
让我们在字典中捕获变量的中值:
median_values = X_train[ numeric_vars].median().to_dict()
小贴士
注意我们如何使用训练集计算中值。我们将使用这些值来替换训练集和测试集中的缺失数据。要计算均值,请使用 pandas 的mean()而不是median()。
如果你执行median_values,你将看到一个包含每个变量的中值字典:{'A2': 28.835, 'A3': 2.75, 'A8': 1.0, 'A11': 0.0, 'A14': 160.0, 'A15': 6.0}。
-
让我们用中值替换缺失数据:
X_train_t = X_train.fillna(value=median_values) X_test_t = X_test.fillna(value=median_values)在插补后,如果你执行
X_train_t[numeric_vars].isnull().sum(),数值变量中的缺失值数量应该是0。
注意
pandas fillna()默认返回一个包含插补值的新数据集。要替换原始 DataFrame 中的缺失数据,请将inplace参数设置为True:X_train.fillna(value=median_values, inplace=True)。
现在,让我们使用scikit-learn用中值插补缺失值。
-
让我们设置插补器,用中值替换缺失数据:
imputer = SimpleImputer(strategy="median")
注意
要执行均值插补,请按以下方式设置SimpleImputer():imputer = SimpleImputer(strategy = "mean")。
-
我们通过使用
ColumnTransformer()来限制插补仅应用于数值变量:ct = ColumnTransformer( [("imputer", imputer, numeric_vars)], remainder="passthrough", force_int_remainder_cols=False, ).set_output(transform="pandas")
注意
根据我们如何设置转换输出,Scikit-learn 可以返回numpy数组、pandas DataFrame 或polar框架。默认情况下,它返回numpy数组。
-
让我们将填充器拟合到训练集,以便它学习中值:
ct.fit(X_train) -
让我们检查学习到的中值:
ct.named_transformers_.imputer.statistics_之前的命令返回了每个变量的中值:
array([ 28.835, 2.75, 1., 0., 160., 6.]) -
让我们用中值替换缺失值:
X_train_t = ct.transform(X_train) X_test_t = ct.transform(X_test) -
让我们显示结果训练集:
print(X_train_t.head())在以下图像中,我们可以看到结果 DataFrame:

图 1.3 – 填充后的训练集。填充的变量由填充器前缀标记;未转换的变量显示前缀余数
最后,让我们使用feature-engine执行中值填充:
-
让我们设置填充器,用中值替换数值变量中的缺失值:
imputer = MeanMedianImputer( imputation_method="median", variables=numeric_vars, )
注意
要执行均值填充,将imputation_method更改为"mean"。默认情况下,MeanMedianImputer()将填充 DataFrame 中的所有数值变量,忽略分类变量。使用variables参数限制填充到数值变量的子集。
-
将填充器拟合以学习中值:
imputer.fit(X_train) -
检查学习到的中值:
imputer.imputer_dict_之前的命令返回了字典中的中值:
{'A2': 28.835, 'A3': 2.75, 'A8': 1.0, 'A11': 0.0, 'A14': 160.0, 'A15': 6.0} -
最后,让我们用中值替换缺失值:
X_train = imputer.transform(X_train) X_test = imputer.transform(X_test)
Feature-engine 的MeanMedianImputer()返回一个DataFrame。您可以使用X_train[numeric_vars].isnull().mean()检查填充的变量是否不包含缺失值。
它是如何工作的...
在这个菜谱中,我们使用pandas、scikit-learn和feature-engine替换了缺失数据,用变量的中值。
我们使用 scikit-learn 的train_test_split()函数将数据集分为训练集和测试集。该函数接受预测变量、目标、测试集中要保留的观测值比例以及用于可重复性的random_state值作为参数。它返回一个包含原始观测值 70%的训练集和一个包含原始观测值 30%的测试集。70:30 的分割是随机的。
要使用 pandas 进行缺失数据填充,在步骤 5中,我们创建了一个字典,其中数值变量名作为键,它们的平均值作为值。中值是从训练集中学习的,以避免数据泄露。为了替换缺失数据,我们对训练集和测试集应用了pandas的fillna(),并将每个变量的中值字典作为参数传递。
要使用 scikit-learn 将缺失值替换为中位数,我们使用了 strategy 设置为 "median" 的 SimpleImputer()。为了限制推断仅限于数值变量,我们使用了 ColumnTransformer()。通过将 remainder 参数设置为 passthrough,我们使 ColumnTransformer() 在转换后的输出中返回训练集中看到的所有变量;推断的变量随后是未转换的变量。
注意
ColumnTransformer() 改变了输出中变量的名称。转换后的变量显示前缀 imputer,而未更改的变量显示前缀 remainder。
在 步骤 8 中,我们将列转换器的输出设置为 pandas 以获得一个 DataFrame 作为结果。默认情况下,ColumnTransformer() 返回 numpy 数组。
注意
从版本 1.4.0 开始,scikit-learn 转换器可以通过 transform() 方法返回 numpy 数组、pandas DataFrame 或 polar 帧。
通过 fit(),SimpleImputer() 学习了训练集中每个数值变量的中位数,并将它们存储在其 statistics_ 属性中。通过 transform(),它用中位数替换了缺失值。
要使用 Feature-engine 将缺失值替换为中位数,我们使用了 imputation_method 设置为 median 的 MeanMedianImputer()。为了限制推断仅限于变量子集,我们将变量名称列表传递给 variables 参数。通过 fit(),转换器学习并存储了每个变量的中位数值,并将其存储在其 imputer_dict_ 属性中的字典中。通过 transform(),它替换了缺失值,返回一个 pandas DataFrame。
推断分类变量
我们通常使用最频繁的类别或特定的字符串来推断分类变量。为了避免数据泄露,我们从训练集中找到频繁的类别。然后,我们使用这些值来推断训练集、测试集和未来的数据集。scikit-learn 和 feature-engine 可以直接找到并存储用于推断的频繁类别。
在这个菜谱中,我们将用最频繁的类别或任意字符串替换分类变量中的缺失数据。
如何操作...
首先,让我们进行一些导入并准备数据:
-
让我们导入
pandas以及从scikit-learn和feature-engine中所需的函数和类: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 -
让我们加载我们在 技术要求 部分准备的数据集:
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, ) -
让我们将分类变量放入一个列表中:
categorical_vars = X_train.select_dtypes( include="O").columns.to_list() -
让我们将变量的最频繁类别存储在一个字典中:
frequent_values = X_train[ categorical_vars].mode().iloc[0].to_dict() -
让我们用频繁的类别替换缺失值:
X_train_t = X_train.fillna(value=frequent_values) X_test_t = X_test.fillna(value=frequent_values)
注意
fillna() 默认返回一个包含推断值的新的 DataFrame。我们可以通过执行 X_train.fillna(value=frequent_values, inplace=True) 来替换原始 DataFrame 中的缺失数据。
-
要用特定字符串替换缺失数据,让我们创建一个以分类变量名称为键、任意字符串为值的填充字典:
imputation_dict = {var: "no_data" for var in categorical_vars}现在,我们可以使用这个字典和 步骤 6 中的代码来替换缺失数据。
注意
使用 pandas 的 value_counts() 我们可以看到填充中添加的字符串。例如,尝试执行 X_train["A1"].value_counts()。
现在,让我们使用 scikit-learn 用最频繁的类别来填充缺失值。
-
让我们设置填充器,为每个变量找到最频繁的类别:
imputer = SimpleImputer(strategy='most_frequent')
注意
SimpleImputer() 将学习数值和分类变量的众数。但在实际操作中,众数填充仅适用于分类变量。
-
让我们将填充限制在分类变量上:
ct = ColumnTransformer( [("imputer",imputer, categorical_vars)], remainder="passthrough" ).set_output(transform="pandas")
注意
要用字符串而不是最频繁的类别来填充缺失数据,请按以下方式设置 SimpleImputer():imputer = SimpleImputer(strategy="constant", fill_value="missing")。
-
将填充器拟合到训练集,以便它学习最频繁的值:
ct.fit(X_train) -
让我们看看填充器学习到的最频繁值:
ct.named_transformers_.imputer.statistics_之前的命令返回每个变量的最频繁值:
array(['b', 'u', 'g', 'c', 'v', 't', 'f', 'f', 'g'], dtype=object) -
最后,让我们用频繁类别替换缺失值:
X_train_t = ct.transform(X_train) X_test_t = ct.transform(X_test)确保通过执行
X_train_t.head()来检查生成的 DataFrames。
注意
ColumnTransformer() 改变了变量的名称。填充的变量显示前缀 imputer,未转换的变量显示前缀 remainder。
最后,让我们使用 feature-engine 来填充缺失值。
-
让我们设置填充器,用最频繁的值替换分类变量中的缺失数据:
imputer = CategoricalImputer( imputation_method="frequent", variables=categorical_vars, )
注意
将 variables 参数设置为 None,CategoricalImputer() 将自动填充训练集中找到的所有分类变量。使用此参数将填充限制为分类变量的子集,如 步骤 13 所示。
-
将填充器拟合到训练集,以便它学习最频繁的类别:
imputer.fit(X_train)
注意
要用特定字符串填充分类变量,将 imputation_method 设置为 missing 并将 fill_value 设置为所需的字符串。
-
让我们检查学习到的类别:
imputer.imputer_dict_我们可以在以下输出中看到包含最频繁值的字典:
{'A1': 'b', 'A4': 'u', 'A5': 'g', 'A6': 'c', 'A7': 'v', 'A9': 't', 'A10': 'f', 'A12': 'f', 'A13': 'g'} -
最后,让我们用频繁类别替换缺失值:
X_train_t = imputer.transform(X_train) X_test_t = imputer.transform(X_test)如果你想使用
CategoricalImputer()用字符串或最频繁的值填充数值变量,请将ignore_format参数设置为True。
CategoricalImputer() 返回一个 pandas DataFrame 作为结果。
它是如何工作的...
在这个示例中,我们用最频繁的类别或任意字符串替换了分类变量中的缺失值。我们使用了 pandas、scikit-learn 和 feature-engine。
在步骤 5中,我们创建了一个字典,变量名作为键,频繁类别作为值。为了捕获频繁类别,我们使用了 pandas 的mode(),为了返回一个字典,我们使用了 pandas 的to_dict()。由于一个变量中可能有多个众数,所以我们确保只捕获其中一个值,使用了.iloc[0]。
要使用scikit-learn替换缺失值,我们使用了将strategy设置为most_frequent的SimpleImputer()。为了将填充限制在分类变量上,我们使用了ColumnTransformer()。通过将remainder设置为passthrough,我们使ColumnTransformer()在transform()方法的结果中返回训练集中存在的所有变量。
注意
ColumnTransformer()会更改输出中变量的名称。转换后的变量显示前缀imputer,未更改的变量显示前缀remainder。
在fit()中,SimpleImputer()学习了变量的最频繁类别并将它们存储在其statistics_属性中。在transform()中,它用学习到的参数替换了缺失数据。
SimpleImputer()和ColumnTransformer()默认返回 NumPy 数组。我们可以通过set_output()参数更改此行为。
要使用feature-engine替换缺失值,我们使用了将imputation_method设置为frequent的CategoricalImputer()。在fit()中,转换器学习并存储了最频繁类别在一个字典中,该字典存储在其imputer_dict_属性中。在transform()中,它用学习到的参数替换了缺失值。
与SimpleImputer()不同,CategoricalImputer()只会填充分类变量,除非通过将ignore_format参数设置为True来明确指示不要这样做。此外,使用feature-engine转换器,我们可以通过转换器本身限制转换到变量子集。对于scikit-learn转换器,我们需要额外的ColumnTransformer()类来将转换应用于变量子集。
用任意数字替换缺失值
我们可以用任意值替换缺失数据。常用的值是999、9999或对于正分布的-1。这种方法用于数值变量。对于分类变量,等效的方法是像在填充分类变量配方中描述的那样,用任意字符串替换缺失数据。
当用任意数字替换缺失值时,我们需要小心不要选择接近均值、中位数或分布中任何其他常见值的数值。
注意
当数据不是随机缺失时,使用非线性模型,或者当缺失数据的百分比很高时,我们会使用任意数字填充。这种填充技术会扭曲原始变量分布。
在这个菜谱中,我们将使用 pandas、scikit-learn 和 feature-engine 来用任意数字填充缺失数据。
如何做...
让我们先导入必要的工具并加载数据:
-
导入
pandas和所需的函数和类:import pandas as pd from sklearn.model_selection import train_test_split from sklearn.impute import SimpleImputer from feature_engine.imputation import ArbitraryNumberImputer -
让我们加载 技术要求 部分中描述的数据集:
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, )我们将选择大于分布最大值的任意值。
-
让我们找到四个数值变量的最大值:
X_train[['A2','A3', 'A8', 'A11']].max()之前的命令返回以下输出:
A2 76.750 A3 26.335 A8 28.500 A11 67.000 99 for the imputation because it is bigger than the maximum values of the numerical variables in *step 4*. -
让我们复制原始的 DataFrames:
X_train_t = X_train.copy() X_test_t = X_test.copy() -
现在,我们将缺失值替换为
99: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)
注意
要使用 pandas 的 fillna() 用不同的值填充不同的变量,请使用如下字典:imputation_dict = {"A2": -1, "A3": -1, "A8": 999, "``A11": 9999}。
现在,我们将使用 scikit-learn 用任意数字填充缺失值。
-
让我们设置
imputer,将缺失值替换为99:imputer = SimpleImputer(strategy='constant', fill_value=99)
注意
如果你的数据集包含分类变量,SimpleImputer() 也会将这些变量中的任何缺失值添加 99。
-
让我们将
imputer拟合到包含要填充的变量的训练集的一个子集:vars = ["A2", "A3", "A8", "A11"] imputer.fit(X_train[vars]) -
在所需的变量中将缺失值替换为
99:X_train_t[vars] = imputer.transform(X_train[vars]) X_test_t[vars] = imputer.transform(X_test[vars])执行
X_test_t[["A2", "A3", "``A8", "A11"]].isnull().sum()来检查缺失值的缺失情况。为了完成,让我们使用
feature-engine填充缺失值。 -
让我们设置
imputer,在 4 个特定变量中将缺失值替换为99:imputer = ArbitraryNumberImputer( arbitrary_number=99, variables=["A2", "A3", "A8", "A11"], )
注意
如果我们将 variables 参数设置为 None,ArbitraryNumberImputer() 将自动选择训练集中的所有数值变量进行填充。
-
最后,让我们将缺失值替换为
99:X_train = imputer.fit_transform(X_train) X_test = imputer.transform(X_test)
注意
要用不同的数字填充不同的变量,请按以下方式设置 ArbitraryNumberImputer():ArbitraryNumberImputer(imputer_dict = {"A2": -1, "A3": -1, "A8": 999, "``A11": 9999})。
我们现在已经使用三个不同的开源库用任意数字替换了缺失数据。
它是如何工作的...
在这个菜谱中,我们使用 pandas、scikit-learn 和 feature-engine 用任意数字替换了数值变量中的缺失值。
为了确定使用哪个任意值,我们使用 pandas 的 max() 检查了四个数值变量的最大值。我们选择了 99,因为它大于所选变量的最大值。在 步骤 5 中,我们使用 pandas 的 fillna() 替换了缺失数据。
要使用 scikit-learn 替换缺失值,我们使用了 SimpleImputer(),将 strategy 设置为 constant,并在 fill_value 参数中指定了 99。接下来,我们将填充器拟合到包含要填充的数值变量的训练集的一个子集。最后,我们使用 transform() 替换了缺失值。
要使用feature-engine替换缺失值,我们使用了ArbitraryValueImputer(),指定值为99和要插补的变量作为参数。接下来,我们应用了fit_transform()方法来替换训练集中的缺失数据,以及transform()方法来替换测试集中的缺失数据。
寻找插补的极值
用变量分布的末端值(极值)替换缺失值,就像用任意值替换一样,但不是手动设置任意值,而是自动从变量分布的末端选择值。
我们可以用一个大于变量中大多数值的值来替换缺失数据。为了选择一个较大的值,我们可以使用平均值加上标准差的因子。或者,我们可以将其设置为第 75 百分位数 + IQR × 1.5。IQR代表四分位距,是第 75 百分位数和第 25 百分位数之间的差。为了用小于剩余值的值替换缺失数据,我们可以使用平均值减去标准差的因子,或者第 25 百分位数 – IQR × 1.5。
注意
尾部插补可能会扭曲原始变量的分布,因此可能不适合线性模型。
在这个菜谱中,我们将使用pandas和feature-engine实现尾部或极值插补。
如何做...
要开始这个菜谱,让我们导入必要的工具并加载数据:
-
让我们导入
pandas和所需的函数和类:import pandas as pd from sklearn.model_selection import train_test_split from feature_engine.imputation import EndTailImputer -
让我们加载在技术要求部分描述的数据集:
data = pd.read_csv("credit_approval_uci.csv") -
让我们将数值变量放入一个列表中,排除目标变量:
numeric_vars = [ var for var in data.select_dtypes( exclude="O").columns.to_list() if var !="target" ] -
让我们将数据分为训练集和测试集,只保留数值变量:
X_train, X_test, y_train, y_test = train_test_split( data[numeric_vars], data["target"], test_size=0.3, random_state=0, ) -
我们现在将确定 IQR:
IQR = X_train.quantile(0.75) - X_train.quantile(0.25)我们可以通过执行
IQR或print(IQR)来可视化 IQR 值:A2 16.4200 A3 6.5825 A8 2.8350 A11 3.0000 A14 212.0000 A15 450.0000 dtype: float64 -
让我们创建一个包含变量名称和插补值的字典:
imputation_dict = ( X_train.quantile(0.75) + 1.5 * IQR).to_dict()
注意
如果我们使用四分位距邻近规则,我们将通过将 1.5 倍的 IQR 加到第 75 百分位数来确定插补值。如果变量呈正态分布,我们可以将插补值计算为平均值加上标准差的因子,imputation_dict = (X_train.mean() + 3 * X_train.std()).to_dict()。
-
最后,让我们替换缺失数据:
X_train_t = X_train.fillna(value=imputation_dict) X_test_t = X_test.fillna(value=imputation_dict)
注意
我们也可以使用分布左尾的值来替换缺失数据,使用value = X_train[var].quantile(0.25) - 1.5 * IQR或value = X_train[var].mean() – 3 * X_train[var].std()。
最后,让我们使用feature-engine来插补缺失值。
-
让我们设置
imputer来使用 IQR 邻近规则估计分布右侧的值:imputer = EndTailImputer( imputation_method="iqr", tail="right", fold=3, variables=None, )
注意
要使用平均值和标准差来计算替换值,设置imputation_method="Gaussian"。在tail参数中使用left或right来指定在寻找插补值时要考虑分布的哪一侧。
-
让我们将
EndTailImputer()拟合到训练集,以便它学习填充的值:imputer.fit(X_train) -
让我们检查学习到的值:
imputer.imputer_dict_之前的命令返回一个包含用于填充每个变量的值的字典:
{'A2': 88.18, 'A3': 27.31, 'A8': 11.504999999999999, 'A11': 12.0, 'A14': 908.0, 'A15': 1800.0} -
最后,让我们替换缺失值:
X_train = imputer.transform(X_train) X_test = imputer.transform(X_test)
记住,你可以使用X_train[['A2','A3', 'A8', 'A11', '``A14', 'A15']].isnull().mean()来验证缺失值已被替换。
它是如何工作的...
在这个菜谱中,我们使用pandas和feature-engine将数值变量中的缺失值替换为分布末尾的数字。
我们根据本菜谱引言中描述的公式确定了填充值。我们使用 pandas 的quantile()找到特定的分位数值,或使用pandas的mean()和std()来计算均值和标准差。通过 pandas 的fillna()我们替换了缺失值。
要使用feature-engine中的EndTailImputer()替换缺失值,我们将distribution设置为iqr,根据 IQR 邻近规则计算值。将tail设置为right,转换器从分布的右侧找到填充值。通过fit(),填充器学习并存储在imputer_dict_属性中的字典中的填充值。通过transform(),我们替换了缺失值,返回 DataFrame。
标记填充值
在之前的菜谱中,我们专注于用它们值的估计来替换缺失数据。此外,我们还可以添加缺失指标来标记值缺失的观测值。
缺失指标是一个二元变量,它取值为1或True以指示值是否缺失,否则为0或False。在将缺失观测值替换为均值、中位数或最频繁的类别的同时,用缺失指标标记这些缺失观测值是一种常见做法。在这个菜谱中,我们将学习如何使用pandas、scikit-learn和feature-engine添加缺失指标。
如何操作...
让我们先进行一些导入和加载数据:
-
让我们导入所需的库、函数和类:
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 ) -
让我们加载并分割技术要求部分中描述的数据集:
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, ) -
让我们将变量名捕获到一个列表中:
varnames = ["A1", "A3", "A4", "A5", "A6", "A7", "A8"] -
让我们为缺失指标创建名称并将它们存储在一个列表中:
indicators = [f"{var}_na" for var in varnames]如果我们执行
indicators,我们将看到用于新变量的名称:['A1_na', 'A3_na', 'A4_na', 'A5_na', 'A6_na', '``A7_na', 'A8_na']。 -
让我们复制原始的 DataFrames:
X_train_t = X_train.copy() X_test_t = X_test.copy() -
让我们添加缺失指标:
X_train_t[indicators] =X_train[ varnames].isna().astype(int) X_test_t[indicators] = X_test[ varnames].isna().astype(int)
注意
如果你希望指标以True和False作为值而不是0和1,请在步骤 6中移除astype(int)。
-
让我们检查生成的 DataFrame:
X_train_t.head()我们可以在以下图像的 DataFrame 右侧看到新添加的变量:

图 1.4 – 带有缺失指标的 DataFrame
现在,让我们使用 Feature-engine 添加缺失指示符。
-
设置缺失值填充器,为每个有缺失数据的变量添加二进制指示符:
imputer = AddMissingIndicator( variables=None, missing_only=True ) -
将填充器拟合到训练集,以便它找到有缺失数据的变量:
imputer.fit(X_train)
注意
如果我们执行 imputer.variables_,我们将找到需要添加缺失指示符的变量。
-
最后,让我们添加缺失指示符:
X_train_t = imputer.transform(X_train) X_test_t = imputer.transform(X_test)到目前为止,我们只是添加了缺失指示符。但我们的变量中仍然有缺失数据。我们需要用数字来替换它们。在本食谱的其余部分,我们将结合使用缺失指示符和均值以及众数填充。
-
让我们创建一个管道,将缺失指示符添加到分类和数值变量中,然后使用最频繁的类别填充分类变量,使用均值填充数值变量:
pipe = Pipeline([ ("indicators", AddMissingIndicator(missing_only=True)), ("categorical", CategoricalImputer( imputation_method="frequent")), ("numerical", MeanMedianImputer()), ])
注意
feature-engine 缺失值填充器会自动识别数值或分类变量。因此,在这种情况下,无需切片数据或传递变量名作为参数给转换器。
-
让我们添加指示符并填充缺失值:
X_train_t = pipe.fit_transform(X_train) X_test_t = pipe.transform(X_test)
注意
使用 X_train_t.isnull().sum() 来证实没有数据缺失。执行 X_train_t.head() 来查看结果数据帧。
最后,让我们添加缺失指示符,并使用 scikit-learn 分别用均值和最频繁的类别来填充数值和分类变量。
-
让我们列出数值变量和分类变量的名称:
numvars = X_train.select_dtypes( exclude="O").columns.to_list() catvars = X_train.select_dtypes( include="O").columns.to_list() -
让我们设置一个管道来执行均值和频繁类别填充,同时标记缺失数据:
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") -
现在,让我们进行填充:
X_train_t = pipe.fit_transform(X_train) X_test_t = pipe.transform(X_test)
确保探索 X_train_t.head() 以熟悉管道的输出。
它是如何工作的...
要使用 pandas 添加缺失指示符,我们使用了 isna(),它创建了一个新向量,如果存在缺失值则分配 True 的值,否则分配 False。我们使用了 astype(int) 将布尔向量转换为具有值 1 和 0 的二进制向量。
要使用 feature-engine 添加缺失指示符,我们使用了 AddMissingIndicator()。使用 fit() 方法,转换器找到了有缺失数据的变量。使用 transform() 方法,它将缺失指示符追加到训练集和测试集的右侧。
为了顺序添加缺失指示符,然后用最频繁的类别或均值替换 nan 值,我们在 pipeline 中排列了 Feature-engine 的 AddMissingIndicator()、CategoricalImputer() 和 MeanMedianImputer()。pipeline 的 fit() 方法使转换器找到 nan 变量,并计算数值变量的均值和分类变量的众数。pipeline 的 transform() 方法使转换器添加缺失指示符,然后用估计值替换缺失值。
注意
特征工程转换返回的 DataFrame 保留了变量的原始名称和顺序。另一方面,Scikit-learn 的 ColumnTransformer() 会改变结果数据中变量的名称和顺序。
最后,我们添加了缺失指示符,并使用 scikit-learn 将缺失数据替换为平均值和最频繁的类别。我们排列了两个 SimpleImputer() 实例,第一个用于用平均值插补数据,第二个用于用最频繁的类别插补数据。在这两种情况下,我们将 add_indicator 参数设置为 True 以添加缺失指示符。我们用 ColumnTransformer() 包装 SimpleImputer() 以专门修改数值或分类变量。然后我们使用 pipeline 中的 fit() 和 transform() 方法来训练转换器,然后添加指示符并替换缺失数据。
当返回 DataFrame 时,ColumnTransformer() 会改变变量的名称和顺序。通过执行 X_train_t.head() 来查看 步骤 15 的结果。你会看到,将每个步骤的名称添加为前缀到变量中,以标记每个转换器修改了哪个变量。然后,num_imputer__A2 由管道的第一个步骤返回,而 cat_imputer__A12 由管道的第二个步骤返回。
还有更多...
Scikit-learn 包含一个名为 MissingIndicator() 的转换器,它仅添加缺失指示符。请在文档中查看相关信息:scikit-learn.org/stable/modules/generated/sklearn.impute.MissingIndicator.html,并在配套的 GitHub 仓库中找到一个示例:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch01-missing-data-imputation/Recipe-06-Marking-imputed-values.ipynb。
实现前向和后向填充
时间序列数据也会显示缺失值。要在时间序列中插补缺失数据,我们使用特定的方法。前向填充插补涉及在数据序列中用最接近的前一个非缺失值填充数据集中的缺失值。换句话说,我们将最后看到的值向前传递到下一个有效值。后向填充插补涉及用数据序列中跟随它的下一个非缺失值填充缺失值。换句话说,我们将最后一个有效值向后传递到其前面的有效值。
在这个菜谱中,我们将使用前向和后向填充替换时间序列中的缺失数据。
如何实现...
让我们先导入所需的库和时间序列数据集:
-
让我们导入
pandas和matplotlib:import matplotlib.pyplot as plt import pandas as pd -
让我们加载我们在 技术要求 部分描述的航空乘客数据集,并显示时序的前五行:
df = pd.read_csv( "air_passengers.csv", parse_dates=["ds"], index_col=["ds"], ) print(df.head())我们在以下输出中看到时序:
y ds 1949-01-01 112.0 1949-02-01 118.0 1949-03-01 132.0 1949-04-01 129.0 1949-05-01 121.0
注意
你可以通过执行 df.isnull().mean() 来确定缺失数据的百分比。
-
让我们绘制时序图以查找任何明显的数据间隙:
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 – 显示缺失值的时序数据
-
让我们通过将任何区间中最后观测到的值携带到下一个有效值来插补缺失数据:
df_imputed = df.ffill()你可以通过执行
df_imputed.isnull().sum()来验证缺失数据的缺失。 -
现在让我们绘制完整的数据集,并将用于插补的值以虚线形式叠加:
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")之前的代码返回以下图表,其中我们看到用于替换
nan的值作为连续时序线之间的虚线叠加:

图 1.6 – 缺失值被最后观测到的值替换的时序数据(虚线)
-
或者,我们可以使用后向填充来插补缺失数据:
df_imputed = df.bfill()如果我们绘制插补数据集,并像在 步骤 5 中那样叠加插补值,我们将看到以下图表:

图 1.7 – 缺失值被后向填充替换的时序数据(虚线)
注意
用于插补的值的高度在 图 1.6 和 1.7 中不同。在 图 1.6 中,我们向前携带最后一个值,因此高度较低。在 图 1.7 中,我们向后携带下一个值,因此高度较高。
我们现在已经获得了完整的数据集,我们可以用它来进行时序分析和建模。
它是如何工作的...
pandas 的 ffill() 在时序中的任何时间间隙中取最后观测到的值,并将其传播到下一个观测值。因此,在 图 1.6 中,我们看到与最后观测值高度对应的插补值虚线叠加。
pandas 的 bfill() 在时序中的任何时间间隙中取下一个有效值,并将其传播到之前观测到的值。因此,在 图 1.7 中,我们看到与间隙中下一个观测值高度对应的插补值虚线叠加。
默认情况下,ffill() 和 bfill() 将在有效观测值之间插补所有值。我们可以通过设置一个限制,使用两种方法中的 limit 参数来限制任何区间内的数据点数量。例如,ffill(limit=10) 将只替换任何间隙中的前 10 个数据点。
执行插值
我们可以通过在两个非缺失数据点之间进行插值来对时间序列中的缺失数据进行插补。插值是通过函数估计一个或多个值在范围内的方法。在线性插值中,我们在最后一个观测值和下一个有效点之间拟合一个线性函数。在样条插值中,我们在最后一个和下一个观测值之间拟合一个低度多项式。使用插值的想法是获得更好的缺失数据估计。
在这个菜谱中,我们将在一个时间序列中执行线性插值和样条插值。
如何做到这一点...
让我们首先导入所需的库和时间序列数据集。
-
让我们导入
pandas和matplotlib:import matplotlib.pyplot as plt import pandas as pd -
让我们加载在 技术要求 部分描述的时间序列数据:
df = pd.read_csv( "air_passengers.csv", parse_dates=["ds"], index_col=["ds"], )
注意
你可以绘制时间序列以找到数据缺口,就像我们在 实现前向和后向填充 菜谱的 步骤 3 中所做的那样。
-
让我们通过线性插值来插补缺失数据:
df_imputed = df.interpolate(method="linear")
注意
如果行之间的时间间隔不均匀,则应将 method 设置为 time 以实现线性拟合。
你可以通过执行 df_imputed.isnull().sum() 来验证缺失数据的缺失。
-
让我们现在绘制完整数据集,并将用于插补的值作为虚线叠加:
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")之前的代码返回以下图表,其中我们可以看到用于替换
nan的值作为时间序列连续线之间的虚线:

图 1.8 – 通过在最后和下一个有效数据点之间进行线性插值替换缺失值的时间序列数据(虚线)
-
或者,我们可以通过进行样条插值来插补缺失数据。我们将使用二次多项式:
df_imputed = df.interpolate(method="spline", order=2)如果我们绘制插补数据集,并像在 步骤 4 中那样叠加插补值,我们将看到以下图表:

图 1.9 – 通过在最后和下一个有效数据点之间拟合二次多项式替换缺失值的时间序列数据(虚线)
注意
改变用于插值的二次多项式的度数,以查看替换值的变化。
我们现在已经获得了可以用于分析和建模的完整数据集。
它是如何工作的...
pandas 的 interpolate() 方法通过使用插值方法在范围内填充缺失值。当我们设置 method 为 linear 时,interpolate() 将所有数据点视为等距,并在缺失数据的区间内拟合一条线,连接最后一个和下一个有效点。
注意
如果你想要执行线性插值,但你的数据点不是等距的,请将 method 设置为 time。
然后,我们通过将method设置为spline和order设置为2,使用二次多项式进行样条插值。
pandas的interpolate()在内部使用scipy.interpolate.interp1d和scipy.interpolate.UnivariateSpline,因此可以实现其他插值方法。有关更多详细信息,请查看 pandas 文档,链接为pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.interpolate.html。
参见
虽然插值旨在比前向填充和后向填充获得更好的缺失数据估计,但如果时间序列显示出强烈的趋势和季节性,这些估计可能仍然不准确。为了获得这些类型时间序列中缺失数据的更好估计,请查看在www.trainindata.com/p/feature-engineering-for-forecasting的时间序列特征工程课程中关于时间序列分解后插值的说明。
通过链式方程进行多变量插补
与单变量插补不同,多变量插补方法使用多个变量来估计缺失值。链式方程多变量插补(MICE)将每个有缺失值的变量建模为数据集中剩余变量的函数。该函数的输出用于替换缺失数据。
MICE 包括以下步骤:
-
首先,它对每个有缺失数据的变量执行简单的单变量插补。例如,中位数插补。
-
接下来,它选择一个特定的变量,比如
var_1,并将缺失值重新设置为缺失。 -
它训练一个模型,使用其他变量作为输入特征来预测
var_1。 -
最后,它用模型的输出替换
var_1的缺失值。
MICE 对剩余的每个变量重复步骤 2到4。
当所有变量都已建模时,一个插补周期结束。MICE 执行多个插补周期,通常是 10 次。也就是说,我们对每个变量重复步骤 2到4共 10 次。其想法是,到周期结束时,我们应该找到了每个变量的最佳可能缺失数据估计。
注意
多变量插补在不想扭曲变量分布的情况下,可以作为一个有用的替代单变量插补的方法。当我们对拥有良好估计的缺失数据感兴趣时,多变量插补也非常有用。
在这个菜谱中,我们将使用 scikit-learn 实现 MICE。
如何操作...
要开始这个菜谱,让我们导入所需的库并加载数据:
-
让我们导入所需的 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 ) -
让我们从技术要求部分描述的数据集中加载一些数值变量:
variables = [ "A2", "A3", "A8", "A11", "A14", "A15", "target"] data = pd.read_csv( "credit_approval_uci.csv", usecols=variables) -
让我们将数据分为训练集和测试集:
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, ) -
让我们使用贝叶斯回归创建一个 MICE 填充器,指定迭代周期数,并设置
random_state以实现可重复性:imputer = IterativeImputer( estimator= BayesianRidge(), max_iter=10, random_state=0, ).set_output(transform="pandas")
注意
IterativeImputer()包含其他有用的参数。例如,我们可以使用initial_strategy参数指定第一个填充策略。我们可以从均值、中位数、众数或任意填充中进行选择。我们还可以指定我们想要如何循环变量,是随机循环还是从缺失值最少的变量到缺失值最多的变量。
-
让我们拟合
IterativeImputer(),以便它训练估计器来预测每个变量的缺失值:imputer.fit(X_train)
注意
我们可以使用任何回归模型,通过IterativeImputer()来估计缺失数据。
-
最后,让我们在训练集和测试集中填充缺失值:
X_train_t = imputer.transform(X_train) X_test_t = imputer.transform(X_test)
注意
为了证实缺失数据的缺乏,我们可以执行X_train_t.isnull().sum()。
为了总结这个配方,让我们使用简单的单变量填充方法来填充变量,并比较对变量分布的影响。
-
让我们设置 scikit-learn 的
SimpleImputer()以执行均值填充,然后转换数据集: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) -
让我们现在制作 MICE 填充后的
A3变量的直方图,然后是相同变量均值填充后的直方图: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 – 使用 MICE 填充(顶部)或均值填充(底部)后的变量 A3 的直方图,显示了后者导致的变量分布的扭曲
它是如何工作的...
在这个配方中,我们使用scikit-learn的IterativeImputer()执行了多元填充。当我们拟合模型时,IterativeImputer()执行了我们在配方的介绍中描述的步骤。也就是说,它使用均值填充所有变量。然后它选择一个变量,将其缺失值重新设置为缺失。最后,它拟合一个贝叶斯回归器来根据其他变量估计该变量。它对每个变量重复此过程。这是填充的一个周期。我们将它设置为重复此过程 10 次。到这个过程的最后,IterativeImputer()已经训练了一个贝叶斯回归器,用于根据数据集中的其他变量预测每个变量的值。通过transform(),它使用这些贝叶斯模型的预测来填充缺失数据。
IterativeImputer()只能基于数值变量在数值变量中填充缺失数据。如果您想使用分类变量作为输入,您需要先对其进行编码。然而,请注意,它只会执行回归。因此,它不适用于估计离散或分类变量中的缺失数据。
参见
要了解更多关于 MICE 的信息,请查看以下资源:
-
一种用于使用一系列回归模型乘性填充缺失值的多变量技术:
www.researchgate.net/publication/244959137 -
链式方程多重填充:它是怎样的,又是如何工作的?:
www.jstatsoft.org/article/download/v045i03/550
使用最近邻估计缺失数据
使用K-最近邻(KNN)进行填充涉及通过考虑其最近邻的值来估计数据集中的缺失值,其中数据点之间的相似性基于距离度量,例如欧几里得距离。它将缺失值分配给最近邻值的平均值,并按其距离进行加权。
考虑以下包含 4 个变量(列)和 11 个观测值(行)的数据集。我们希望填充第二个变量第五行的缺失值。首先,我们找到该行的 k 个最近邻,在我们的例子中,k=3,它们通过矩形框(中间面板)突出显示。接下来,我们取最近邻变量 2 的平均值。

图 1.11 – 显示待填充值(深色框)、待填充值的三条最近行(方形框)以及用于填充平均值的考虑值
填充的值由(value1 × w1 + value2 × w2 + value3 × w3)/ 3 给出,其中 w1、w2 和 w3 与邻居到要填充的数据的距离成比例。
在本配方中,我们将使用 scikit-learn 执行 KNN 填充。
如何操作...
为了继续执行配方,让我们导入所需的库并准备数据:
-
让我们导入所需的库、类和函数:
import matplotlib.pyplot as plt import pandas as pd from sklearn.model_selection import train_test_split from sklearn.impute import KNNImputer -
让我们加载技术要求部分中描述的数据集(仅包含一些数值变量):
variables = [ "A2", "A3", "A8", "A11", "A14", "A15", "target"] data = pd.read_csv( "credit_approval_uci.csv", usecols=variables, ) -
让我们将数据分为训练集和测试集:
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, ) -
让我们设置填充器,用其最近五个邻居的加权平均值替换缺失数据:
imputer = KNNImputer( n_neighbors=5, weights="distance", ).set_output(transform="pandas")
注意
替换值可以计算为 k 个最近邻的均匀平均值,通过将weights设置为uniform,或者如我们在配方中所做的那样,作为加权平均值。权重基于邻居到要填充的观测的距离。最近的邻居权重更大。
-
查找最近邻:
imputer.fit(X_train) -
用邻居显示的值的加权平均值替换缺失值:
X_train_t = imputer.transform(X_train) X_test_t = imputer.transform(X_test)
结果是一个 pandas DataFrame,其中缺失数据已被替换。
它是如何工作的...
在这个方法中,我们用每个观测值的 k 个最近邻的平均值替换了缺失数据。我们设置了KNNImputer()来根据欧几里得距离找到每个观测值的五个最近邻。替换值被估计为要插补的变量五个最近邻显示的值的加权平均值。通过transform()方法,插补器计算了替换值并替换了缺失数据。
第二章:对分类变量进行编码
Home owner变量具有owner和non-owner的值,是分类变量,同样Marital status变量具有never married、married、divorced和widowed的值,也是分类变量。在一些分类变量中,标签具有内在顺序;例如,在Student's grade变量中,A、B、C和Fail的值是有序的,其中A是最高等级,Fail是最低等级。这些被称为City变量,具有London、Manchester、Bristol等值。
分类变量的值通常编码为字符串。为了训练大多数机器学习模型,我们需要将这些字符串转换为数字。用数字替换字符串的行为称为分类编码。在本章中,我们将讨论多种分类编码方法。
本章将涵盖以下食谱:
-
通过单热编码创建二进制变量
-
对频繁类别执行单热编码
-
将类别替换为计数或观察频率
-
将类别替换为序数
-
基于目标值进行序数编码
-
实现目标均值编码
-
使用证据权重进行编码
-
对稀有或罕见类别进行分组
-
执行二进制编码
技术要求
在本章中,我们将使用Matplotlib、pandas、NumPy、scikit-learn、feature-engine和 Category Encoders Python 库。如果您需要安装 Python,免费的 Anaconda Python 发行版([www.anaconda.com/](https://www.anaconda.com/))包括大多数数值计算库。
可以使用pip安装feature-engine:
pip install feature-engine
如果您使用 Anaconda,可以使用conda安装feature-engine:
conda install -c conda-forge feature_engine
要安装 Category Encoders,请使用以下pip命令:
pip install category_encoders
我们将使用来自UCI 机器学习仓库的Credit Approval数据集(https://archive.ics.uci.edu/),该数据集根据 CC BY 4.0 创意共享许可:creativecommons.org/licenses/by/4.0/legalcode。您可以在以下链接找到数据集:archive.ics.uci.edu/dataset/27/credit+approval。
我已下载并修改了如本笔记本所示的数据:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch02-categorical-encoding/credit-approval-dataset.ipynb。
您可以在附带的 GitHub 仓库中找到修改后的数据集副本:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch02-categorical-encoding/。
注意
在对分类变量进行编码之前,你可能想要填充它们的缺失数据。查看第一章,填充 缺失数据的方法。
通过单热编码创建二进制变量
如果类别存在,则为1,否则为0。
以下表格显示了Smoker变量的单热编码表示,其中包含Smoker和Non-Smoker类别:

图 2.1 – Smoker 变量的单热编码表示
如*图 2**.1 所示,从Smoker变量中,我们可以推导出一个二进制变量Smoker,对于吸烟者显示值为1,或者推导出一个Non-Smoker的二进制变量,对于不吸烟者显示值为1。
对于具有red,blue和green值的Color分类变量,我们可以创建三个变量,分别称为red,blue和green。如果观察结果对应相应的颜色,这些变量将被分配值为1,如果不对应,则为0。
一个具有k个唯一类别的分类变量可以使用k-1个二进制变量进行编码。对于Smoker变量,k是2,因为它包含两个标签(Smoker和Non-Smoker),所以我们只需要一个二进制变量(k - 1 = 1)来捕捉所有信息。对于Color变量,它有 3 个类别(k = 3;red,blue和green),我们需要 2 个(k - 1 = 2)二进制变量来捕捉所有信息,以便以下发生:
-
如果观察结果是红色,它将被
red变量捕获(red=1,blue=0) -
如果观察结果是蓝色,它将被
blue变量捕获(red=0,blue=1) -
如果观察结果是绿色,它将被
red和blue的组合捕获(red=0,blue=0)
将编码到k-1个二进制变量非常适合线性模型。在某些情况下,我们可能更喜欢使用k个二进制变量对分类变量进行编码:
-
在训练决策树时,因为它们不会同时评估整个特征空间
-
在递归选择特征时
-
在确定变量中每个类别的相对重要性时
在这个菜谱中,我们将比较pandas,scikit-learn和feature-engine的单热编码实现。
如何做到...
首先,让我们进行一些导入并准备好数据:
-
从
scikit-learn导入pandas和train_test_split函数:import pandas as pd from sklearn.model_selection import train_test_split -
让我们加载 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, ) -
让我们检查
A4变量的唯一类别:X_train["A4"].unique()我们可以在以下输出中看到
A4的独特值:A4 into *k-1* binary variables using pandas and then inspect the first five rows of the resulting DataFrame:dummies = pd.get_dummies(
X_train["A4"], drop_first=True)
dummies.head()
注意
使用pandas的get_dummies(),我们可以通过dummy_na参数忽略或编码缺失数据。通过将dummy_na=True设置,缺失数据将编码在一个新的二进制变量中。要将变量编码为k个虚拟变量,请使用drop_first=False。
这里,我们可以看到步骤 5的输出,其中每个标签现在都是一个二进制变量:
Missing l u y
596 False False True False
303 False False True False
204 False False False True
351 False False False True
118 False False True False
-
现在,让我们将所有分类变量编码为k-1个二进制变量:
X_train_enc = pd.get_dummies(X_train, drop_first=True) X_test_enc = pd.get_dummies(X_test, drop_first=True)
备注
pandas的get_dummies()默认情况下将对象、字符串或类别类型的所有变量编码。要编码变量子集,请将变量名列表传递给columns参数。
-
让我们检查结果 DataFrame 的前五行:
X_train_enc.head()
备注
当编码多个变量时,get_dummies()捕获变量名 – 比如,A1 – 并在类别名前放置一个下划线来标识结果二进制变量。
我们可以在以下输出中看到二进制变量:

图 2.2 – 一个转换后的 DataFrame,显示了数值变量随后是分类变量的独热编码表示
备注
pandas的get_dummies()将为 DataFrame 中看到的每个类别创建一个二进制变量。因此,如果训练集中的类别比测试集中的多,get_dummies()将在转换后的训练集中返回比测试集更多的列,反之亦然。为了避免这种情况,最好使用scikit-learn或feature-engine进行独热编码。
让我们使用scikit-learn进行独热编码。
-
让我们导入
scikit-learn中的编码器和ColumnTransformer:from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer -
让我们创建一个包含分类变量名的列表:
cat_vars = X_train.select_dtypes( include="O").columns.to_list() -
让我们设置编码器以创建k-1个二进制变量:
encoder = OneHotEncoder(drop="first", sparse_output=False)
备注
要将变量编码为k个虚拟变量,将drop参数设置为None。要将仅二进制变量编码为k-1,将drop参数设置为if_binary。后者很有用,因为将二进制变量编码为k个虚拟变量是多余的。
-
让我们将编码限制在分类变量上:
ct = ColumnTransformer( [("encoder", encoder, cat_vars)], remainder="passthrough", force_int_remainder_cols=False, ).set_output(transform="pandas") -
让我们调整编码器,使其能够识别要编码的类别:
ct.fit(X_train) -
让我们检查将被二进制变量表示的类别:
ct.named_transformers_["encoder"].categories_变换器将为以下类别添加二进制变量:

图 2.3 – 将被编码为二进制变量的类别数组(每个变量一个数组)
备注
scikit-learn的OneHotEncoder()只会对从训练集中学习到的类别进行编码。如果测试集中有新的类别,我们可以通过设置handle_unknown参数为ignore、error或infrequent_if_exists来指示编码器忽略它们、返回错误或用不常见的类别替换它们。
-
让我们编码分类变量:
X_train_enc = ct.transform(X_train) X_test_enc = ct.transform(X_test)确保通过执行
X_test_enc.head()来检查结果。 -
为了熟悉输出,让我们打印结果 DataFrame 的变量名称:
ct.get_feature_names_out()在以下图像中,我们可以看到转换后的 DataFrame 中的变量名称:

图 2.4 – 结果 DataFrame 中变量的名称数组
注意
ColumnTransformer()在转换过程中会更改变量的名称和顺序。如果变量被编码,它将附加encoder前缀;如果变量未被修改,它将附加remainder前缀。
为了总结这个菜谱,让我们使用feature-engine进行 one-hot 编码。
-
让我们从
f``eature-engine导入编码器:from feature_engine.encoding import OneHotEncoder -
让我们设置编码器,使其返回k-1个二进制变量:
ohe_enc = OneHotEncoder(drop_last=True)
注意
feature-engine的OneHotEncoder()默认编码所有分类变量。要编码变量的子集,请传递变量名称列表:OneHotEncoder(variables=["A1", "A4"])。要编码数值变量,将ignore_format参数设置为True或将变量转换为对象类型。
-
让我们将编码器拟合到训练集中,以便它学习要编码的类别和变量:
ohe_enc.fit(X_train)
注意
要将二进制变量编码为k-1,并将其他分类变量编码为k个虚拟变量,将drop_last_binary参数设置为True。
-
让我们探索将要编码的变量:
ohe_enc.variables_转换器找到了并存储了对象或分类类型的变量,如下面的输出所示:
['A1', 'A4', 'A5', 'A6', 'A7', 'A9', 'A10', 'A12', 'A13'] -
让我们探索将创建虚拟变量的类别:
ohe_enc.encoder_dict_以下字典包含将编码到每个变量中的类别:
{'A1': ['a', 'b'], 'A4': ['u', 'y', 'Missing'], 'A5': ['g', 'p', 'Missing'], 'A6': ['c', 'q', 'w', 'ff', 'm', 'i', 'e', 'cc', 'x', 'd', 'k', 'j', 'Missing', 'aa'], 'A7': ['v', 'ff', 'h', 'dd', 'z', 'bb', 'j', 'Missing', 'n'], 'A9': ['t'], 'A10': ['t'], 'A12': ['t'], 'A13': ['g', 's']} -
让我们在训练集和测试集中编码分类变量:
X_train_enc = ohe_enc.transform(X_train) X_test_enc = ohe_enc.transform(X_test)如果我们执行
X_train_enc.head(),我们将看到以下 DataFrame:

图 2.5 – 转换后的 DataFrame,其中包含数值变量,后面跟着分类变量的 one-hot 编码表示
注意到A4分类变量被替换为A4_u、A4_y等等。
注意
我们可以通过执行ohe_enc.get_feature_names_out()来获取转换数据集中所有变量的名称。
它是如何工作的...
在这个菜谱中,我们使用pandas、scikit-learn和feature-engine对分类变量进行了 one-hot 编码。
pandas 的 get_dummies() 将类别变量替换为表示每个类别的二进制变量集合。当在整个数据集上使用时,它返回数值变量,随后是每个变量类型为对象、字符串或类别的每个变量中看到的每个类别的独热编码表示。
注意
pandas 会为数据集中出现的每个类别返回二进制变量。在实际应用中,为了避免数据泄露并预测部署情况,我们只想为训练集中出现的类别返回虚拟变量。因此,使用 scikit-learn 和 feature-engine 更为安全。
OneHotEncoder() 从 scikit-learn 或 feature-engine 中学习,在应用 fit() 时从训练集中识别出应该用二进制变量表示的类别。使用 transform(),scikit-learn 仅返回二进制变量,而 feature-engine 则返回数值变量,随后是类别变量的独热编码表示。
scikit-learn 的 OneHotEncoder() 默认对所有变量进行编码。为了限制编码只针对类别变量,我们使用了 ColumnTransformer()。我们将 transform() 的输出设置为 pandas,以获得结果数据作为 DataFrame。
注意
独热编码适用于线性模型。它还会扩展特征空间。如果你的数据集中包含许多类别变量或高度基数变量,你可以通过仅编码最频繁的类别来限制二进制变量的数量。你可以像我们在 对频繁类别进行独热编码 食谱中描述的那样,使用 scikit-learn 和 feature-engine 自动完成此操作。
还有更多...
我们还可以使用 Category Encoders Python 库进行独热编码:contrib.scikit-learn.org/category_encoders/onehot.html。
为了限制二进制变量的数量,我们可以选择要编码的类别和要忽略的类别;查看以下文章中的 Python 示例:https://www.blog.trainindata.com/one-hot-encoding-categorical-variables/。
对频繁类别进行独热编码
独热编码使用二进制变量表示每个变量的类别。因此,对高度基数变量或具有多个类别特征的集合进行独热编码可以显著扩展特征空间。这反过来可能会增加使用机器学习模型的计算成本或降低其性能。为了减少二进制变量的数量,我们可以对最频繁的类别进行独热编码。对顶级类别进行独热编码相当于将剩余的、较少出现的类别视为一个唯一的类别。
在本食谱中,我们将使用 pandas、Scikit-learn 和 feature-engine 实现对最流行类别的独热编码。
如何实现...
首先,让我们导入必要的 Python 库并准备好数据集:
-
导入所需的 Python 库、函数和类:
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split -
让我们加载信用审批数据集并将其分为训练集和测试集:
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, )
备注
在训练集中需要确定最频繁的类别。这是为了避免数据泄露。
-
让我们检查
A6变量的唯一类别:X_train["A6"].unique()A6的唯一值显示在以下输出中:A6, sort them in decreasing order, and then display the five most frequent categories:X_train["A6"].value_counts().sort_values(
ascending=False).head(5)
We can see the five most frequent categories and the number of observations per category in the following output:A6
c 93
q 56
w 48
i 41
ff 38
使用步骤 4中的代码通过列表推导式将 A6 放入一个列表中:
top_5 = [x for x in X_train[ " A6"].value_counts().sort_values( ascending=False).head(5).index ] -
让我们在训练集和测试集的副本中为每个顶级类别添加一个二元变量:
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) -
让我们在训练集中显示原始变量和编码变量
A6的前10行:X_train_enc[["A6"] + [f"A6_{ label}" for label in top_5]].head(10)在第 7 步的输出中,我们可以看到
A6变量,随后是二元变量:A6 A6_c A6_q A6_w A6_i A6_ff 596 c 1 0 0 0 0 303 q 0 1 0 0 0 204 w 0 0 1 0 0 351 ff 0 0 0 0 1 118 m 0 0 0 0 0 247 q 0 1 0 0 0 652 i 0 0 0 1 0 513 e 0 0 0 0 0 230 cc 0 0 0 0 0 scikit-learn. -
让我们导入编码器:
from sklearn.preprocessing import OneHotEncoder -
让我们设置编码器,以编码至少有
39个观察值的类别,并将编码的类别数量限制为5:encoder = OneHotEncoder( min_frequency=39, max_categories=5, sparse_output=False, ).set_output(transform="pandas") -
最后,让我们将转换器拟合到两个高基数变量,然后转换数据:
X_train_enc = encoder.fit_transform(X_train[ ['A6', 'A7']]) X_test_enc = encoder.transform(X_test[['A6', 'A7']])如果您执行
X_train_enc.head(),您将看到生成的 DataFrame:

图 2.6 – 包含至少有 39 个观察值的类别二元变量以及表示所有剩余类别的额外二元变量的转换 DataFrame
为了总结这个配方,让我们使用feature-engine对最频繁的类别进行编码。
-
让我们设置一热编码器来编码
A6和A7变量中最频繁的五个类别:From feature_engine.encoding import OneHotEncoder ohe_enc = OneHotEncoder( top_categories=5, variables=["A6", "A7"] )
备注
要编码的频繁类别数量由用户任意确定。
-
让我们将编码器拟合到训练集,以便它学习并存储
A6和A7变量的最频繁类别:ohe_enc.fit(X_train) -
最后,让我们在训练集和测试集中对
A6和A7进行编码:X_train_enc = ohe_enc.transform(X_train) X_test_enc = ohe_enc.transform(X_test)您可以通过执行
X_train_enc.head()来查看转换 DataFrame 中的新二元变量。您还可以通过执行ohe_enc.encoder_dict_来找到编码器学习到的前五个类别。
它是如何工作的...
在这个配方的第一部分,我们处理了A6分类变量。我们使用pandas的unique()检查其唯一类别。接下来,我们使用pandas的value_counts()计算每个类别的观测值数量,它返回一个以类别为索引、观测值数量为值的pandas系列。然后,我们使用pandas的sort_values()将类别从观测值最多到最少的顺序排序。我们然后使用pandas的head()将系列缩减到最流行的五个类别。我们使用这个系列在一个列表推导式中捕获最频繁类别的名称。之后,我们遍历每个类别,并使用 NumPy 的where(),如果观测值显示了该类别,则创建值为1的二元变量,否则为0。
我们在“通过独热编码创建二元变量”的配方中讨论了如何使用scikit-learn和feature-engine。在这里,我将只强调编码最频繁类别所需的参数。
要使用scikit-learn编码频繁类别,我们将min_frequency参数设置为39。因此,在少于39个观测值中出现的类别将被组合成一个额外的二元变量,称为infrequent_sklearn。
要使用feature-engine编码频繁类别,我们将top_categories参数设置为5。因此,创建的转换器只为 5 个最频繁的类别创建二元变量。较少见的类别将在所有二元变量中显示为0。
还有更多...
这个配方基于 2009 年知识发现与数据(KDD)挖掘杯的获胜方案,使用集成选择赢得 KDD Cup Orange 挑战(http://proceedings.mlr.press/v7/niculescu09/niculescu09.pdf),其中作者将独热编码限制为每个变量的 10 个最频繁类别。
用观测值的计数或频率替换类别
在“计数与观测值的计数或频率”或频率编码中,我们将类别替换为显示该类别的观测值的计数或分数。也就是说,如果有 10 个观测值中的 100 个显示Color变量的blue类别,我们在进行计数编码时将blue替换为10,或者在执行频率编码时替换为0.1。这些编码方法在类别频率与目标之间存在关系时很有用。例如,在销售中,产品的频率可能表明其受欢迎程度。
注意
如果两个不同的类别在相同数量的观测值中出现,它们将被相同的值替换,这可能会导致信息丢失。
在这个配方中,我们将使用pandas和feature-engine执行计数和频率编码。
如何做到这一点...
我们将首先使用pandas编码一个变量,然后我们将使用feature-engine自动化这个过程:
-
让我们从导入开始:
import pandas as pd from sklearn.model_selection import train_test_split from feature_engine.encoding import CountFrequencyEncoder -
让我们加载信用批准数据集并将其分为训练集和测试集:
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, ) -
让我们通过计数或观察频率来捕获
A7变量每个类别的观察数量,并将其存储在一个字典中:counts = X_train["A7"].value_counts().to_dict()
注意
要找到频率,请执行 X_train["A7"].value_counts(normalize=True).to_dict()。
如果我们执行 print(counts),我们将看到 A7 每个类别的观察计数:
{'v': 277, 'h': 101, 'ff': 41, 'bb': 39, 'z': 7, 'dd': 5, 'j': 5, 'Missing': 4, 'n': 3, 'o': 1}
-
让我们在数据集的副本中将
A7中的类别替换为计数: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)继续执行
X_train_enc.head()来检查类别是否已被替换为计数。要将此过程应用于多个变量,我们可以使用
feature-engine。 -
让我们设置编码器,使其使用观察计数来编码所有分类变量:
count_enc = CountFrequencyEncoder( encoding_method="count", variables=None, )
注意
CountFrequencyEncoder() 将自动找到并编码训练集中的所有分类变量。要仅编码变量子集,请将变量名称列表传递给 variables 参数。要使用频率进行编码,请使用 encoding_method="frequency"。
-
让我们将编码器拟合到训练集,以便它存储每个变量每个类别的观察数量:
count_enc.fit(X_train) -
编码器自动找到了分类变量。让我们来看看:
count_enc.variables_之前的命令返回训练集中分类变量的名称:
['A1', 'A4', 'A5', 'A6', 'A7', 'A9', 'A10', 'A12', 'A13'] -
让我们打印每个变量每个类别的观察计数:
count_enc.encoder_dict_之前的属性存储了将用于替换类别的映射:

图 2.7 – 包含每个变量每个类别的观察数量的字典;这些值将用于编码分类变量
-
最后,让我们在训练和测试集中使用“计数或观察频率”将类别替换为计数:
X_train_enc = count_enc.transform(X_train) X_test_enc = count_enc.transform(X_test)
通过执行 X_train_enc.head() 来检查结果。编码器返回 pandas DataFrame,其中分类变量的字符串被观察计数替换,使变量准备好在机器学习模型中使用。
它是如何工作的...
在这个菜谱中,我们使用 pandas 和 feature-engine 将类别替换为观察计数。
使用 pandas 的 value_counts(),我们确定了 A7 变量每个类别的观察数量,并通过 pandas 的 to_dict(),将这些值捕获在“计数或观察频率”字典中,其中每个键是一个唯一的类别,每个值是该类别的观察数量。通过 pandas 的 map() 和使用此字典,我们在训练和测试集中将类别替换为观察计数。
注意
编码的观测值数量应从训练集中获取,以避免数据泄露。请注意,测试集中的新类别将没有对应的映射,因此将被替换为nan。为了避免这种情况,请使用feature-engine。或者,您可以将nan替换为0。
要使用feature-engine执行计数编码,我们使用了CountFrequencyEncoder()并将encoding_method设置为'count'。我们将variables参数设置为None,以便编码器自动找到数据集中的所有分类变量。使用fit(),转换器找到了分类变量,并将每个类别的观测值计数存储在encoder_dict_属性中。使用transform(),转换器用计数替换了类别,返回一个pandas DataFrame。
注意
如果测试集中有训练集中不存在的类别,编码器将默认引发错误。您可以使其忽略它们,在这种情况下,它们将显示为nan,或者将它们编码为0。
参见
您还可以使用 Python 库 Category Encoders 执行计数和频率编码:contrib.scikit-learn.org/category_encoders/count.html。
要查看计数编码的一些有用应用,请参阅这篇文章:letsdatascience.com/frequency-encoding/。
用序数替换类别
序数编码包括用从1到k(或根据实现从0到k-1)的数字替换类别,其中k是变量的不同类别的数量。这些数字是任意分配的。序数编码更适合非线性机器学习模型,这些模型可以通过任意分配的数字来寻找与目标相关的模式。
在这个菜谱中,我们将使用pandas、scikit-learn和feature-engine执行序数编码。
如何操作...
首先,让我们进行导入并准备数据集:
-
导入
pandas和数据拆分函数:import pandas as pd from sklearn.model_selection import train_test_split -
让我们加载 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, ) -
要编码
A7变量,让我们创建一个类别到整数的字典:ordinal_mapping = {k: i for i, k in enumerate( X_train["A7"].unique(), 0) }如果我们执行
print(ordinal_mapping),我们将看到将替换每个类别的数字:{'v': 0, 'ff': 1, 'h': 2, 'dd': 3, 'z': 4, 'bb': 5, 'j': 6, 'Missing': 7, 'n': 8, 'o': 9} -
现在,让我们在 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)执行
print(X_train["A7"].head())以查看上一操作的结果。接下来,我们将使用
scikit-learn执行序数编码。 -
让我们导入所需的类:
from sklearn.preprocessing import OrdinalEncoder from sklearn.compose import ColumnTransformer
注意
不要混淆OrdinalEncoder()和来自scikit-learn的LabelEncoder()。前者旨在编码预测特征,而后者旨在修改目标变量。
-
让我们设置编码器:
enc = OrdinalEncoder() -
让我们创建一个包含需要编码的分类变量的列表:
cat_vars = X_train.select_dtypes(include="O").columns.to_list() -
让我们将编码限制在分类变量上:
ct = ColumnTransformer( [("encoder", enc, cat_vars)], remainder="passthrough", force_int_remainder_cols=False, ).set_output(transform="pandas")
注意
记得将remainder设置为"passthrough",以便ColumnTransformer()返回未转换的变量。
-
让我们将编码器拟合到训练集,以便它创建并存储类别到数字的表示:
ct.fit(X_train)
注意
通过执行ct.named_transformers_["encoder"].categories_,您可以可视化每个变量的唯一类别。
-
现在,让我们对训练集和测试集中的分类变量进行编码:
X_train_enc = ct.transform(X_train) X_test_enc = ct.transform(X_test)执行
X_train_enc.head()来查看生成的 DataFrame。
注意
ColumnTransformer()将通过在变量名后附加encoder来标记已编码的变量。未修改的变量显示remainder前缀。
现在,让我们使用feature-engine进行顺序编码。
-
让我们导入编码器:
from feature_engine.encoding import OrdinalEncoder -
让我们设置编码器,使其在步骤 7中指定的分类变量中将类别替换为任意整数:
enc = OrdinalEncoder( encoding_method="arbitrary", variables=cat_vars, )
注意
如果variables参数为None,feature-engine的OrdinalEncoder()将自动查找并编码所有分类变量。或者,它将编码列表中指示的变量。此外,它可以根据目标平均值分配整数(参见基于目标 值 进行顺序编码的配方)。
-
让我们将编码器拟合到训练集,以便它学习并存储类别到整数的映射:
enc.fit(X_train)
注意
类别到整数的映射存储在encoder_dict_属性中,可以通过执行enc.encoder_dict_来访问。
-
最后,让我们将训练集和测试集中的分类变量进行编码:
X_train_enc = enc.transform(X_train) X_test_enc = enc.transform(X_test)
feature-engine返回pandas DataFrame,其中原始变量的值被数字替换,使 DataFrame 准备好在机器学习模型中使用。
它是如何工作的...
在这个配方中,我们将类别替换为任意分配的整数。
我们使用pandas的unique()找到A7变量的唯一类别。接下来,我们创建了一个类别到整数的字典,并将其传递给pandas的map(),以将A7中的字符串替换为整数。
接下来,我们使用scikit-learn的OrdinalEncoder()进行了顺序编码,并使用ColumnTransformer()将编码限制在分类变量上。通过fit(),转换器根据训练集中的类别创建了类别到整数的映射。通过transform(),类别被替换为整数。通过将remainder参数设置为passthrough,我们使ColumnTransformer()将未编码的变量连接到编码特征之后。
使用feature-engine进行有序编码时,我们使用了OrdinalEncoder(),表示整数应通过encoding_method任意分配,并通过variables参数传递了一个包含要编码的变量的列表。使用fit(),编码器将整数分配给每个变量的类别,这些类别存储在encoder_dict_属性中。然后,这些映射被transform()方法用于替换训练集和测试集中的类别,返回 DataFrames。
注意
当测试集中的类别不在训练集中时,它将没有映射到数字。scikit-learn的OrdinalEncoder()和feature-engine默认会引发错误。然而,它们都有选项用用户定义的值或-1替换未看到的类别。
scikit-learn的OrdinalEncoder()可以限制编码到具有最小频率的类别。feature-engine的OrdinalEncoder()可以根据目标平均值分配数字,正如我们将在下一个配方中看到的。
更多...
您也可以使用OrdinalEncoder()从Category Encoders进行有序编码。请查看contrib.scikit-learn.org/category_encoders/ordinal.html。
基于目标值进行有序编码
在前面的配方中,我们用整数替换了类别,这些整数是任意分配的。我们也可以根据目标值给类别分配整数。为此,首先,我们计算每个类别的目标值的平均值。接下来,我们按目标平均值从低到高对类别进行排序。最后,我们将数字分配给有序的类别,从第一个类别开始的0到最后一个类别的k-1。
这种编码方法在分类变量和响应变量之间创建了一个单调关系,因此使得变量更适合用于线性模型。
在这个配方中,我们将使用pandas和feature-engine在遵循目标值的同时对类别进行编码。
如何操作...
首先,让我们导入必要的 Python 库并准备好数据集:
-
导入所需的 Python 库、函数和类:
import pandas as pd import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split -
让我们加载 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, ) -
让我们在
A7中确定每个类别的平均目标值,然后按目标值从低到高排序类别:y_train.groupby(X_train["A7"]).mean().sort_values()以下是在前面的命令中的输出:
A7 o 0.000000 ff 0.146341 j 0.200000 dd 0.400000 v 0.418773 bb 0.512821 h 0.603960 n 0.666667 z 0.714286 Missing 1.000000 Name: target, dtype: float64 -
现在,让我们重复第 3 步的计算,但这次,让我们保留有序的类别名称:
ordered_labels = y_train.groupby( X_train["A7"]).mean().sort_values().index要显示前面命令的输出,我们可以执行
print(ordered_labels):Index(['o', 'ff', 'j', 'dd', 'v', 'bb', 'h', 'n', 'z', 'Missing'], dtype='object', name='A7')。 -
让我们创建一个类别到整数的字典,使用我们在 步骤 4 中创建的有序列表:
ordinal_mapping = { k: i for i, k in enumerate(ordered_labels, 0) }我们可以通过执行
print(ordinal_mapping)来可视化前面代码的结果:A7 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)
注意
如果测试集包含训练集中不存在的类别,前面的代码将引入 np.nan。
为了可视化这种编码的效果,让我们绘制编码前后 A7 变量的类别与目标之间的关系。
-
让我们绘制
A7变量每个类别的目标响应平均值:y_train.groupby(X_train["A7"]).mean().plot() plt.title("Relationship between A7 and the target") plt.ylabel("Mean of target") plt.show()我们可以在以下图表中看到
A7类别和目标之间的非单调关系:

图 2.8 – 编码前 A7 每个类别的目标值平均值
-
让我们绘制编码变量中每个类别的目标值平均值:
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 每个类别的目标值平均值
现在,让我们使用 feature-engine 执行有序顺序编码。
-
让我们导入编码器:
from feature_engine.encoding import OrdinalEncoder -
接下来,让我们设置编码器,使其根据目标平均值将整数分配给数据集中的所有分类变量:
ordinal_enc = OrdinalEncoder( encoding_method="ordered", variables=None)
注意
OrdinalEncoder() 将自动查找并编码所有分类变量。要限制编码到变量的子集,将它们的名称作为列表传递给 variables 参数。要编码数值变量,设置 ignore_format=True。
-
让我们将编码器拟合到训练集,以便它找到分类变量,然后存储类别和整数映射:
ordinal_enc.fit(X_train, y_train) -
最后,让我们在训练集和测试集中将类别替换为数字:
X_train_enc = ordinal_enc.transform(X_train) X_test_enc = ordinal_enc.transform(X_test)
注意
你将在 encoder_dict_ 属性中找到替换每个类别的数字。
通过执行 X_train_enc.head() 来查看转换的输出。
它是如何工作的...
在这个配方中,我们根据目标平均值将类别替换为整数。
在本食谱的第一部分,我们处理了A7分类变量。使用pandas的groupby()函数,我们根据A7的分类对数据进行分组,并使用pandas的mean()函数确定每个分类的目标均值。接下来,我们使用pandas的sort_values()函数按目标均值响应从低到高对分类进行排序。这个操作的输出是一个pandas系列,其中分类作为索引,目标均值作为值。使用pandas的index,我们将排序后的分类存储在一个数组中;然后,使用 Python 字典推导式创建了一个分类到整数的字典对。最后,我们使用这个字典通过pandas的map()函数将分类替换为整数。
注意
为了避免数据泄露,我们从训练集中确定分类到整数的映射。
要使用feature-engine执行编码,我们使用了OrdinalEncoder(),将encoding_method设置为ordered。我们将变量参数设置为None,以便编码器自动检测数据集中的所有分类变量。使用fit(),编码器找到了分类变量,并根据目标均值值分配数字给它们的分类。分类变量的名称和分类到数字对的字典分别存储在variables_和encoder_dict_属性中。最后,使用transform(),我们在训练集和测试集中用数字替换了分类,返回pandas数据框。
参见
要查看使用分类编码器的本食谱的实现,请访问本书的 GitHub 仓库:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch02-categorical-encoding/Recipe-05-Ordered-ordinal-encoding.ipynb。
实现目标均值编码
均值编码或目标编码将每个分类映射到目标属性的预测概率。如果目标是二元的,数值映射是目标在给定分类值条件下的后验概率。如果目标是连续的,数值表示是给定分类值的目标的期望值。
在其最简单形式中,每个类别的数值表示由特定类别组的目标变量的平均值给出。例如,如果我们有一个City变量,类别为London、Manchester和Bristol,我们想要预测违约率(目标取值为0和1);如果London的违约率是 30%,我们将London替换为0.3;如果Manchester的违约率是 20%,我们将Manchester替换为0.2;依此类推。如果目标是连续的——比如说我们想要预测收入——那么我们将London、Manchester和Bristol替换为每个城市所赚取的平均收入。
从数学的角度来看,如果目标是二元的,替换值S的确定如下:

在这里,分子是类别i中具有目标值1的观测值的数量,分母是具有类别值i的观测值的数量。
如果目标是连续的,S,则由以下公式确定:

在这里,分子是类别i中观测值的总和,而
是类别i中的观测值总数。
这些公式在存在足够多的每个类别值的观测值时提供了对目标估计的良好近似——换句话说,如果
很大。然而,在许多数据集中,将存在一些观测值中包含的类别。在这些情况下,从先前的公式中得出的目标估计可能不可靠。
为了减轻对罕见类别返回的估计不佳,目标估计可以确定为两种概率的混合:前一个公式返回的概率和基于整个训练的目标先验概率。这两个概率通过一个权重因子进行混合,该权重因子是类别组大小的函数:

在这个公式中,
是目标取值为 1 的总案例数,N 是训练集的大小,而 𝜆 是权重因子。
当类别组较大时,𝜆 趋向于 1,因此方程的第一项被赋予更多的权重。当类别组较小时,𝜆 趋向于 0,因此估计主要由方程的第二项驱动——即目标的先验概率。换句话说,如果组大小较小,知道类别值并不能告诉我们关于目标值的信息。
权重因子,𝜆,在不同的开源实现中确定方式不同。在 Category Encoders 中,𝜆 是组大小,k,以及平滑参数,f 的函数,它控制着前一个方程中第一项和第二项之间的转换速率:

在这里,k 是我们完全信任方程第一项的最小大小的一半。f 参数由用户任意选择或通过优化选择。
在 scikit-learn 和 feature-engine 中,𝜆 是整个数据集和类别内的目标方差的函数,并按以下方式确定:

在这里,t 是整个数据集的目标方差,而 s 是类别内的目标方差。两种实现方式是等效的,但了解这些方程式很重要,因为它们将帮助你在变压器中设置参数。
注意
平均编码旨在在不扩展特征空间的情况下编码高度基数分类变量。更多详情,请参阅以下文章:Micci-Barreca D. A.,《用于分类和预测问题中高基数分类属性的前处理方案》。ACM SIGKDD Explorations Newsletter,2001。
在这个配方中,我们将使用 scikit-learn 和 feature-engine 执行平均编码。
如何操作...
让我们从这个配方开始:
-
导入
pandas和数据拆分函数:import pandas as pd from sklearn.model_selection import train_test_split -
让我们加载 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, ) -
让我们导入
scikit-learn中的变压器:from sklearn.preprocessing import TargetEncoder from sklearn.compose import ColumnTransformer -
让我们创建一个包含分类变量名称的列表:
cat_vars = X_train.select_dtypes( include="O").columns.to_list() -
让我们将编码器设置为使用目标方差来确定权重因子,正如配方开头所述:
enc = TargetEncoder(smooth="auto", random_state=9) -
让我们将插补限制为分类变量:
ct = ColumnTransformer( [("encoder", enc, cat_vars)], remainder="passthrough", ).set_output(transform="pandas") -
让我们拟合编码器并转换数据集:
X_train_enc = ct.fit_transform(X_train, y_train) X_test_enc = ct.transform(X_test)通过执行
X_train_enc.head()检查结果。
注意
scikit-learn 的 TargetEncoder() 的 fit_transform() 方法与 fit().transform() 的应用不等价。使用 fit_transform(),生成的数据集基于交叉验证方案训练折的局部拟合进行编码。这个功能是故意设计的,以防止机器学习模型过度拟合训练集。
现在,让我们使用 feature-engine 执行目标编码:
-
让我们导入编码器:
from feature_engine.encoding import MeanEncoder -
让我们设置目标平均编码器,在应用平滑的同时编码所有分类变量:
mean_enc = MeanEncoder(smoothing="auto", variables=None)
注意
MeanEncoder() 默认不应用平滑。确保将其设置为 auto 或整数以控制先验和后验目标估计之间的混合。
-
让我们将变压器拟合到训练集,以便它学习并存储每个变量每个类别的平均目标值:
mean_enc.fit(X_train, y_train) -
最后,让我们对训练集和测试集进行编码:
X_train_enc = mean_enc.transform(X_train) X_test_enc = mean_enc.transform(X_test)
注意
类别到数字对存储在 encoder_dict_ 属性中的字典字典中。要显示存储的参数,执行 mean_enc.encoder_dict_。
它是如何工作的...
在这个配方中,我们使用 scikit-learn 和 feature-engine 将类别替换为平均目标值。
要使用 scikit-learn 进行编码,我们使用了 TargetEncoder(),将 smooth 参数保留为其默认值 auto。这样,变压器使用目标方差来确定概率混合的权重因子。使用 fit(),变压器学习它应该用来替换类别的值,而使用 transform(),它替换了类别。
注意,对于TargetEncoder(),fit()方法后面跟transform()方法并不返回与fit_transform()方法相同的数据集。后者基于交叉验证找到的映射来编码训练集。想法是在管道中使用fit_transform(),这样机器学习模型就不会过拟合。然而,这里变得有些复杂,存储在encodings_属性中的映射在fit()和fit_transform()之后是相同的,这是故意为之,以便当我们将transform()应用于新数据集时,无论我们是否将fit()或fit_transform()应用于训练集,我们都会获得相同的结果。
注意
未见过类别由scikit-learn的TargetEncoder()用目标均值进行编码。feature-engine的MeanEncoder()可以返回错误,用nan替换未见过类别,或者用目标均值替换。
使用feature-engine进行目标编码时,我们使用了MeanEncoder(),并将smoothing参数设置为auto。通过fit()方法,转换器找到了并存储了分类变量以及编码每个类别的值。通过transform()方法,它用数字替换了类别,返回pandas数据框。
还有更多...
如果你想使用pandas或类别编码器实现目标编码,请查看随附 GitHub 仓库中的笔记本:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch02-categorical-encoding/Recipe-06-Target-mean-encoding.ipynb。
当类别组较小时,有一种替代方法可以返回更好的目标估计。每个类别的替换值确定如下:

在这里,
是类别 i 的目标均值,而
是具有类别 i 的观测数。目标先验由 pY 给出,而 m 是权重因子。通过这种调整,我们唯一需要设置的参数是权重,m。如果 m 较大,则更重视目标的先验概率。这种调整会影响所有类别的目标估计,但对于观测数较少的类别影响更大,因为在这些情况下,m 可能比公式分母中的
大得多。
注意
这种方法是 Category Encoders 的 TargetEncoder() 的良好替代品,因为在 Category Encoders 的目标编码实现中,我们需要优化两个参数而不是一个(正如我们在 feature-engine 和 scikit-learn 中所做的那样)来控制平滑度。
要了解使用 MEstimateEncoder() 实现此编码方法的示例,请访问此书的 GitHub 仓库:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch02-categorical-encoding/Recipe-06-Target-mean-encoding.ipynb。
使用证据权重进行编码
证据权重(WoE)主要是在信用和金融行业开发的,旨在促进变量筛选和探索性分析,并构建更预测性的线性模型来评估贷款违约的风险。
WoE 是从基本概率比计算得出的:

在这里,正负分别指目标值的1或0,每个类别的正例比例是每个类别组正例总和除以训练集中正例总数。每个类别的负例比例是每个类别组负例总和除以训练集中负观察值的总数。
WOE 具有以下特点:
-
当p(正面) / p(负面) = 1时,WOE = 0;也就是说,如果结果是随机的。
-
当p(正面) > p(负面)时,WOE > 0。
-
当p(负面) > p(正面)时,WOE < 0。
这使我们能够直接可视化变量中类别的预测能力:WOE 越高,事件发生的可能性越大。如果 WOE 为正,则事件很可能发生。
逻辑回归模型基于预测变量X的二进制响应Y,假设X与Y的对数优势之间存在线性关系:

这里,log (p(Y=1)/p(Y=0)) 是优势比的对数。正如你所见,WOE 将类别编码在相同的尺度上——即优势比的对数——与逻辑回归的结果相同。
因此,通过使用 WOE,预测因子被准备并编码在同一尺度上,逻辑回归模型中的参数——即系数——可以直接比较。
在这个菜谱中,我们将使用pandas和feature-engine执行 WOE 编码。
如何做到这一点...
让我们先进行一些导入和准备数据:
-
导入所需的库和函数:
import numpy as np import pandas as pd from sklearn.model_selection import train_test_split -
让我们加载 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, ) -
让我们获取目标值的逆,以便能够计算负例:
neg_y_train = pd.Series( np.where(y_train == 1, 0, 1), index=y_train.index ) -
让我们确定目标变量取值为
1或0的观测数:total_pos = y_train.sum() total_neg = neg_y_train.sum() -
现在,让我们计算我们之前在本菜谱中讨论的 WOE 公式的分子和分母:
pos = y_train.groupby( X_train["A1"]).sum() / total_pos neg = neg_y_train.groupby( X_train["A1"]).sum() / total_neg -
现在,让我们计算每个类别的 WOE 值:
woe = np.log(pos/neg)我们可以通过执行
print(woe)来显示具有类别到 WOE 对的序列:A1 Missing 0.203599 a 0.092373 b -0.042410 A1 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)
You can inspect the encoded variable by executing `X_train_enc["A1"].head()`.Now, let’s perform WoE encoding using `feature-engine`. -
让我们导入编码器:
from feature_engine.encoding import WoEEncoder -
接下来,让我们设置编码器以编码三个分类变量:
woe_enc = WoEEncoder(variables = ["A1", "A9", "A12"])
注意
对于稀少类别,可能会发生p(0)=0或p(1)=0的情况,这时除法或对数没有定义。为了避免这种情况,请按照分组稀少或不常见类别食谱中的方法将不常见的类别分组。
-
让我们将转换器拟合到训练集,以便它学习并存储不同类别的 WoE:
woe_enc.fit(X_train, y_train)
注意
我们可以通过执行woe_enc.encoder_dict_来显示具有类别到 WoE 对的字典。
-
最后,让我们将训练集和测试集中的三个分类变量进行编码:
X_train_enc = woe_enc.transform(X_train) X_test_enc = woe_enc.transform(X_test)
feature-engine返回包含编码好的分类变量的pandas DataFrame,这些变量可以用于机器学习模型。
它是如何工作的...
在这个食谱中,我们使用pandas和feature-engine对分类变量进行了 WoE 编码。
我们结合了pandas的sum()和groupby()以及numpy的log(),正如我们在本食谱开头所描述的那样,来确定 WoE。
接下来,我们使用feature-engine自动化了该过程。我们使用了WoEEncoder(),它使用fit()方法学习每个类别的 WoE,然后使用transform()将类别替换为相应的数字。
参见
要查看使用类别编码器的 WoE 的实现,请访问本书的 GitHub 仓库:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch02-categorical-encoding/Recipe-07-Weight-of-evidence.ipynb。
分组稀少或不常见的类别
稀少类别是指只存在于观测中一小部分的类别。没有规则可以确定多小可以被认为是小的,但通常,任何低于 5%的值都可以被认为是稀少的。
不常见的标签通常只出现在训练集或测试集中,这使得算法容易过拟合或无法评分观测。此外,当将类别编码为数字时,我们只为在训练集中观察到的类别创建映射,因此我们不知道如何编码新的标签。为了避免这些复杂性,我们可以将不常见的类别组合成一个称为Rare或Other的单个类别。
在这个食谱中,我们将使用pandas和feature-engine对不常见的类别进行分组。
如何做到这一点...
首先,让我们导入必要的 Python 库并准备好数据集:
-
导入必要的 Python 库、函数和类:
import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from feature_engine.encoding import RareLabelEncoder -
让我们加载 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, ) -
让我们在变量
A7中捕获每个类别的观测比例:freqs = X_train["A7"].value_counts(normalize=True)在执行
print(freqs)后,我们可以看到以下输出中A7每个类别的观测百分比,以小数表示:v 0.573499 h 0.209110 ff 0.084886 bb 0.080745 z 0.014493 dd 0.010352 j 0.010352 Missing 0.008282 n 0.006211 o 0.002070 z, dd, j, Missing, n, and o are rare categories. -
让我们创建一个包含在超过 5%的观测中存在的类别名称的列表:
frequent_cat = [ x for x in freqs.loc[freqs > 0.05].index.values]如果我们执行
print(frequent_cat),我们将看到A7的频繁类别:Rare 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")
-
让我们确定编码变量中观测值的百分比:
X_train["A7"].value_counts(normalize=True)我们可以看到,不频繁的标签现在已经被重新分组到
Rare类别中:v 0.573499 h 0.209110 ff 0.084886 bb 0.080745 Rare 0.051760 feature-engine. -
让我们创建一个稀少标签编码器,将少于 5%的观测值中存在的类别分组,前提是分类变量有超过四个不同的值:
rare_encoder = RareLabelEncoder(tol=0.05, n_categories=4) -
让我们拟合编码器,以便它找到分类变量,然后学习它们的频繁类别:
rare_encoder.fit(X_train)
注意
在拟合过程中,转换器将发出警告,表明许多分类变量少于四个类别,因此它们的值将不会被分组。转换器只是让你知道这种情况正在发生。
我们可以通过执行rare_encoder.encoder_dict_来显示每个变量的频繁类别,以及执行rare_encoder.variables_来显示将被编码的变量。
-
最后,让我们在训练集和测试集中对稀少标签进行分组:
X_train_enc = rare_encoder.transform(X_train) X_test_enc = rare_encoder.transform(X_test)
现在我们已经分组了稀少标签,我们准备对分类变量进行编码,就像我们在本章前面的食谱中所做的那样。
它是如何工作的...
在这个食谱中,我们使用pandas和feature-engine将不频繁的类别分组。
我们使用pandas的value_counts()确定了A7变量每个类别的观测值比例,通过将normalize参数设置为True。使用列表推导,我们捕获了在超过 5%的观测值中存在的变量名称。最后,使用 NumPy 的where(),我们搜索A7的每一行,如果观测值是列表中的频繁类别之一,我们使用pandas的isin()进行检查,则保留其值;否则,将其替换为Rare。
我们使用feature-engine的RareLabelEncoder()自动化了前面多个分类变量的步骤。通过将tol设置为0.05,我们保留了在超过 5%的观测值中存在的类别。通过将n_categories设置为4,我们只对具有超过四个唯一值的变量中的类别进行分组。使用fit(),转换器识别了分类变量,然后学习并存储了它们的频繁类别。使用transform(),转换器将不频繁的类别替换为Rare字符串。
执行二进制编码
整数1可以用1-0的序列表示,整数2用0-1表示,整数3用1-1表示,而整数0用0-0表示。二进制字符串的两个位置的数字成为列,它们是原始变量的编码表示:

图 2.10 – 显示二进制编码颜色变量的步骤的表格
二进制编码在维度上比独热编码少。在我们的例子中,Color变量将通过独热编码编码为k-1类别——即三个变量——但使用二进制编码,我们可以只用两个特征来表示变量。更普遍地,我们确定编码一个变量所需的二进制特征数量为log2(不同类别的数量);在我们的例子中,log2(4) = 2二进制特征。
二进制编码是一种替代独热编码的方法,在这种方法中我们不会丢失变量的信息,但在编码后我们获得的特征更少。这在处理高度基数变量时尤其有用。例如,如果一个变量包含 128 个独特的类别,使用独热编码,我们需要 127 个特征来编码这个变量,而使用二进制编码,我们只需要7(log2(128)=7)。因此,这种编码可以防止特征空间爆炸。此外,二进制编码的特征也适合线性模型。然而,从另一方面来看,派生的二进制特征缺乏人类可解释性,所以如果我们需要解释模型所做的决策,这种编码方法可能不是一个合适的选择。
在这个菜谱中,我们将学习如何使用类别编码器执行二进制编码。
如何做到这一点...
首先,让我们导入必要的 Python 库并准备好数据集:
-
导入所需的 Python 库、函数和类:
import pandas as pd from sklearn.model_selection import train_test_split from category_encoders.binary import BinaryEncoder -
让我们加载 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, ) -
让我们检查
A7中的唯一类别:X_train["A7"].unique()在下面的输出中,我们可以看到
A7有 10 个不同的类别:A7:encoder = BinaryEncoder(cols=["A7"],
drop_invariant=True)
注意
BinaryEncoder()以及类别编码器包中的其他编码器允许我们选择要编码的变量。我们只需将列名列表传递给cols参数。
-
让我们将转换器拟合到训练集,以便它计算所需的二进制变量数量并创建变量到二进制代码表示:
encoder.fit(X_train) -
最后,让我们在训练集和测试集中对
A7进行编码:X_train_enc = encoder.transform(X_train) X_test_enc = encoder.transform(X_test)我们可以通过执行
print(X_train_enc.head())来显示转换后训练集的前几行,它返回以下输出:

图 2.11 – 二进制编码后的变量 DataFrame
二进制编码为A7返回了四个二进制变量,分别是A7_0、A7_1、A7_2和A7_3,而不是独热编码会返回的九个。
它是如何工作的...
在这个食谱中,我们使用了 Category Encoders 包进行二进制编码。我们用BinaryEncoder()来编码A7变量。通过fit()方法,BinaryEncoder()创建了一个从类别到一组二进制列的映射,而通过transform()方法,编码器在训练集和测试集中对A7变量进行了编码。
第三章:转换数值变量
在数据分析中使用的数据统计方法对数据做出某些假设。例如,在一般线性模型中,假设因变量(目标)的值是独立的,目标变量与自变量(预测变量)之间存在线性关系,以及残差——即预测值与目标真实值之间的差异——是正态分布且中心在0。当这些假设不成立时,产生的概率陈述可能不准确。为了纠正假设失败并提高模型性能,我们可以在分析之前对变量进行转换。
当我们转换一个变量时,我们用该变量的函数替换其原始值。使用数学函数转换变量有助于减少变量的偏度,提高值的分布范围,有时可以揭示预测变量与目标之间的线性关系和加性关系。常用的数学转换包括对数、倒数、幂、平方和立方根转换,以及 Box-Cox 和 Yeo-Johnson 转换。这一系列转换通常被称为方差稳定转换。方差稳定转换旨在将变量的分布带到更对称——即高斯——的形状。在本章中,我们将讨论何时使用每种转换,然后使用 NumPy、scikit-learn 和 Feature-engine 实现它们。
本章包含以下食谱:
-
使用对数函数转换变量
-
使用倒数函数转换变量
-
使用平方根转换变量
-
使用幂转换
-
执行 Box-Cox 转换
-
执行 Yeo-Johnson 转换
使用对数函数转换变量
对数函数是对处理具有右偏分布(观测值在变量的较低值处累积)的正面数据的一种强大转换。一个常见的例子是收入变量,其值在较低工资处有大量累积。对数转换对变量分布的形状有强烈的影响。
在本食谱中,我们将使用 NumPy、scikit-learn 和 Feature-engine 执行对数转换。我们还将创建一个诊断图函数来评估转换对变量分布的影响。
准备工作
为了评估变量分布并了解变换是否改善了值分布并稳定了方差,我们可以通过直方图和分位数-分位数(Q-Q)图来直观地检查数据。Q-Q 图帮助我们确定两个变量是否显示出相似的分布。在 Q-Q 图中,我们绘制一个变量的分位数与另一个变量的分位数。如果我们绘制感兴趣变量的分位数与正态分布的预期分位数,那么我们可以确定我们的变量是否也是正态分布的。如果变量是正态分布的,Q-Q 图中的点将沿着 45 度对角线分布。
注意
分位数是分布中低于某个数据点分数的值。因此,第 20 分位数是分布中 20%的观测值低于且 80%高于该值的点。
如何做到这一点...
让我们先导入库并准备数据集:
-
导入所需的 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 -
让我们将加利福尼亚住房数据集加载到 pandas DataFrame 中:
X, y = fetch_california_housing(return_X_y=True, as_frame=True) -
让我们通过使用 pandas 绘制直方图来探索数据集中所有变量的分布:
X.hist(bins=30, figsize=(12, 12)) plt.show()在以下输出中,我们可以看到
MedInc变量显示出轻微的右偏分布,例如AveRooms和Population这样的变量具有严重的右偏分布,而HouseAge变量在其范围内显示出值的均匀分布:
图 3

图 3.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() -
让我们使用第 4 步中的函数绘制
MedInc变量的分布图:diagnostic_plots(X, "MedInc")以下输出显示
MedInc具有右偏分布:

图 3.2 – MedInc变量的直方图和 Q-Q 图
现在,让我们使用对数变换数据:
-
首先,让我们复制原始 DataFrame:
X_tf = X.copy()我们创建了一个副本,这样我们就可以修改副本中的值,而不是原始 DataFrame 中的值,这对于本食谱的其余部分是必要的。
注意
如果我们执行X_tf = X而不是使用 pandas 的copy()函数,X_tf将不是 DataFrame 的副本;相反,它将是相同数据的另一个视图。因此,对X_tf所做的更改也将反映在X中。
-
让我们创建一个包含我们想要变换的变量的列表:
vars = ["MedInc", "AveRooms", "AveBedrms", "Population"] -
让我们使用 NumPy 对第 7 步中的变量进行对数变换,并将变换后的变量捕获到新的 DataFrame 中:
X_tf[vars] = np.log(X[vars])
注意
记住,对数变换只能应用于严格正数变量。如果变量有零或负值,有时添加一个常数使这些值变为正数是有用的。我们可以使用X_tf[vars] = np.log(X[vars] + 1)添加一个常数值1。
-
让我们使用步骤 4 中的诊断函数检查变换后的
MedInc分布:diagnostic_plots(X_tf, "MedInc")在以下输出中,我们可以看到对数变换返回了一个分布更均匀的变量,在 Q-Q 图中更好地逼近理论正态分布:

图 3.3 – 对数变换后 MedInc 变量的直方图和 Q-Q 图
继续绘制其他变换后的变量,以便熟悉对数变换对分布的影响。
现在,让我们使用scikit-learn应用对数变换。
-
让我们导入
FunctionTransformer():from sklearn.preprocessing import FunctionTransformer在我们继续之前,我们需要复制原始数据集,就像我们在步骤 6 中做的那样。
-
我们将设置变换器以应用对数变换,并能够将变换后的变量恢复到其原始表示:
transformer = FunctionTransformer(np.log, inverse_func=np.exp)
注意
如果我们使用默认参数validate=False设置FunctionTransformer(),在变换数据之前不需要拟合变换器。如果我们把validate设置为True,变换器将检查fit方法的数据输入。后者在用 DataFrame 拟合变换器时很有用,以便它学习并存储变量名。
-
让我们将步骤 7 中的正变量进行变换:
X_tf[vars] = transformer.transform(X[vars])
注意
Scikit-learn 变换器返回 NumPy 数组,并默认变换整个 DataFrame。在这种情况下,我们将数组的输出结果直接赋值给我们的现有 DataFrame。我们可以通过set_output方法更改返回的格式,并且我们可以通过ColumnTransformer()限制要变换的变量。
使用步骤 4 中的诊断函数检查变换的结果。
-
现在,让我们将变换恢复到原始变量表示:
X_tf[vars] = transformer.inverse_transform(X_tf[vars])如果你通过执行
diagnostic_plots(X_tf, "MedInc")检查分布,你应该看到一个与步骤 5 返回的相同的图。
注意
要给变量添加一个常数,以防它们不是严格正数,可以使用transformer = FunctionTransformer(lambda x: np.log(x + 1))。
现在,让我们使用 Feature-engine 应用对数变换。
-
让我们导入
LogTransformer():from feature_engine.transformation import LogTransformer -
我们将设置变换器以变换步骤 7 中的变量,然后拟合变换器到数据集:
lt = LogTransformer(variables = vars) lt.fit(X)
注意
如果variables参数留为None,LogTransformer()将对fit()期间找到的所有数值变量应用对数变换。或者,我们可以指定要修改的变量,就像我们在步骤 15 中做的那样。
-
最后,让我们进行数据变换:
X_tf = lt.transform(X)X_tf是XDataFrame 的一个副本,其中步骤 7中的变量通过对数进行了变换。 -
我们也可以将变换后的变量恢复到其原始表示:
X_tf = lt.inverse_transform(X_tf)如果你检查步骤 17之后的变量分布,它们应该与原始数据相同。
注意
Feature-engine 有一个专门的转换器,在应用对数变换之前向变量添加常数值。请在此菜谱的更多内容…部分查看更多详细信息。
它是如何工作的...
在这个菜谱中,我们使用 NumPy、scikit-learn 和 Feature-engine 对正变量子集应用了对数变换。
为了比较变换对变量分布的影响,我们创建了一个诊断函数,用于在直方图旁边绘制 Q-Q 图。为了创建 Q-Q 图,我们使用了scipy.stats.probplot(),它在y轴上绘制了感兴趣变量的分位数,与在x轴上设置dist参数为norm的理论正态分布的分位数进行比较。我们使用matplotlib通过设置plot参数为plt来显示图表。
使用plt.figure()和figsize,我们调整了图表的大小,并使用plt.subplot()将两个图表组织在一行两列中——也就是说,一个图表紧挨着另一个图表。plt.subplot()中的数字分别表示行数、列数和图表在图中的位置。我们将直方图放在位置 1,将 Q-Q 图放在位置 2——也就是说,左和右,分别。
为了测试函数,我们在变换之前为MedInc变量绘制了直方图和 Q-Q 图,观察到MedInc并不是正态分布。大多数观测值位于直方图的左侧,并且在分布的两端,Q-Q 图中的值偏离了 45 度线。
接下来,使用np.log(),我们对 DataFrame 中四个正变量的一个子集应用了对数变换。为了评估变换的效果,我们绘制了变换后的MedInc变量的直方图和 Q-Q 图。我们观察到,在对数变换之后,值在直方图中更加集中,并且在 Q-Q 图中,它们只在分布的两端偏离了 45 度线。
接下来,我们使用了 scikit-learn 的FunctionTransformer(),它将任何用户定义的函数应用于数据集。我们将np.log()作为参数传递以应用对数变换,并将 NumPy 的exp()用于逆变换到FunctionTransformer()。使用transform()方法,我们通过对数变换对 DataFrame 中正变量的一个子集进行了变换。使用inverse_transform(),我们将变量值恢复到其原始表示。
最后,我们使用了 Feature-engine 的 LogTransformer() 并使用 variables 参数指定要转换的变量列表。使用 fit(),转换器检查变量是否为数值且为正,使用 transform(),在底层应用 np.log() 来转换选定的变量。使用 inverse_transform(),我们将转换后的变量恢复到其原始表示。
还有更多...
Feature-engine 有一个专门的转换器用于在应用对数之前向不是严格正的变量添加常数:LogCpTransformer()。LogCpTransformer() 可以:
-
将相同的常数添加到所有变量中
-
自动识别并添加使变量为正所需的最小值
-
将用户定义的不同值添加到不同的变量中。
您可以在本书的 GitHub 仓库中找到 LogCpTransformer() 的代码实现:github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch03-variable-transformation/Recipe-1-logarithmic-transformation.ipynb
使用逆函数变换变量
逆函数定义为 1/x。当我们有比率时,它通常很有用,即两个变量的除法结果。这类例子包括人口密度,即每单位面积的人数,以及我们将在本食谱中看到的房屋占用率,即每户的人数。
逆变换对于 0 值未定义,尽管对于负值是定义的,但它主要用于转换正变量。
在本食谱中,我们将使用 NumPy、scikit-learn 和 Feature-engine 实现逆变换,并使用直方图和 Q-Q 图比较其对变量分布的影响。
如何操作...
让我们先导入库并准备数据集:
-
导入所需的 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 -
让我们加载加利福尼亚住房数据集:
X, y = fetch_california_housing(return_X_y=True, as_frame=True) -
为了评估变量分布,我们将创建一个函数,该函数接受 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() -
现在,让我们绘制
AveOccup变量的分布图,该变量指定了房屋的平均占用率:diagnostic_plots(X, "AveOccup")AveOccup变量显示出非常强的右偏分布,如下面的输出所示:

图 3.4 – AveOccup 变量的直方图和 Q-Q 图
注意
AveOccup 变量指的是平均家庭人数——即某区域内人数与房屋数量的比率。这是一个适合倒数转换的有前途的变量。您可以通过执行 data = fetch_california_housing() 然后跟 print(data.DESCR) 来找到更多关于变量和数据集的详细信息。
现在,让我们使用 NumPy 应用倒数转换。
-
首先,让我们复制原始 DataFrame,以便我们可以修改副本中的值,而不是原始 DataFrame 中的值,这对于本食谱的其余部分是必需的:
X_tf = X.copy()
注意
记住,执行 X_tf = X 而不是使用 pandas 的 copy() 会创建相同数据的额外视图。因此,在 X_tf 中所做的更改也会反映在 X 中。
-
让我们将倒数转换应用于
AveOccup变量:X_tf["AveOccup"] = np.reciprocal(X_tf["AveOccup"]) -
让我们使用我们在 第 3 步 中创建的诊断函数检查转换后
AveOccup变量的分布:diagnostic_plots(X_tf, "AveOccup")
注意
转换后,AveOccup 现在是某个区域内房屋数量与人口数量的比率——换句话说,每公民房屋数。
在这里,我们可以看到倒数转换后 AveOccup 变量分布的显著变化:

图 3.5 – 倒数转换后 AveOccup 变量的直方图和 Q-Q 图
现在,让我们使用 scikit-learn 应用倒数转换。
-
让我们导入
FunctionTransformer():from sklearn.preprocessing import FunctionTransformer -
让我们通过传递
np.reciprocal作为参数来设置转换器:transformer = FunctionTransformer(np.reciprocal)
注意
默认情况下,FunctionTransformer() 在转换数据之前不需要拟合。
-
现在,让我们复制原始数据集并转换变量:
X_tf = X.copy() X_tf["AveOccup"] = transformer.transform( X["AveOccup"])您可以使用 第 3 步 中的函数来检查转换的效果。
注意
倒数函数的逆变换也是倒数函数。因此,如果您重新对转换后的数据应用 transform(),您将将其还原为其原始表示。更好的做法是将 FunctionTransformer() 的 inverse_transform 参数设置为 np.reciprocal。
现在,让我们使用 feature-engine 应用倒数转换。
-
让我们导入
ReciprocalTransformer():from feature_engine.transformation import ReciprocalTransformer -
让我们设置转换器以修改
AveOccup变量,然后将其拟合到数据集中:rt = ReciprocalTransformer(variables=»AveOccup») rt.fit(X)
注意
如果将 variables 参数设置为 None,则转换器将倒数函数应用于数据集中的 所有数值变量。如果某些变量包含 0 值,转换器将引发错误。
-
让我们将数据集中的选定变量进行转换:
X_tf = rt.transform(X)ReciprocalTransformer()将返回一个新的包含原始变量的 pandas DataFrame,其中在第 12 步 指示的变量通过倒数函数进行了转换。
它是如何工作的...
在本配方中,我们使用 NumPy、scikit-learn 和 Feature-engine 应用了倒数转换。
为了评估变量分布,我们使用了函数来绘制直方图和 Q-Q 图,这些图在我们本章前面关于“使用对数函数转换变量”的How it works…部分中进行了描述。
我们绘制了AveOccup变量的直方图和 Q-Q 图,这显示了严重的右偏分布;其大部分值位于直方图的左侧,并且在 Q-Q 图中偏离了向右端分布的 45 度线。
为了执行互变转换,我们对变量应用了np.reciprocal()。转换后,AveOccup的值在值范围内分布得更均匀,并且在 Q-Q 图中更紧密地遵循正态分布的理论分位数。
接下来,我们使用 scikit-learn 的FunctionTransformer()与np.reciprocal()。transform()方法将np.reciprocal()应用于数据集。
注意
要将FunctionTransformer()的效果限制为一组变量,请使用ColumnTransformer()。要将输出更改为 pandas DataFrame,请将转换输出设置为 pandas。
最后,我们使用 Feature-engine 的ReciprocalTransformer()专门修改了一个变量。通过fit(),转换器检查变量是否为数值型。通过transform(),转换器在幕后应用np.reciprocal()以转换变量。
Feature-engine 的ReciprocalTransformer()通过inverse_transform()方法提供将变量恢复到其原始表示的功能。
注意
使用 scikit-learn 或 Feature-engine 的转换器,而不是 NumPy 的reciprocal()函数,允许我们在 scikit-learn 的Pipeline对象中作为特征工程管道的额外步骤应用倒数函数。
FunctionTransformer()与ReciprocalTransformer()之间的区别在于,前者可以应用任何用户指定的转换,而后者仅应用倒数函数。scikit-learn 默认返回 NumPy 数组并转换数据集中的所有变量。另一方面,Feature-engine 的转换器返回 pandas DataFrame,并且可以在不使用额外类的情况下修改数据中的变量子集。
使用平方根转换变量
平方根转换,√x,以及其变体,Anscombe 转换,√(x+3/8),和 Freeman-Tukey 转换,√x + √(x+1),是方差稳定转换,可以将具有泊松分布的变量转换为具有近似标准高斯分布的变量。平方根转换是一种幂转换形式,其指数为1/2,并且仅对正值有定义。
泊松分布是一种概率分布,表示事件可能发生的次数。换句话说,它是一种计数分布。它是右偏斜的,其方差等于其均值。可能遵循泊松分布的变量示例包括客户的金融项目数量,例如当前账户或信用卡的数量,车辆上的乘客数量,以及家庭中的居住者数量。
在这个菜谱中,我们将使用 NumPy、scikit-learn 和 Feature-engine 实现平方根变换。
如何操作...
我们首先创建一个包含两个变量的数据集,这两个变量的值是从泊松分布中抽取的:
-
让我们先导入必要的库:
import numpy as np import pandas as pd import scipy.stats as stats -
让我们创建一个包含两个变量的 DataFrame,这两个变量分别从均值为
2和3的泊松分布中抽取,并且有10000个观测值:df = pd.DataFrame() df["counts1"] = stats.poisson.rvs(mu=3, size=10000) df["counts2"] = stats.poisson.rvs(mu=2, size=10000) -
让我们创建一个函数,该函数接受 DataFrame 和变量名作为输入,并在 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() -
让我们使用第 3 步中的函数创建一个条形图和 Q-Q 图,用于数据中的一个变量:
diagnostic_plots(df, "counts1")在这里,我们可以看到输出中的泊松分布:

图 3.6 – counts1 变量的条形图和 Q-Q 图
-
现在,让我们复制数据集:
df_tf = df.copy() -
让我们将平方根变换应用于两个变量:
df_tf[["counts1", "counts2"]] = np.sqrt( df[["counts1","counts2"]]) -
让我们将值四舍五入到两位小数,以便更好地可视化:
df_tf[["counts1", "counts2"]] = np.round( df_tf[["counts1", "counts2"]], 2) -
让我们绘制
counts1变量变换后的分布图:diagnostic_plots(df_tf, "counts1")我们看到方差更加 稳定,因为 Q-Q 图中的点更接近 45 度对角线:

图 3.7 – 平方根变换后 counts1 变量的条形图和 Q-Q 图
现在,让我们使用 scikit-learn 应用平方根变换:
-
让我们导入
FunctionTransformer()并将其设置为执行平方根变换:from sklearn.preprocessing import FunctionTransformer transformer = FunctionTransformer( np.sqrt).set_output(transform="pandas")
注意
如果我们想要像在第 7 步中那样四舍五入值,我们可以使用 transformer = FunctionTransformer(func=lambda x: np.round(np.sqrt(x), 2)) 设置变换器。
-
让我们复制数据并变换变量:
df_tf = df.copy() df_tf = transformer.transform(df)按照我们在 第 8 步 中所做的那样,继续检查变换的结果。
要使用 Feature-engine 应用平方根变换,我们使用指数为 0.5 的
PowerTransformer():from feature_engine.transformation import PowerTransformer root_t = PowerTransformer(exp=1/2) -
接下来,我们将变换器拟合到数据上:
root_t.fit(df)
注意
变换器自动识别数值变量,我们可以通过执行 root_t.variables_ 来探索。
-
最后,让我们对数据进行变换:
df_tf = root_t.transform(df)PowerTransformer()返回一个包含变换变量的 pandas DataFrame。
它是如何工作的…
在这个菜谱中,我们使用了 NumPy、scikit-learn 和 Feature-engine 实现了平方根变换。
我们使用 NumPy 的sqrt()函数直接或在其内部使用 scikit-learn 的FunctionTransformer()来确定变量的平方根。或者,我们使用 Feature-engine 的PowerTransformer(),将指数设置为平方根函数的 0.5。NumPy 直接修改了变量。scikit-learn 和 Feature-engine 的转换器在调用transform()方法时修改了变量。
使用幂变换
幂函数是遵循
格式的数学变换,其中 lambda 可以取任何值。平方根和立方根变换是幂变换的特殊情况,其中 lambda 分别为 1/2 和 1/3。挑战在于找到 lambda 参数的值。Box-Cox 变换,作为幂变换的推广,通过最大似然估计找到最优的 lambda 值。我们将在下一个菜谱中讨论 Box-Cox 变换。在实践中,我们将尝试不同的 lambda 值,并通过视觉检查变量分布来确定哪一个提供了最佳的变换。一般来说,如果数据是右偏斜的——也就是说,如果观测值累积在较低值附近——我们使用小于 1 的 lambda 值,而如果数据是左偏斜的——也就是说,在较高值附近的观测值更多——那么我们使用大于 1 的 lambda 值。
在这个菜谱中,我们将使用 NumPy、scikit-learn 和 Feature-engine 执行幂变换。
如何做到这一点...
让我们先导入库并准备好数据集:
-
导入所需的 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 -
让我们将加利福尼亚住房数据集加载到一个 pandas DataFrame 中:
X, y = fetch_california_housing( return_X_y=True, as_frame=True) -
为了评估变量分布,我们将创建一个函数,该函数接受一个 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() -
让我们使用之前的功能绘制
Population变量的分布图:diagnostic_plots(X, "Population")在前一个命令返回的图表中,我们可以看到
Population严重向右偏斜:

图 3.8 – Population变量的直方图和 Q-Q 图
现在,让我们对MedInc和Population变量应用幂变换。由于这两个变量都向右偏斜,一个小于1的指数可能会使变量值分布得更好。
-
让我们将要变换的变量捕获到一个列表中:
variables = ["MedInc", "Population"] -
让我们复制 DataFrame,然后对步骤 5中的变量应用幂变换,其中指数为
0.3:X_tf = X.copy() X_tf[variables] = np.power(X[variables], 0.3)
注意
使用np.power(),我们可以通过改变函数第二个位置中的指数值来应用任何幂变换。
-
让我们检查
Population分布的变化:diagnostic_plots(X_tf, "Population")如前一个命令返回的图所示,
Population现在在值范围内分布得更均匀,并且更接近正态分布的分位数:

Figure 3.9 – 变换后的 Population 变量的直方图和 Q-Q 图
现在,让我们使用 scikit-learn 应用幂变换。
-
让我们设置
FunctionTransformer(),使用指数为0.3的幂变换:transformer = FunctionTransformer( lambda x: np.power(x,0.3)) -
让我们复制 DataFrame 并将 步骤 5 中的变量进行转换:
X_tf = X.copy() X_tf[variables] = transformer.transform(X[variables])就这样 – 我们现在可以检查变量分布了。最后,让我们使用 Feature-engine 执行指数变换。
-
让我们设置
PowerTransformer(),指数为0.3,以转换 步骤 5 中的变量。然后,我们将它拟合到数据中:power_t = PowerTransformer(variables=variables, exp=0.3) power_t.fit(X)
注意
如果我们不定义要转换的变量,PowerTransformer() 将选择并转换 DataFrame 中的所有数值变量。
-
最后,让我们转换这两个变量:
X_tf = power_t.transform(X)
转换器返回一个包含原始变量的 DataFrame,其中 步骤 5 中指定的两个变量使用幂函数进行了转换。
它是如何工作的...
在这个配方中,我们使用了 NumPy、scikit-learn 和 Feature-engine 应用了幂变换。
要使用 NumPy 应用幂函数,我们应用了 power() 方法到包含要转换的变量的数据集切片。要使用 scikit-learn 应用此转换,我们在 lambda 函数中设置了 FunctionTransformer(),使用 np.power(),并将 0.3 作为指数。要使用 Feature-engine 应用幂函数,我们设置了 PowerTransformer(),其中包含要转换的变量列表和指数 0.3。
当我们调用 transform() 方法时,scikit-learn 和 Feature-engine 转换器应用了变换。scikit-learn 的 FunctionTransformer() 修改整个数据集,并默认返回 NumPy 数组。要返回 pandas DataFrame,我们需要将转换输出设置为 pandas,并且要应用转换到特定变量,我们可以使用 ColumnTransformer()。另一方面,Feature-engine 的 PowerTransformer() 可以直接应用于变量子集,并默认返回 pandas DataFrame。
执行 Box-Cox 变换
Box-Cox 变换是幂变换家族的推广,定义为如下:


在这里,y 是变量,λ 是转换参数。它包括变换的重要特殊情况,例如未转换的 (λ = 1),对数 (λ = 0),倒数 (λ = - 1),平方根(当 λ = 0.5 时,它应用了一个缩放和移动的平方根函数),以及立方根。
Box-Cox 转换使用最大似然估计评估 λ 的几个值,并选择返回最佳转换的 λ 参数。
在这个菜谱中,我们将使用 scikit-learn 和 Feature-engine 执行 Box-Cox 转换。
注意
Box-Cox 转换只能用于正变量。如果你的变量有负值,尝试使用下一道菜谱中描述的 Yeo-Johnson 转换,即执行 Yeo-Johnson 转换。或者,你可以在转换前添加一个常数来改变变量的分布。
如何做到这一点...
让我们先导入必要的库并准备好数据集:
-
导入所需的 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 -
让我们将加利福尼亚住房数据集加载到 pandas DataFrame 中:
X, y = fetch_california_housing( return_X_y=True, as_frame=True) -
让我们删除
Latitude和Longitude变量:X.drop(labels=["Latitude", "Longitude"], axis=1, inplace=True) -
让我们使用直方图检查变量分布:
X.hist(bins=30, figsize=(12, 12), layout=(3, 3)) plt.show()在以下输出中,我们可以看到
MedInc变量显示出轻微的右偏分布,例如AveRooms和Population这样的变量具有严重的右偏分布,而HouseAge变量在其范围内显示出值的均匀分布:

图 3.10 – 数值变量的直方图
-
让我们在下一步中使用这些变量之前,将变量名捕获到一个列表中:
variables = list(X.columns) -
让我们创建一个函数,该函数将为数据中的所有变量绘制 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() -
现在,让我们使用前面的函数显示 Q-Q 图:
make_qqplot(X)通过查看以下图表,我们可以证实变量不是正态分布的:

图 3.11 – 数值变量的 Q-Q 图
接下来,让我们使用 scikit-learn 执行 Box-Cox 转换。
-
让我们设置
PowerTransformer()以应用 Box-Cox 转换并将其拟合到数据中,以便找到最优的 λ 参数:transformer = PowerTransformer( method="box-cox", standardize=False, ).set_output(transform="pandas") transformer.fit(X)
注意
为了避免数据泄露,λ 参数应该从训练集中学习,然后用于转换训练集和测试集。因此,在拟合 PowerTransformer() 之前,请记住将你的数据分成训练集和测试集。
-
现在,让我们转换数据集:
X_tf = transformer.transform(X)
注意
scikit-learn 的 PowerTransformer() 将学习到的 lambda 值存储在其 lambdas_ 属性中,你可以通过执行 transformer.lambdas_ 来显示它。
-
让我们通过直方图检查变换后数据的分布:
X_tf.hist(bins=30, figsize=(12, 12), layout=(3, 3)) plt.show()在以下输出中,我们可以看到变量的值在其范围内分布得更均匀:

图 3.12 – 变换后变量的直方图
-
现在,让我们返回变换后变量的 Q-Q 图:
make_qqplot(X_tf)在以下输出中,我们可以看到,经过变换后,变量更接近理论上的正态分布:

图 3.13 – 变换后变量的 Q-Q 图
现在,让我们使用 Feature-engine 实现 Box-Cox 变换。
-
让我们设置
BoxCoxTransformer()以转换数据集中的所有变量,并将其拟合到数据:bct = BoxCoxTransformer() bct.fit(X) -
现在,让我们继续变换变量:
X_tf = bct.transform(X)变换返回一个包含修改后变量的 pandas DataFrame。
注意
scikit-learn 的 PowerTransformer() 将转换整个数据集。另一方面,Feature-engine 的 BoxCoxTransformer() 可以修改变量子集,如果我们设置转换器时将它们的名称列表传递给 variables 参数。如果将 variables 参数设置为 None,转换器将在 fit() 期间转换所有遇到的数值变量。
-
Box-Cox 变换的最佳 lambda 值存储在
lambda_dict_属性中。让我们检查一下:bct.lambda_dict_上一条命令的输出如下:
{'MedInc': 0.09085449361507383, 'HouseAge': 0.8093980940712507, 'AveRooms': -0.2980048976549959, 'AveBedrms': -1.6290002625859639, 'Population': 0.23576757812051324, 'AveOccup': -0.4763032278973292}
现在你已经知道如何使用两个不同的 Python 库实现 Box-Cox 变换。
它是如何工作的...
scikit-learn 的 PowerTransformer() 可以应用 Box-Cox 和 Yeo-Johnson 变换,因此我们在设置转换器时通过传递 box-cox 字符串指定了转换。接下来,我们将转换器拟合到数据,以便转换器学习每个变量的最佳 lambda 值。学习到的 lambda 值存储在 lambdas_ 属性中。最后,我们使用 transform() 方法转换变量。
注意
记住,要返回 DataFrames 而不是数组,你需要通过 set_output() 方法指定转换输出。你可以通过使用 ColumnTransformer() 将转换应用于值子集。
最后,我们使用 Feature-engine 应用了 Box-Cox 转换。我们初始化 BoxCoxTransformer(),将参数 variables 设置为 None。因此,转换器在 fit() 期间自动找到了数据中的数值变量。我们将转换器拟合到数据中,使其学习每个变量的最佳 λ 值,这些值存储在 lambda_dict_ 中,并使用 transform() 方法转换变量。Feature-engine 的 BoxCoxTransformer() 可以接受整个 DataFrame 作为输入,并且只修改选定的变量。
还有更多...
我们可以使用 SciPy 库应用 Box-Cox 转换。有关代码实现,请访问本书的 GitHub 仓库:github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch03-variable-transformation/Recipe-5-Box-Cox-transformation.ipynb
执行 Yeo-Johnson 转换
Yeo-Johnson 转换是 Box-Cox 转换的扩展,不再受正值约束。换句话说,Yeo-Johnson 转换可以用于具有零和负值的变量,以及正值变量。这些转换如下定义:
-
; 如果 λ ≠ 0 且 X >= 0 -
ln(X + 1 ); 如果 λ = 0 且 X >= 0
-
; 如果 λ ≠ 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 转换。
如何实现...
让我们先导入必要的库并准备数据集:
-
导入所需的 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 YeoJohnsonTransformer -
让我们将加利福尼亚住房数据集加载到 pandas DataFrame 中,然后删除
Latitude和Longitude变量:X, y = fetch_california_housing( return_X_y=True, as_frame=True) X.drop(labels=[«Latitude», «Longitude»], axis=1, inplace=True)
注意
我们可以使用直方图和 Q-Q 图评估变量分布,就像我们在步骤 4到7的执行 Box-Cox 变换菜谱中所做的那样。
现在,让我们使用 scikit-learn 应用 Yeo-Johnson 变换。
-
让我们设置
PowerTransformer()使用yeo-johnson变换:transformer = PowerTransformer( method="yeo-johnson", standardize=False, ).set_output(transform="pandas") -
让我们将转换器拟合到数据中:
transformer.fit(X)
注意
λ参数应该从训练集中学习,然后用于变换训练集和测试集。因此,在拟合PowerTransformer()之前,请记住将你的数据分为训练集和测试集。
-
现在,让我们转换数据集:
X_tf = transformer.transform(X)
注意
PowerTransformer()将其学习到的参数存储在其lambda_属性中,你可以通过执行transformer.lambdas_来返回。
-
让我们使用直方图检查变换数据的分布:
X_tf.hist(bins=30, figsize=(12, 12), layout=(3, 3)) plt.show()在以下输出中,我们可以看到变量的值在其范围内分布得更均匀:

图 3.14 – yeo-Johnson 变换后变量的直方图
最后,让我们使用 Feature-engine 实现 Yeo-Johnson 变换。
-
让我们设置
YeoJohnsonTransformer()以转换所有数值变量,然后将其拟合到数据中:yjt = YeoJohnsonTransformer() yjt.fit(X)
注意
如果将variables参数设置为None,转换器将选择并转换数据集中所有的数值变量。或者,我们可以传递一个包含要修改的变量名称的列表。
与 scikit-learn 的PowerTransformer()相比,Feature-engine 的转换器可以将整个 DataFrame 作为fit()和transform()方法的参数,同时它只会修改选定的变量。
-
让我们转换变量:
X_tf = yjt.transform(X) -
YeoJohnsonTransformer()将其每个变量的最佳参数存储在其lambda_dict_属性中,我们可以如下显示:yjt.lambda_dict_之前的命令返回以下字典:
{'MedInc': -0.1985098937827175, 'HouseAge': 0.8081480895997063, 'AveRooms': -0.5536698033957893, 'AveBedrms': -4.3940822236920365, 'Population': 0.23352363517075606, 'AveOccup': -0.9013456270549428}
现在你已经知道了如何使用两个不同的开源库实现 Yeo-Johnson 变换。
它是如何工作的...
在这个菜谱中,我们使用了scikit-learn和Feature-engine应用了 Yeo-Johnson 变换。
scikit-learn的PowerTransformer()可以应用 Box-Cox 和 Yeo-Johnson 变换,因此我们使用yeo-johnson字符串指定了变换。standardize参数允许我们确定是否想要标准化(缩放)变换后的值。接下来,我们将转换器拟合到 DataFrame,以便它为每个变量学习最优的λ值。PowerTransformer()将学习到的λ值存储在其lambdas_属性中。最后,我们使用transform()方法返回变换后的变量。我们将变换输出设置为pandas,以便在变换后返回 DataFrame。
之后,我们使用 Feature-engine 应用了 Yeo-Johnson 转换。我们设置了YeoJohnsonTransformer(),以便在fit()过程中转换所有数值变量。我们将转换器拟合到数据上,以便它学习每个变量的最优 lambda 值,这些值存储在lambda_dict_中,并最终使用transform()方法转换变量。Feature-engine 的YeoJohnnsonTransformer()可以接受整个 DataFrame 作为输入,但它只会转换选定的变量。
还有更多……
我们可以使用 SciPy 库应用 Yeo-Johnson 转换。有关代码实现,请访问本书的 GitHub 仓库:github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Second-Edition/blob/main/ch03-variable-transformation/Recipe-6-Yeo-Johnson-transformation.ipynb
第四章:执行变量离散化
离散化是将连续变量通过创建一系列连续区间(也称为bins,跨越变量值的范围)转换为离散特征的过程。随后,这些区间被视为分类数据。
许多机器学习模型,如决策树和朴素贝叶斯,与离散属性配合工作效果更好。事实上,基于决策树的模型是根据属性上的离散分区做出决策的。在归纳过程中,决策树评估所有可能的特征值以找到最佳分割点。因此,特征值越多,树的归纳时间就越长。从这个意义上说,离散化可以减少模型训练所需的时间。
离散化还有额外的优势。数据被减少和简化;离散特征可以更容易被领域专家理解。离散化可以改变偏斜变量的分布;在按等频对区间进行排序时,值在范围内分布得更均匀。此外,离散化可以通过将它们放置在较低或较高的区间中,与分布的剩余内点值一起,最小化异常值的影响。总的来说,离散化减少了数据并简化了数据,使学习过程更快,并可能产生更准确的结果。
离散化也可能导致信息丢失,例如,通过将强烈关联不同类别或目标值的值组合到同一个区间中。因此,离散化算法的目标是在不造成重大信息丢失的情况下找到最小数量的区间。在实践中,许多离散化过程需要用户输入将值排序到的区间数量。然后,算法的任务是找到这些区间的分割点。在这些过程中,我们发现最广泛使用的等宽和等频离散化方法。基于决策树的离散化方法则能够找到最佳分区数量以及分割点。
离散化过程可以分为监督和非监督。非监督离散化方法仅使用变量的分布来确定连续区间的界限。另一方面,监督方法使用目标信息来创建区间。
在本章中,我们将讨论在成熟的开源库中广泛使用的监督和非监督离散化过程。在这些过程中,我们将涵盖等宽、等频、任意、k-均值和基于决策树的离散化。更详细的方法,如 ChiMerge 和 CAIM,超出了本章的范围,因为它们的实现尚未开源。
本章包含以下食谱:
-
执行等宽离散化
-
实现等频率离散化
-
将变量离散化到任意区间
-
使用 k-means 聚类进行离散化
-
实现特征二值化
-
使用决策树进行离散化
技术要求
在本章中,我们将使用数值计算库pandas、numpy、matplotlib、scikit-learn和feature-engine。我们还将使用yellowbrick Python 开源库,您可以使用pip进行安装:
pip install yellowbrick
想了解更多关于yellowbrick的信息,请访问以下文档:
https://www.scikit-yb.org/en/latest/index.html
进行等宽离散化
等宽离散化包括将变量的观测值范围划分为用户提供的k个等大小的区间。X变量的区间宽度如下所示:

然后,如果变量的值在 0 到 100 之间变化,我们可以创建五个箱,如下所示:width = (100-0) / 5 = 20。箱将分别是 0–20、20–40、40–60 和 80–100。第一个和最后一个箱(0–20 和 80–100)可以通过扩展限制到负无穷和正无穷来扩展,以容纳小于 0 或大于 100 的值。
在本配方中,我们将使用pandas、scikit-learn和feature-engine进行等宽离散化。
如何操作...
首先,让我们导入必要的 Python 库并准备好数据集:
-
让我们导入库和函数:
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 -
让我们加载加利福尼亚住房数据集的预测变量和目标变量:
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)接下来,我们将使用
pandas和配方开头描述的公式将连续的HouseAge变量划分为 10 个区间。 -
让我们捕获
HouseAge的最小和最大值:min_value = int(X_train["HouseAge"].min()) max_value = int(X_train["HouseAge"].max()) -
让我们确定区间宽度,即变量的值范围除以箱数:
width = int((max_value - min_value) / 10)如果我们执行
print(width),我们将获得5,这是区间的尺寸。 -
现在我们需要定义区间限制并将它们存储在一个列表中:
interval_limits = [i for i in range( min_value, max_value, width)]如果我们现在执行
print(interval_limits),我们将看到区间限制:[1, 6, 11, 16, 21, 26, 31, 36, 41, 46, 51] -
让我们将第一个和最后一个区间的限制范围扩展,以适应测试集或未来数据源中可能找到的较小或较大的值:
interval_limits[0] = -np.inf interval_limits[-1] = np.inf -
让我们复制 DataFrame,这样我们就不会覆盖原始的 DataFrame,我们将在后续步骤中需要它们:
train_t = X_train.copy() test_t = X_test.copy() -
让我们将
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)
注意
我们已将 include_lowest=True 设置为包含第一个区间中的最低值。请注意,我们使用训练集来找到区间,然后使用这些限制对两个数据集中的变量进行排序。
-
让我们打印离散化和原始变量的前
5个观测值:print(train_t[["HouseAge", "HouseAge_disc"]].head(5))在以下输出中,我们可以看到
52值被分配到 46–无限区间,43值被分配到 41–46 区间,依此类推:HouseAge HouseAge_disc 1989 52.0 (46.0, inf] 256 43.0 (41.0, 46.0] 7887 17.0 (16.0, 21.0] 4581 17.0 (16.0, 21.0] 1993 50.0 (46.0, inf]
注意
区间中的括号和方括号表示一个值是否包含在区间内。例如,(41, 46] 区间包含所有大于 41 且小于或等于 46 的值。
等宽离散化将不同数量的观测值分配给每个区间。
-
让我们绘制一个条形图,显示训练集和测试集中
HouseAge区间的观测值比例: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 – 离散化后每个区间的观测值比例
使用 feature-engine,我们可以用更少的代码行和一次对多个变量执行等宽离散化。
-
首先,让我们导入离散化器:
from feature_engine.discretisation import EqualWidthDiscretiser -
让我们将离散化器设置为将三个连续变量排序到八个区间中:
variables = ['MedInc', 'HouseAge', 'AveRooms'] disc = EqualWidthDiscretiser( bins=8, variables=variables)
注意
EqualWidthDiscretiser() 返回一个整数,表示默认情况下值是否被排序到第一个、第二个或第八个箱中。这相当于顺序编码,我们在 第二章 的 用顺序数字替换类别 菜谱中描述过,编码分类变量。要使用 feature-engine 或 category encoders Python 库执行不同的编码,请将返回的变量作为对象类型,通过设置 return_object 为 True。或者,通过设置 return_boundaries 为 True,让转换器返回区间限制。
-
让我们将离散化器适配到训练集,以便它为每个变量学习切点:
disc.fit(X_train)适配后,我们可以通过执行
print(disc.binner_dict_)来检查binner_dict_属性中的切点。
注意
feature-engine 将自动将下限和上限区间的范围扩展到无限大,以适应未来数据中的潜在异常值。
-
让我们将训练集和测试集中的变量进行离散化:
train_t = disc.transform(X_train) test_t = disc.transform(X_test)EqualWidthDiscretiser()返回一个 DataFrame,其中选定的变量已离散化。如果我们运行test_t.head(),我们将看到以下输出,其中MedInc、HouseAge和AveRooms的原始值被区间编号所取代:

图 4.2 – 包含三个离散化变量:HouseAge、MedInc 和 AveRooms 的 DataFrame
-
现在,让我们通过绘制每个区间的观测比例的条形图来更好地理解等宽离散化的效果:
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 – 离散化后每个区间的观测比例的条形图
现在,让我们使用 scikit-learn 实现等宽离散化。
-
让我们导入 scikit-learn 中的类:
from sklearn.compose import ColumnTransformer from sklearn.preprocessing import kBinsDiscretizer -
让我们通过将其
strategy设置为uniform来设置一个等宽离散化器:disc = KBinsDiscretizer( n_bins=8, encode='ordinal', strategy='uniform')
注意
KBinsDiscretiser() 可以通过将 encoding 设置为 'ordinal' 返回整数箱,或者通过将 encoding 设置为 'onehot-dense' 返回独热编码。
-
让我们使用
ColumnTransformer()来将离散化限制为从 步骤 13 中选择的变量:ct = ColumnTransformer( [("discretizer", disc, variables)], remainder="passthrough", ).set_output(transform="pandas")
注意
将 remainder 设置为 passthrough,ColumnTransformer() 返回变换后的输入 DataFrame 中的所有变量。要仅返回变换后的变量,将 remainder 设置为 drop。
-
让我们将离散化器拟合到训练集,以便它学习区间界限:
ct.fit(X_train) -
最后,让我们对训练集和测试集中的选定变量进行离散化:
train_t = ct.transform(X_train) test_t = ct.transform(X_test)我们可以通过执行
ct.named_transformers_["discretizer"].bin_edges_来检查变压器学习到的切分点。
注意
ColumnTransformer() 将 discretize 添加到已离散化的变量,将 remainder 添加到未修改的变量。
我们可以通过执行 test_t.head() 来检查输出。
它是如何工作的…
在这个菜谱中,我们将变量值排序到等距区间中。要使用 pandas 进行离散化,我们首先使用 max() 和 min() 方法找到 HouseAge 变量的最大值和最小值。然后,我们通过将值范围除以任意箱子的数量来估计区间宽度。有了宽度和最小值、最大值,我们确定了区间界限并将它们存储在一个列表中。我们使用这个列表与 pandas 的 cut() 方法将变量值排序到区间中。
注意
Pandas 的 cut() 默认按等大小区间对变量进行排序。它将在每侧扩展变量范围的 .1%,以包含最小值和最大值。我们手动生成区间的理由是为了适应在部署我们的模型时,未来数据源中可能出现的比数据集中看到的更小或更大的值。
离散化后,我们通常将区间视为分类值。默认情况下,pandas 的 cut() 函数返回区间值作为有序整数,这相当于顺序编码。或者,我们可以通过将 labels 参数设置为 None 来返回区间限制。
为了显示每个区间的观测数,我们创建了一个条形图。我们使用 pandas 的 value_counts() 函数来获取每个区间的观测数比例,该函数返回一个 pandas Series,其中索引是区间,计数是值。为了绘制这些比例,首先,我们使用 pandas 的 concat() 函数在一个 DataFrame 中连接了训练集和测试集系列,并将其分配给 train 和 test 列名称。最后,我们使用 plot.bar() 显示条形图。我们使用 Matplotlib 的 xticks() 函数旋转标签,并使用 xlabels() 和 ylabel() 添加了 x 和 y 图例,以及使用 title() 添加了标题。
要使用 feature-engine 进行等宽离散化,我们使用了 EqualWidthDiscretiser(),它接受区间数量和要离散化的变量作为参数。通过 fit(),离散化器为每个变量学习了区间限制。通过 transform(),它将值排序到每个区间。
EqualWidthDiscretiser() 默认返回按顺序排列的整数作为区间,这相当于顺序编码。为了在 feature-engine 或 category encoders 库中跟随任何其他编码过程进行离散化,我们需要通过在设置转换器时将 return_object 设置为 True 来返回作为对象的区间。
注意
EqualWidthDiscretiser() 默认将第一个和最后一个区间的值扩展到负无穷和正无穷,以自动适应训练集中未见到的较小和较大的值。
我们使用条形图跟随离散化,以显示每个转换变量的每个区间的观测数比例。我们可以看到,如果原始变量是偏斜的,条形图也是偏斜的。注意 MedInc 和 AveRooms 变量的某些区间,这些变量具有偏斜分布,其中包含非常少的观测值。特别是,尽管我们想要为 AveRooms 创建八个区间,但只有足够的数据来创建五个,并且大多数变量的值都被分配到了第一个区间。
最后,我们使用 scikit-learn 的 KBinsDiscretizer() 将三个连续变量离散化到等宽区间。为了创建等宽区间,我们将 strategy 参数设置为 uniform。通过 fit(),转换器学习了区间的限制,而通过 transform(),它将值排序到每个区间。
我们使用了 ColumnTransformer() 来限制离散化到选定的变量,并将转换输出设置为 pandas,以在转换后获得 DataFrame。KBinsDiscretizer() 可以返回作为序数的区间,正如我们在配方中所做的那样,或者作为 one-hot 编码的变量。这种行为可以通过 encode 参数进行修改。
参考以下内容
为了比较等宽离散化与更复杂的方法,请参阅 Dougherty J, Kohavi R, Sahami M. Supervised and unsupervised discretization of continuous features. In: Proceedings of the 12th international conference on machine learning. San Francisco: Morgan Kaufmann; 1995. p. 194–202.
实现等频离散化
等宽离散化直观且易于计算。然而,如果变量是偏斜的,那么将会有很多空区间或只有少数值的区间,而大多数观测值将被分配到少数几个区间。这可能导致信息丢失。这个问题可以通过自适应地找到区间切点来解决,使得每个区间包含相似比例的观测值。
等频离散化将变量的值划分为具有相同观测比例的区间。区间宽度由 分位数 决定。分位数是分割数据为相等部分的值。例如,中位数是一个将数据分为两半的分位数。四分位数将数据分为四个相等的部分,而百分位数将数据分为 100 个相等大小的部分。因此,区间可能具有不同的宽度,但观测数数量相似。区间的数量由用户定义。
在本配方中,我们将使用 pandas、scikit-learn 和 feature-engine 来执行等频离散化。
如何操作...
首先,让我们导入必要的 Python 库并准备数据集:
-
让我们导入所需的 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 -
让我们将加利福尼亚住房数据集加载到 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) -
让我们复制 DataFrame:
train_t = X_train.copy() test_t = X_test.copy() -
我们将使用 pandas 的
qcut()函数来获取HouseAge变量的离散化副本,并将其存储为训练集中的一个新列,以及八个等频区间的界限:train_t["House_disc"], interval_limits = pd.qcut( x=X_train["HouseAge"], q=8, labels=None, retbins=True, )如果你执行
print(interval_limits),你会看到以下区间界限:array([ 1., 14., 18., 24., 29., 34., 37., 44., 52.])。 -
让我们打印离散化和原始变量的前五个观测值:
print(train_t[["HouseAge", "House_disc"]].head(5))在以下输出中,我们可以看到
52值被分配到 44–52 区间,43值被分配到 37–44 区间,依此类推:HouseAge House_disc 1989 52.0 (44.0, 52.0] 256 43.0 (37.0, 44.0] 7887 17.0 (14.0, 18.0] 4581 17.0 (14.0, 18.0] HouseAge in the test set, using pandas cut() with the interval limits determined in *step 5*:test_t["House_disc"] = pd.cut(
x=X_test["HouseAge"],
bins=interval_limits,
include_lowest=True)
-
让我们制作一个条形图,展示训练集和测试集中每个区间的观测比例:
# 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()在以下图中,我们可以看到每个区间包含相似比例的观测值:

图 4.4 – HouseAge 在等频率离散化后的每个区间的观测比例
使用feature-engine,我们可以将多个变量应用等频率离散化。
-
让我们导入离散化器:
from feature_engine.discretisation import EqualFrequencyDiscretiser -
让我们设置转换器,将三个连续变量离散化到八个区间:
variables = ['MedInc', 'HouseAge', 'AveRooms'] disc = EqualFrequencyDiscretiser( q=8, variables=variables, return_boundaries=True)
注
使用return_boundaries=True,转换器将在离散化后返回区间边界。要返回区间编号,将其设置为False。
-
让我们将离散化器拟合到训练集,以便它学习区间限制:
disc.binner_dict_ attribute.
注
feature-engine将自动将下限和上限区间的限制扩展到无限,以适应未来数据中的潜在异常值。
-
让我们将训练集和测试集中的变量进行转换:
train_t = disc.transform(X_train) test_t = disc.transform(X_test) -
让我们用每个区间的观测比例制作条形图,以便更好地理解等频率离散化的效果:
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()在以下图中,我们可以看到区间具有相似比例的观测值:

图 4.5 – 对三个变量进行等频率离散化后每个区间的观测比例。
现在,让我们使用 scikit-learn 进行等频率离散化:
-
让我们导入转换器:
from sklearn.preprocessing import KBinsDiscretizer -
让我们设置离散化器,将变量排序到八个等频率的区间:
disc = KBinsDiscretizer( n_bins=8, encode='ordinal', strategy='quantile') -
让我们将离散化器拟合到包含从步骤 10的变量的训练集切片,以便它学习区间限制:
disc.fit(X_train[variables])
注
scikit-learn 的KBinsDiscretiser()将离散化数据集中的所有变量。要仅对子集进行离散化,我们将转换器应用于包含感兴趣变量的 DataFrame 切片。或者,我们可以通过使用ColumnTransformer()来限制离散化到变量的子集,就像我们在执行等宽 离散化配方中所做的那样。
-
让我们复制包含我们将存储离散化变量的 DataFrames:
train_t = X_train.copy() test_t = X_test.copy() -
最后,让我们将训练集和测试集中的变量进行转换:
train_t[variables] = disc.transform( X_train[variables]) test_t[variables] = disc.transform(X_test[variables])
我们可以通过执行disc.bin_edges_来检查切分点。
它是如何工作的...
在这个配方中,我们将变量值排序到具有相似观测比例的区间中。
我们使用 pandas 的 qcut() 从训练集中识别区间限制,并将 HouseAge 变量的值排序到这些区间中。接下来,我们将这些区间限制传递给 pandas 的 cut(),以在测试集中对 HouseAge 进行离散化。请注意,pandas 的 qcut(),就像 pandas 的 cut() 一样,返回区间值作为有序整数,这相当于顺序编码,
注意
在等频率离散化中,小连续范围内值的许多出现可能导致具有非常相似值的观测值,从而产生不同的区间。这个问题在于,它可能会在实际上性质相当相似的数据点之间引入人为的区别,从而偏置模型或后续数据分析。
使用 Feature-engine 的 EqualFrequencyDiscretiser(),我们将三个变量离散化到八个箱中。通过 fit(),离散化器学习了区间限制并将它们存储在 binner_dict_ 属性中。通过 transform(),观测值被分配到各个箱中。
注意
EqualFrequencyDiscretiser() 返回一个整数,表示默认情况下值是否被排序到第一个、第二个或第八个箱中。这相当于顺序编码,我们在第二章的用顺序数字替换类别食谱中描述过,编码 分类变量。
要使用不同类型的编码来跟进离散化,我们可以通过将 return_object 设置为 True 来返回作为对象的变量,然后使用任何 feature-engine 或 category encoders 转换器。或者,我们可以返回区间限制,就像在这个食谱中所做的那样。
最后,我们使用 scikit-learn 的 KBinsDiscretizer() 将变量离散化到八个等频率箱中。通过 fit(),转换器学习了切点并将它们存储在其 bin_edges_ 属性中。通过 transform(),它将值排序到每个区间中。请注意,与 EqualFrequencyDiscretiser() 不同,KBinsDiscretizer() 将转换数据集中的所有变量。为了避免这种情况,我们只在需要修改变量的数据子集上应用离散化器。
注意
scikit-learn 的 KbinsDiscretizer 有一个选项可以返回作为顺序数字或独热编码的区间。可以通过 encode 参数修改行为。
将变量离散化到任意区间
在各个行业中,将变量值分组到对业务有意义的段是常见的。例如,我们可能希望将变量年龄分组到代表儿童、年轻人、中年人和退休人员的区间。或者,我们可能将评级分为差、好和优秀。有时,如果我们知道变量处于某个尺度(例如,对数尺度),我们可能希望在尺度内定义区间切点。
在这个食谱中,我们将使用 pandas 和 feature-engine 将变量离散化到预定义的用户区间。
人口值在 0 到大约 40,000 之间变化:
让我们检查原始变量和离散化变量的前五行:
-
导入 Python 库和类:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import fetch_california_housing -
让我们将加利福尼亚住房数据集加载到
pandasDataFrame 中:X, y = fetch_california_housing( return_X_y=True, as_frame=True) -
让我们绘制
Population变量的直方图以找出其值范围:X["Population"].hist(bins=30) plt.title("Population") plt.ylabel("Number of observations") plt.show()![图 4.6 – Population 变量的直方图]()
首先,让我们导入必要的 Python 库并准备好数据集:
图 4.6 – Population 变量的直方图
-
让我们创建一个具有任意区间限制的列表,将上限设置为无穷大以适应更大的值:
intervals = [0, 200, 500, 1000, 2000, np.inf] -
让我们创建一个包含区间限制的字符串列表:
labels = ["0-200", "200-500", "500-1000", "1000-2000", ">2000"] -
让我们复制数据集并使用步骤 4中的预定义限制对
Population变量进行离散化:X_t = X.copy() X_t[«Population_limits»] = pd.cut( X["Population"], bins=intervals, labels=None, include_lowest=True) -
现在,让我们将
Population离散化到预定义的区间,并使用我们在步骤 5中定义的标签来命名这些区间以进行比较:X_t[«Population_range»] = pd.cut( X[„Population"], bins=intervals, labels=labels, include_lowest=True) -
如何操作...
X_t[['Population', 'Population_range', 'Population_limits']].head()在 DataFrame 的最后两列中,我们可以看到离散化变量:第一个以我们在步骤 5中创建的字符串作为值,第二个以区间限制作为值:
Population Population_range Population_limits 0 322.0 200-500 (200.0, 500.0] 1 2401.0 >2000 (2000.0, inf] 2 496.0 200-500 (200.0, 500.0] 3 558.0 500-1000 (500.0, 1000.0] 4 565.0 500-1000 (500.0, 1000.0]
注意
我们只需要变量版本中的一个,无论是值范围还是区间限制。在这个菜谱中,我创建了两个来突出pandas提供的不同选项。
-
最后,我们可以计算并绘制每个区间内的观测数:
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 – 离散化后每个区间的观测数比例。
为了总结这个菜谱,让我们利用feature-engine对多个变量进行离散化:
-
让我们导入转换器:
from feature_engine.discretisation import ArbitraryDiscretiser -
让我们创建一个字典,以变量作为键,以区间限制作为值:
intervals = { "Population": [0, 200, 500, 1000, 2000, np.inf], "MedInc": [0, 2, 4, 6, np.inf]} -
让我们使用步骤 11中的限制设置离散化器:
discretizer = ArbitraryDiscretiser( binning_dict=intervals, return_boundaries=True) -
现在,我们可以继续对变量进行离散化:
X_t = discretizer.fit_transform(X)如果我们执行
X_t.head(),我们将看到以下输出,其中Population和MedInc变量已被离散化:

图 4.8 – 包含离散化变量的 DataFrame
让我们检查原始变量和离散化变量的前五行:
人口值在 0 到大约 40,000 之间变化:
在这个菜谱中,我们将一个变量的值排序到用户定义的区间中。首先,我们绘制了Population变量的直方图,以了解其值范围。接下来,我们任意确定了区间的界限,并将它们记录在一个列表中。我们创建了包含 0–200、200–500、500–1000、1000–2000 以及超过 2,000 的区间,通过将上限设置为np.inf来表示无限大。然后,我们创建了一个包含区间名称的字符串列表。使用 pandas 的cut()函数并传递包含区间界限的列表,我们将变量值排序到预先定义的箱中。我们执行了两次命令;在第一次运行中,我们将labels参数设置为None,结果返回区间界限。在第二次运行中,我们将labels参数设置为字符串列表。我们将返回的输出捕获在两个变量中:第一个变量显示区间界限作为值,第二个变量具有字符串作为值。最后,我们使用 pandas 的value_counts()函数统计每个变量的观测数。
最后,我们使用feature-engine的ArbitraryDiscretiser()函数自动化了该过程。这个转换器接受一个字典,其中包含要离散化的变量作为键,以及作为值的区间界限列表,然后在底层使用 pandas 的cut()函数来离散化变量。使用fit()时,转换器不会学习任何参数,但会检查变量是否为数值型。使用transform()时,它会离散化变量。
使用 k-means 聚类进行离散化
离散化过程的目标是找到一组切割点,将变量划分为具有良好类别一致性的少量区间。为了创建将相似观测值分组在一起的分区,我们可以使用 k-means 等聚类算法。
在使用 k-means 聚类进行离散化时,分区是由 k-means 算法识别的聚类。k-means 聚类算法有两个主要步骤。在初始化步骤中,随机选择k个观测值作为k个聚类的初始中心,剩余的数据点被分配到最近的聚类中。聚类接近度是通过距离度量来衡量的,例如欧几里得距离。在迭代步骤中,聚类的中心被重新计算为聚类内所有观测值的平均值,观测值被重新分配到新创建的最近聚类。迭代步骤会继续进行,直到找到最优的k个中心。
使用 k-means 进行离散化需要一个参数,即k,即聚类数量。有几种方法可以确定最佳聚类数量。其中之一是肘部法,我们将在本食谱中使用这种方法。该方法包括使用不同的k值在数据上训练几个 k-means 算法,然后确定聚类返回的解释变异。在下一步中,我们将解释变异作为聚类数量k的函数进行绘图,并选择曲线的肘部作为要使用的聚类数量。肘部是表明增加k的数量不会显著增加模型解释的变异的拐点。有不同指标可以量化解释变异。我们将使用每个点到其分配中心的平方距离之和。
在本食谱中,我们将使用 Python 库yellowbrick来确定最佳聚类数量,然后使用 scikit-learn 执行 k-means 离散化。
如何操作...
让我们先导入必要的 Python 库并准备好数据集:
-
导入所需的 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 -
让我们将加利福尼亚住房数据集加载到
pandasDataFrame 中:X, y = fetch_california_housing( return_X_y=True, as_frame=True) -
应该使用训练集来确定 k-means 最佳聚类,因此让我们将数据分为训练集和测试集:
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=0) -
让我们创建一个包含要转换的变量的列表:
variables = ['MedInc', 'HouseAge', 'AveRooms'] -
让我们设置一个 k-means 聚类算法:
k_means = KMeans(random_state=10) -
现在,使用 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 变量的聚类数量与解释变异的关系
-
让我们设置一个使用 k-means 聚类创建六个分区并返回聚类作为独热编码变量的离散化器:
disc = KBinsDiscretizer( n_bins=6, encode="onehot-dense", strategy="kmeans", subsample=None, ).set_output(transform="pandas") -
让我们将离散化器拟合到包含要离散化变量的 DataFrame 切片,以便它为每个变量找到聚类:
disc.fit(X_train[variables])
注意
在本食谱中,我们将所有三个变量的值排序到六个聚类中。要将MedInc和HouseAge离散化到六个分区,将AveRooms离散化到七个分区,我们需要为每个变量组设置一个离散化器实例,并使用ColumnTransformer()来限制离散化到每个组。
-
让我们检查切分点:
disc.bin_edges_每个数组包含
MedInc、HouseAge和AveRooms六个聚类的切分点:array([array([0.4999, 2.49587954, 3.66599029, 4.95730115, 6.67700141, 9.67326677, 15.0001]), array([1., 11.7038878, 19.88430419, 27.81472503, 35.39424098, 43.90930314, 52.]), array([0.84615385, 4.84568771, 6.62222005, 15.24138445, 37.60664483, 92.4473438, 132.53333333])], dtype=object) -
让我们从训练测试集中获取变量的离散化形式:
train_features = disc.transform(X_train[variables]) test_features = disc.transform(X_test[variables])使用
print(test_features),我们可以检查离散化器返回的 DataFrame。它包含 18 个二进制变量,对应于每个三个数值变量返回的六个簇的一热编码转换:MedInc_0.0 MedInc_1.0 MedInc_2.0 MedInc_3.0 MedInc_4.0 MedInc_5.0 \ 14740 0.0 0.0 1.0 0.0 0.0 0.0 10101 0.0 0.0 0.0 1.0 0.0 0.0 20566 0.0 0.0 1.0 0.0 0.0 0.0 2670 1.0 0.0 0.0 0.0 0.0 0.0 15709 0.0 0.0 0.0 1.0 0.0 0.0 HouseAge_0.0 HouseAge_1.0 HouseAge_2.0 HouseAge_3.0 HouseAge_4.0 \ 14740 0.0 0.0 1.0 0.0 0.0 10101 0.0 0.0 0.0 1.0 0.0 20566 0.0 0.0 0.0 1.0 0.0 2670 0.0 0.0 0.0 0.0 1.0 15709 0.0 0.0 1.0 0.0 0.0 HouseAge_5.0 AveRooms_0.0 AveRooms_1.0 AveRooms_2.0 AveRooms_3.0 \ 14740 0.0 0.0 1.0 0.0 0.0 10101 0.0 0.0 1.0 0.0 0.0 20566 0.0 0.0 1.0 0.0 0.0 2670 0.0 0.0 1.0 0.0 0.0 15709 0.0 1.0 0.0 0.0 0.0 AveRooms_4.0 AveRooms_5.0 14740 0.0 0.0 10101 0.0 0.0 20566 0.0 0.0 2670 0.0 0.0 15709 0.0 0.0
您可以使用 pandas 将结果连接到原始 DataFrame,然后删除原始数值变量。或者,使用 ColumnTransformer() 类将离散化限制为所选变量,并通过将 remainder 设置为 "passthrough" 将结果添加到数据中。
它是如何工作的...
在这个配方中,我们使用 k-means 聚类进行了离散化。首先,我们通过使用 Yellowbrick 的 KElbowVisualizer() 利用肘部方法确定了最佳簇数量。
要执行 k-means 离散化,我们使用了 scikit-learn 的 KBinsDiscretizer(),将 strategy 设置为 kmeans,并在 n_bins 参数中将簇的数量设置为六。使用 fit(),转换器通过 k-means 算法学习了簇边界。使用 transform(),它将变量值排序到相应的簇。我们将 encode 设置为 "onehot-dense";因此,在离散化后,转换器对簇应用了一热编码。我们还设置了离散化器的输出为 pandas,因此转换器返回了作为 DataFrame 的聚类变量的一个热编码版本。
参见
-
在 Palaniappan 和 Hong, Discretization of Continuous Valued Dimensions in OLAP Data Cube 文章中描述了使用 k-means 进行离散化。国际计算机科学和网络安全杂志,第 8 卷第 11 期,2008 年 11 月。
paper.ijcsns.org/07_book/200811/20081117.pdf。 -
要了解更多关于肘部方法的信息,请访问 Yellowbrick 的文档和参考资料:
www.scikit-yb.org/en/latest/api/cluster/elbow.html。 -
要了解确定 k-means 聚类拟合的其他方法,请查看 Yellowbrick 中的其他可视化工具:
www.scikit-yb.org/en/latest/api/cluster/index.html。
实现特征二值化
一些数据集包含稀疏变量。稀疏变量是指大多数值都是 0 的变量。稀疏变量的经典例子是通过词袋模型从文本数据中得到的,其中每个变量是一个单词,每个值代表单词在某个文档中出现的次数。鉴于一个文档包含有限数量的单词,而特征空间包含所有文档中出现的单词,大多数文档,即大多数行,对于大多数列将显示 0 值。然而,单词并不是唯一的例子。如果我们考虑房屋细节数据,桑拿数量变量对于大多数房屋也将是 0。总之,一些变量具有非常偏斜的分布,其中大多数观测值显示相同的值,通常是 0,而只有少数观测值显示不同的值,通常是更高的值。
为了更简单地表示这些稀疏或高度偏斜的变量,我们可以通过将所有大于 1 的值裁剪为 1 来对它们进行二值化。实际上,二值化通常在文本计数数据上执行,我们考虑的是特征的缺失或存在,而不是单词出现次数的量化。
在本配方中,我们将使用scikit-learn执行二值化。
准备工作
我们将使用一个包含单词袋的数据集,该数据集可在 UCI 机器学习仓库(https://archive.ics.uci.edu/ml/datasets/Bag+of+Words)中找到。它受 CC BY 4.0 许可(creativecommons.org/licenses/by/4.0/legalcode)。
我下载并准备了一个小型的单词袋数据集,它代表了一个数据集的简化版本。您可以在附带的 GitHub 仓库中找到这个数据集:
如何操作...
让我们先导入库并加载数据:
-
让我们导入所需的 Python 库、类和数据集:
import pandas as pd import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from sklearn.preprocessing import Binarizer -
让我们加载数据集,该数据集包含单词作为列,不同的文本作为行:
data = pd.read_csv("bag_of_words.csv") -
让我们显示直方图以可视化变量的稀疏性:
data.hist(bins=30, figsize=(20, 20), layout=(3,4)) plt.show()在以下直方图中,我们可以看到不同的单词在大多数文档中出现的次数为零:

图 4.10 – 表示每个单词在文档中出现的次数的直方图
-
让我们设置
binarizer以裁剪所有大于 1 的值到 1,并返回结果为 DataFrames:binarizer = Binarizer(threshold = 0) .set_output(transform="pandas") -
让我们二值化变量:
data_t = binarizer.fit_transform(data)现在,我们可以通过显示直方图来探索二值化变量的分布,就像在步骤 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()在下面的图表中,我们可以看到二值化变量,其中大多数出现次数显示的是
0值:

图 4.11 – 包含显示或不显示每个单词的文档数量的条形图
就这样;现在我们有了数据的一个更简单的表示。
它是如何工作的...
在这个配方中,我们将稀疏变量的表示方式改为考虑出现的存在或不存在,在我们的案例中,这是一个单词。数据由一个词袋组成,其中每个变量(列)是一个单词,每行是一个文档,值表示单词在文档中出现的次数。大多数单词不会出现在大多数文档中;因此,数据中的大多数值都是 0。我们通过直方图证实了数据的稀疏性。
scikit-learn 的Binarizer()将大于阈值的值映射到1,在我们的案例中,这个阈值是 0,而小于或等于阈值的值被映射到 0。Binarizer()有fit()和transform()方法,其中fit()不做任何事情,而transform()对变量进行二值化。
Binarizer()默认通过 NumPy 数组修改数据集中的所有变量。要返回pandas数据框,我们将转换输出设置为pandas。
使用决策树进行离散化
在本章的所有先前配方中,我们任意确定区间的数量,然后离散化算法会以某种方式找到区间界限。决策树可以自动找到区间界限和最优的箱数。
决策树方法在学习过程中对连续属性进行离散化。在每个节点,决策树评估一个特征的所有可能值,并通过利用性能指标(如熵或基尼不纯度用于分类,或平方或绝对误差用于回归)选择最大化类别分离或样本一致性的切割点。因此,观察结果根据它们的特征值是否大于或小于某些切割点而最终落在某些叶子节点上。
在下面的图中,我们可以看到训练用来根据房产的平均房间数预测房价的决策树的图:

图 4.12 – 基于房产平均房间数预测房价的决策树图
基于此决策树,平均房间数小于 5.5 的房屋将进入第一个叶子节点,平均房间数在 5.5 到 6.37 之间的房屋将进入第二个叶子节点,平均房间数在 6.37 到 10.77 之间的房屋将进入第三个叶子节点,平均房间数大于 10.77 的房屋将进入第四个叶子节点。
如你所见,按照设计,决策树可以找到将变量分割成具有良好类别一致性的区间的切割点集。
在这个菜谱中,我们将使用 Feature-engine 执行基于决策树的离散化。
如何做到这一点...
让我们从导入一些库和加载数据开始:
-
让我们导入所需的 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 -
让我们将加利福尼亚住房数据集加载到
pandasDataFrame 中,然后将其拆分为训练集和测试集: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) -
让我们创建一个包含要离散化变量名的列表:
variables = list(X.columns)[:-2]如果我们执行
print(variables),我们将看到以下变量名:['MedInc','HouseAge','AveRooms','AveBedrms','Population','AveOccup']。 -
让我们设置转换器来离散化第 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]}, ) -
让我们使用训练集来拟合离散化器,以便它为每个变量找到最佳的决策树:
disc.fit(X_train, y_train)
注意
你可以通过执行 disc.binner_dict_ 来检查每个变量在binner_dict_属性中找到的区间限制。注意离散化器如何将负无穷和正无穷添加到限制中,以适应训练集中观察到的较小和较大的值。
-
让我们离散化变量,然后显示转换训练集的前五行:
train_t = disc.transform(X_train) test_t = disc.transform(X_test) train_t[variables].head()在以下输出中,我们可以看到每个观测值分配的区间限制:

图 4.13 – 包含离散化变量的转换训练集的前五行
注意
如果你选择返回区间限制并想使用这些数据集来训练机器学习模型,你需要对离散化进行后续的一热编码或序数编码。请参阅第二章,“分类变量编码”,以获取更多详细信息。
-
而不是返回区间限制,我们可以通过设置转换器如下来返回每个观测值分配的区间编号:
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]}) -
我们现在可以拟合并转换训练集和测试集:
train_t = disc.fit_transform(X_train, y_train) test_t = disc.transform(X_test)如果你现在执行
train_t[variables].head(),你将看到整数作为结果而不是区间限制:

图 4.14 – 包含离散化变量的转换训练集的前五行
为了总结这个菜谱,我们将使离散化器返回树的预测作为离散化变量的替换值:
-
让我们设置转换器以返回预测,然后将其拟合到训练集,并最终转换两个数据集:
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) -
让我们探索
AveRooms变量在离散化前后的唯一值数量:X_test["AveRooms"].nunique(), test_t["AveRooms"].nunique()在以下输出中,我们可以看到决策树的预测也是离散的或有限的,因为树包含有限数量的终端叶子;
7,而原始变量包含超过 6000 个不同的值:(6034, 7) -
为了更好地理解树的结构,我们可以将其捕获到一个变量中:
tree = disc.binner_dict_["AveRooms"].best_estimator_
注意
当我们将转换器设置为返回整数或区间限制时,我们将在 binner_dict_ 属性中获得区间限制。如果我们设置转换器以返回树预测,binner_dict_ 将包含每个变量的训练树。
-
现在,我们可以显示树结构:
fig = plt.figure(figsize=(20, 6)) plot_tree(tree, fontsize=10, proportion=True) plt.show() -
在以下图中,我们可以看到树根据房间数量的平均值将样本分配到不同的终端叶子所使用的值:

图 4.15 – 训练用于离散化 AveRooms 的决策树结构
-
为了总结这个方法,我们可以绘制三个变量的每个区间的观察值数量:
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 – 使用决策树离散化变量后的每个区间的观察值比例
如图中所示,使用决策树进行离散化会在每个节点或区间返回不同的观察值比例。
它是如何工作的...
要使用决策树进行离散化,我们使用了 feature-engine 的 DecisionTreeDiscretiser()。这个转换器使用每个变量作为离散化的输入来拟合决策树,并优化模型的超参数以找到基于性能指标的最佳分区。它自动找到了最佳区间数量以及它们的限制,并返回限制、区间编号或预测作为结果。
更多内容...
feature-engine 的实现灵感来源于 KDD 2009 数据科学竞赛的获胜方案。获胜者通过基于连续特征获取决策树的预测来创建新特征。您可以在《使用集成选择赢得 KDD Cup Orange 挑战赛》一文中找到更多详细信息,该文章系列位于 www.mtome.com/Publications/CiML/CiML-v3-book.pdf 的第 27 页。
为了回顾离散化技术,您可能会发现以下文章很有用:
-
Dougherty 等人,监督和非监督连续特征离散化,机器学习:第 12 届国际会议论文集,1995 年,(
ai.stanford.edu/~ronnyk/disc.pdf)。 -
Lu 等人,离散化:一种使能技术,数据挖掘与知识发现,第 6 卷,第 393–423 页,2002 年,(
www.researchgate.net/publication/220451974_Discretization_An_Enabling_Technique)。 -
Garcia 等人,离散化技术综述:监督学习中的分类和实证分析,IEEE 知识数据工程杂志 25 (4),2013 年,(
ieeexplore.ieee.org/document/6152258)。
第五章:与异常值一起工作
异常值是指与变量中其他值显著不同的数据点。异常值可能源于特征本身的固有变异性,表现为在分布中不常出现的极端值(通常出现在尾部)。它们可能是实验误差或数据收集过程中的不准确性的结果,或者它们可能表明重要事件。例如,信用卡交易中异常高的费用可能表明欺诈活动,需要标记并可能阻止卡片以保护客户。同样,异常不同的肿瘤形态可能表明恶性,需要进一步检查。
异常值可以对统计分析产生不成比例的巨大影响。例如,少数异常值可以逆转测试的统计显著性(例如 A/B 测试)或直接影响统计模型参数的估计(例如系数)。一些机器学习模型因其对异常值的敏感性而闻名,如线性回归。其他模型因其对异常值的鲁棒性而闻名,如基于决策树的模型。AdaBoost 据说对目标变量的异常值敏感,原则上,基于距离的模型,如 PCA 和 KNN,也可能受到异常值存在的影响。
并没有严格的数学定义来界定什么算是异常值,也没有关于如何在统计或机器学习模型中处理异常值的共识。如果异常值源于数据收集的缺陷,丢弃它们似乎是一个安全的选择。然而,在许多数据集中,确定异常值的准确性质是具有挑战性的。最终,检测和处理异常值仍然是一项主观的练习,依赖于领域知识和对它们对模型潜在影响的了解。
在本章中,我们将首先讨论识别潜在异常值的方法,或者更确切地说,识别那些与整体显著不同的观察结果。然后,我们将在假设这些观察结果对分析不相关的前提下继续讨论,并展示如何通过截断移除它们或减少它们对模型的影响。
本章包含以下食谱:
-
使用箱线图和四分位数间距规则可视化异常值
-
使用均值和标准差查找异常值
-
使用中位数绝对偏差查找异常值
-
移除异常值
-
将异常值拉回到可接受的范围内
-
应用 winsorization
技术要求
在本章中,我们将使用 Python 的numpy、pandas、matplotlib、seaborn和feature-engine库。
使用箱线图和四分位数间距规则可视化异常值
可视化异常值的一种常见方法是使用箱线图。箱线图基于四分位数提供变量的标准化分布显示。箱子包含第一和第三四分位数内的观测值,称为四分位距(IQR)。第一四分位数是低于该值的观测值占 25%(相当于第 25 百分位数),而第三四分位数是低于该值的观测值占 75%(相当于第 75 百分位数)。IQR 的计算如下:

箱线图也显示须须,这些须须是从箱子的两端向外延伸到最小值和最大值的线条,并延伸到一个极限。这些极限由分布的最小值或最大值给出,或者在存在极端值的情况下,由以下方程给出:


根据四分位数间距规则(IQR proximity rule),如果值落在由前述方程确定的须须极限之外,我们可以将其视为异常值。在箱线图中,异常值用点表示。
注意
如果变量服从正态分布,大约 99%的观测值将位于由须须定义的区间内。因此,我们可以将须须之外的值视为异常值。然而,箱线图是非参数的,这就是为什么我们也使用它们来可视化偏斜变量的异常值。
在这个菜谱中,我们将首先使用箱线图可视化变量分布,然后我们将手动计算触须的极限来识别那些我们可以将其视为异常值的点。
如何做到这一点...
我们将使用seaborn库创建箱线图。让我们首先导入 Python 库并加载数据集:
-
让我们导入 Python 库和数据集:
import matplotlib.pyplot as plt import seaborn as sns from sklearn.datasets import fetch_california_housing -
修改
seaborn的默认背景(它使图表更美观,但这当然是主观的):sns.set(style="darkgrid") -
从 scikit-learn 加载加利福尼亚房价数据集:
X, y = fetch_california_housing( return_X_y=True, as_frame=True) -
绘制
MedInc变量的箱线图以可视化其分布:plt.figure(figsize=(8, 3)) sns.boxplot(data=X["MedInc"], orient="y") plt.title("Boxplot") plt.show()在下面的箱线图中,我们识别出包含在四分位数范围(IQR)内的观测值所在的箱子,即第一和第三四分位数之间的观测值。我们还可以看到触须。在左侧,触须延伸到
MedInc的最小值;在右侧,触须延伸到第三四分位数加上 1.5 倍的四分位数范围。超出右侧触须的值用点表示,可能构成异常值:

图 5.1 – 突出显示分布右侧尾部的潜在异常值的MedInc变量的箱线图
注意
如图 5.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() -
让我们使用之前的函数来创建
MedInc变量的绘图:plot_boxplot_and_hist(X, "MedInc")在下面的图中,我们可以看到箱线图和变量分布在直方图中的关系。注意
MedInc的大多数观测值都位于四分位数范围内。MedInc的潜在异常值位于右侧尾部,对应于收入异常高的个人:

图 5.2 – 箱线图和直方图 – 显示变量分布的两种方式
现在我们已经看到了如何可视化异常值,接下来让我们看看如何计算分布两侧的异常值所在的极限。
-
让我们创建一个函数,该函数根据 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 的quantile函数来确定这些值。
-
使用第 7 步中的函数,我们将计算
MedInc的极端极限:lower_limit, upper_limit = find_limits( X, "MedInc", 1.5)如果我们现在执行
lower_limit和upper_limit,我们将看到-0.7063和8.013这两个值。下限超出了MedInc的最小值,因此在箱线图中,触须只延伸到最小值。另一方面,上限与右触须的极限相吻合。
注意
乘以 IQR 的常见值是 1.5,这是箱线图中的默认值,或者如果我们想更保守一些,是 3。
-
让我们显示
HouseAge变量的箱线图和直方图:plot_boxplot_and_hist(X, "HouseAge")我们可以看到,这个变量似乎不包含异常值,因此箱线图中的触须延伸到最小值和最大值:

图 5.3 – HouseAge 变量的箱线图和直方图
-
让我们根据四分位数间距规则找到变量的极限:
lower_limit, upper_limit = find_limits( X, "HouseAge", 1.5)
如果我们执行 lower_limit 和 upper_limit,我们将看到 -10.5 和 65.5 这两个值,它们超出了图表的边缘,因此我们没有看到任何异常值。
它是如何工作的...
在这个菜谱中,我们使用了 Seaborn 的 boxplot 方法来创建箱线图,然后我们根据四分位数间距规则计算了可以被认为是异常值的极限。
在 图 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) 来确定精确值)。触须延伸到分布的最小值和最大值。图表中触须的极限与基于四分位数间距规则(我们在 步骤 10 中计算的)的极限不一致,因为这些极限远远超出了这个变量观察到的值范围。
使用均值和标准差查找异常值
在正态分布的变量中,大约 99.8% 的观测值位于均值加减三倍标准差的区间内。因此,超出这些极限的值可以被认为是异常值;它们是罕见的。
注意
使用均值和标准差来检测异常值有一些缺点。首先,它假设包括异常值在内的正态分布。其次,异常值强烈影响均值和标准差。因此,一个推荐的替代方案是中位数绝对偏差(MAD),我们将在下一个菜谱中讨论。
在这个菜谱中,我们将识别异常值为那些位于均值加减三倍标准差定义的区间之外的观测值。
如何做到...
让我们开始菜谱,导入 Python 库并加载数据集:
-
让我们导入 Python 库和数据集:
import numpy as np import matplotlib.pyplot as plt import seaborn as sns from sklearn.datasets import load_breast_cancer -
从 scikit-learn 加载乳腺癌数据集:
X, y = load_breast_cancer( return_X_y=True, as_frame=True) -
创建一个函数来在直方图旁边绘制箱线图:
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 的函数,使用箱线图和四分位数接近规则可视化异常值。
-
让我们绘制
meansmoothness变量的分布:plot_boxplot_and_hist(X, "mean smoothness")在下面的箱线图中,我们看到变量的值显示出类似于正态分布的分布,并且它有六个异常值——一个在左侧,五个在右侧尾部:

图 5.4 – 变量均值平滑度的箱线图和直方图
-
创建一个函数,该函数返回均值加减
fold倍标准差,其中fold是函数的参数: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 -
使用该函数来识别
meansmoothness变量的极端限制:lower_limit, upper_limit = find_limits( X, "mean smoothness", 3)如果我们现在执行
lower_limit或upper_limit,我们会看到值0.0541和0.13855,这对应于我们可以考虑值为异常值的限制之外的范围。
注意
如果变量是正态分布的,均值加减三倍标准差之间的区间包含了 99.87% 的观测值。对于不那么保守的限制,我们可以将标准差乘以 2 或 2.5,这将产生包含 95.4% 和 97.6% 观测值的区间。
-
创建一个布尔向量,标记超出在 步骤 6 中确定的限制的观测值:
outliers = np.where( (X[«mean smoothness»] > upper_limit) | (X[«mean smoothness»] < lower_limit), True, False )如果我们现在执行
outliers.sum(),我们会看到值5,这表明有五个异常值或观测值比使用均值和标准差找到的极端值小或大。根据这些限制,我们将比 IQR 规则少识别一个异常值。 -
让我们在 步骤 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() -
现在让我们制作这些图表:
plot_boxplot_and_hist(X, "mean smoothness")在下面的图中,我们看到箱线图中 IQR 接近规则观察到的限制比均值和标准差识别的限制更保守。因此,我们在箱线图中观察到六个潜在的异常值,但根据均值和标准差计算只有五个:

图 5.5 – 比较箱线图中触须的极限与使用平均值和标准差确定的极限(直方图中的垂直线)
由平均值和标准差推导出的边界是对称的。它们从分布的中心向两侧等距离延伸。如前所述,这些边界仅适用于正态分布的变量。
它是如何工作的…
使用 pandas 的mean()和std(),我们捕捉了变量的平均值和标准差。我们将极限确定为平均值加减三倍的标准差。为了突出异常值,我们使用了 NumPy 的where()。where()函数扫描变量的行,如果值大于上限或小于下限,则被分配True,否则分配False。最后,我们使用 pandas 的sum()对这个布尔向量进行求和,以计算异常值的总数。
最后,我们比较了边界,以确定由 IQR 邻近规则返回的异常值,我们在之前的配方中讨论了该规则,即使用箱线图和四分位数邻近规则可视化异常值,以及平均值和标准差。我们观察到 IQR 规则的极限更为保守。这意味着使用 IQR 规则,我们会在这个特定变量中标记出更多的异常值。
使用中位数绝对偏差来寻找异常值
平均值和标准差受到异常值的影响很大。因此,使用这些参数来识别异常值可能会适得其反。一种更好的识别异常值的方法是使用 MAD。MAD 是每个观测值与变量中位数绝对偏差的中位数:

在前一个方程中,xi是变量X中的每个观测值。MAD 的美丽之处在于它使用中位数而不是平均值,这使得它对异常值具有鲁棒性。b常数用于从 MAD 估计标准差,如果我们假设正态性,那么b = 1.4826。
注意
如果假设变量具有不同的分布,则b的计算为 75 百分位数除以 1。在正态分布的情况下,1/75 百分位数 = 1.4826。
在计算 MAD 之后,我们使用中位数和 MAD 来建立分布极限,将超出这些极限的值指定为异常值。这些极限被设置为中位数加减 MAD 的倍数,通常在 2 到 3.5 之间。我们选择的乘数反映了我们希望有多严格(越高,越保守)。在这个菜谱中,我们将使用 MAD 来识别异常值。
如何做...
让我们从导入 Python 库和加载数据集开始这个菜谱:
-
让我们导入 Python 库和数据集:
import numpy as np import matplotlib.pyplot as plt import seaborn as sns from sklearn.datasets import load_breast_cancer -
从 scikit-learn 加载乳腺癌数据集:
X, y = load_breast_cancer( return_X_y=True, as_frame=True) -
创建一个函数,根据 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 -
让我们使用该函数来捕获
meansmoothness变量的极端极限:lower_limit, upper_limit = find_limits( X, "mean smoothness", 3)如果我们执行
lower_limit或upper_limit,我们将看到0.0536和0.13812的值,这对应于我们可以考虑值为异常值的极限。 -
让我们创建一个布尔向量,标记超出极限的观测值:
outliers = np.where( (X[«mean smoothness»] > upper_limit) | (X[«mean smoothness»] < lower_limit), True, False )如果我们现在执行
outliers.sum(),我们将看到5的值,这表明有五个异常值或观测值,这些值小于或大于使用 MAD 找到的极端值。 -
让我们编写一个函数,在变量的直方图旁边绘制箱线图,突出显示在直方图中计算的步骤 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() -
现在让我们制作这些图表:
plot_boxplot_and_hist(X, "mean smoothness")在下面的图中,我们可以看到,箱线图中 IQR 邻近规则观察到的极限比使用 MAD 确定的极限更为保守。MAD 返回对称边界,而箱线图生成非对称边界,这对于高度偏斜的分布更为合适:

图 5.6 – 箱线图中触须与使用 MAD 确定的极限之间的比较
注意
使用 MAD 检测异常值需要变量具有一定的变异性。如果一个变量中超过 50%的值是相同的,中位数将与最频繁的值一致,MAD=0。这意味着所有与中位数不同的值将被标记为异常值。这构成了使用 MAD 进行异常检测的另一个限制。
就这样!你现在知道如何使用中位数和 MAD 来识别异常值。
它是如何工作的…
我们使用 pandas 的median()确定了中位数,使用 pandas 的abs()确定了绝对差异。接下来,我们使用 NumPy 的where()函数创建一个布尔向量,如果值大于上限或小于下限,则为True,否则为False。最后,我们使用 pandas 的sum()在这个布尔向量上计算异常值的总数。
最后,我们将边界与 IQR 接近规则返回的异常值进行比较,我们在 使用箱线图和四分位数范围接近规则可视化异常值 烹饪法中讨论了这一点,以及使用 MAD 返回的异常值。IQR 规则返回的边界不太保守。通过将 IQR 乘以 3 而不是默认的 1.5(箱线图中的默认值)来改变这种行为。此外,我们注意到 MAD 返回对称边界,而箱线图提供了不对称的边界,这可能更适合不对称分布。
参见
要彻底讨论检测异常值的不同方法的优缺点,请查看以下资源:
-
Rousseeuw PJ, Croux C. 中位数绝对偏差的替代方案。美国统计学会杂志,1993.
www.jstor.org/stable/2291267。 -
Leys C, et. al. 检测异常值:不要使用围绕平均值的标准差,而要使用围绕中位数的绝对差分。实验社会心理学杂志,2013. http://dx.doi.org/10.1016/j.jesp.2013.03.013。
-
Thériault R, et. al. 检查你的异常值**!使用 easystats 在 R 中识别统计异常值入门。行为研究方法,2024.
doi.org/10.3758/s13428-024-02356-w。
移除异常值
近期研究区分了三种类型的异常值:错误异常值、有趣异常值和随机异常值。错误异常值可能是由人为或方法错误引起的,应该纠正或从数据分析中删除。在这个烹饪法中,我们假设异常值是错误(你不想删除有趣或随机异常值)并从数据集中删除它们。
如何做...
我们将使用 IQR 接近规则来查找异常值,然后使用 pandas 和 Feature-engine 从数据中移除它们。
-
让我们导入 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 -
从 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) -
让我们创建一个函数,使用 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
注意
在 第 3 步中,我们使用 IQR 接近规则找到将数据点视为异常值的极限,我们在 使用箱线图和四分位数接近规则可视化异常值 烹饪法中讨论了这一点。或者,你可以使用均值和标准差或 MAD 来识别异常值,正如我们在 使用均值和标准差查找异常值 和 使用中位数绝对偏差查找异常值 烹饪法中所涵盖的。
-
使用第 3 步中的函数,让我们确定
MedInc变量的范围:lower, upper = find_limits(X_train, "MedInc", 3)如果你执行
print(lower_limit, upper_limit),你会看到上一个命令的结果:(-3.925900000000002, 11.232600000000001)。 -
让我们保留训练集和测试集中值大于或等于(
ge)下限的观测值:inliers = X_train["MedInc"].ge(lower) train_t = X_train.loc[inliers] inliers = X_test["MedInc"].ge(lower) test_t = X_test.loc[inliers] -
让我们保留值低于或等于(
le)上限的观测值:inliers = X_train["MedInc"].le(upper) train_t = X_train.loc[inliers] inliers = X_test["MedInc"].le(upper) test_t = X_test.loc[inliers]继续执行
X_train.shape然后执行train_t.shape以证实转换后的 DataFrame 在移除异常值后比原始的一个包含更少的观测值。我们可以使用
feature-engine同时从多个变量中移除异常值。 -
设置一个转换器,使用 IQR 规则识别三个变量中的异常值:
trimmer = OutlierTrimmer( variables = [«MedInc", "HouseAge", "Population"], capping_method="iqr", tail="both", fold=1.5, )
注意
OutlierTrimmer 可以使用 IQR 来识别边界,正如我们在本食谱中所示,也可以通过使用平均值和标准差,或 MAD 来实现。您需要将 capping_method 分别更改为 gaussian 或 mad。
-
将转换器拟合到训练集,以便它学习这些限制:
trimmer.fit(X_train)
注意
通过执行 trimmer.left_tail_caps_,我们可以可视化三个变量的下限:{'MedInc': -0.6776500000000012, 'HouseAge': -10.5, 'Population': -626.0}。通过执行 trimmer.right_tail_caps_,我们可以看到变量的上限:{'MedInc': 7.984350000000001, 'HouseAge': 65.5, 'Population': 3134.0}。
-
最后,让我们从训练集和测试集中移除异常值:
X_train_t = trimmer.transform(X_train) X_test_t = trimmer.transform(X_test)为了完成本食谱,让我们比较在移除异常值前后变量的分布情况。
-
让我们创建一个函数来在直方图上方显示箱线图:
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 中的代码。
-
让我们绘制移除异常值前的
MedInc分布图:plot_boxplot_and_hist(X_train, "MedInc")在下面的图中,我们看到
MedInc是偏斜的,并且大于 8 的观测值被标记为异常值:

图 5.7– 移除异常值前的 MedInc 箱线图和直方图。
-
最后,让我们绘制移除异常值后的
MedInc分布图:plot_boxplot_and_hist(train_t, "MedInc")移除异常值后,
MedInc的偏斜度似乎减小了,其值分布得更均匀:

图 5.8 – 移除异常值后的 MedInc 箱线图和直方图
注意
使用 IQR 规则对转换变量进行操作会揭示新的异常值。这并不令人惊讶;移除分布两端的观测值会改变参数,如中位数和四分位数,这些参数反过来又决定了触须的长度,从而可能将更多的观测值识别为异常值。我们用来识别异常值的工具只是工具。为了明确地识别异常值,我们需要用额外的数据分析来支持这些工具。
如果考虑从数据集中移除错误异常值,请确保比较并报告有异常值和无异常值的结果,以了解它们对模型的影响程度。
它是如何工作的...
pandas 中的ge()和le()方法创建了布尔向量,用于识别超过或低于由 IQR 接近规则设定的阈值的观测值。我们使用这些向量与 pandas 的loc一起保留在 IQR 定义的区间内的观测值。
feature-engine库的OutlierTrimmer()自动化了为多个变量移除异常值的程序。OutlierTrimmer()可以根据均值和标准差、IQR 接近规则、MAD 或分位数来识别异常值。我们可以通过capping_method参数修改这种行为。
通过改变我们乘以 IQR、标准差或 MAD 的系数,可以使得识别异常值的方法更加或更加保守。通过OutlierTrimmer(),我们可以通过fold参数控制方法的强度。
将tails设置为"both"时,OutlierTrimmer()在变量的分布两端找到了并移除了异常值。要仅移除一端的异常值,我们可以将"left"或"right"传递给tails参数。
OutlierTrimmer()采用 scikit-learn 的fit()方法来学习参数,并使用transform()来修改数据集。通过fit(),转换器学习并存储了每个变量的限制。通过transform(),它从数据中移除了异常值,返回pandas数据框。
参考内容
这是我之前提到的研究,它将异常值分类为错误;它很有趣且随机:Leys C, et.al. 2019. 如何分类、检测和管理单变量和多变量异常,重点在于预注册。国际社会心理学评论。doi.org/10.5334/irsp.289.
将异常值恢复到可接受的范围内
移除错误异常值可能是一种有效的策略。然而,这种方法可能会降低统计功效,特别是在许多变量都有异常值的情况下,因为我们最终移除了数据集的大部分内容。处理错误异常值的另一种方法是将其恢复到可接受的范围内。在实践中,这意味着用 IQR 接近规则、均值和标准差或 MAD 识别的某些阈值替换异常值的值。在这个菜谱中,我们将使用pandas和feature-engine替换异常值。
如何做到这一点...
我们将使用均值和标准差来查找异常值,然后使用pandas和feature-engine替换它们的值:
-
让我们导入所需的 Python 库和函数:
from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from feature_engine.outliers import Winsorizer -
从 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) -
让我们创建一个函数来使用均值和标准差查找异常值:
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 来识别异常值,正如我们在使用箱线图和四分位数间距规则可视化异常值和使用中位数绝对偏差查找异常值配方中所述。
-
使用第 3 步中的函数,让我们确定
mean smoothness变量的极限,该变量大约遵循高斯分布:var = "worst smoothness" lower_limit, upper_limit = find_limits( X_train, var, 3) -
让我们复制原始数据集:
train_t = X_train.copy() test_t = X_test.copy() -
现在,在新 DataFrame 中将异常值替换为第 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 步中确定的值替换,执行
train_t["worst smoothness"].agg(["min", "max"])以获取新的最大值和最小值。它们应该与变量的最小值和最大值或第 4 步中返回的极限相一致。我们可以通过利用
feature-engine同时替换多个变量的异常值。 -
让我们设置一个转换器,用平均值和标准差确定的极限替换两个变量中的异常值:
capper = Winsorizer( variables=[«worst smoothness», «worst texture»], capping_method="gaussian", tail="both", fold=3, )
注意
Winsorizer可以使用平均值和标准差识别边界,正如我们在本配方中所示,以及 IQR 间距规则和 MAD。您需要将capping_method更改为iqr或mad。
-
让我们将转换器拟合到数据中,以便它学习这些极限:
capper.fit(X_train)通过执行
capper.left_tail_caps_,我们可以可视化两个变量的下限:{'worst smoothness': 0.06364743973736293, 'worst texture': 7.115307053129349}。通过执行capper.right_tail_caps_,我们可以看到变量的上限:{'worst smoothness': 0.20149734880520967, 'worst texture': 43.97692158753917}。 -
最后,让我们用第 8 步的极限值替换异常值:
X_train = capper.transform(X_train) X_test = capper.transform(X_test)如果我们现在执行
train_t[capper.variables_].agg(["min", "max"]),我们将看到转换后的 DataFrame 的最大值和最小值与变量的最大值和最小值或识别的极限相一致,以先到者为准:worst smoothness worst texture min 0.071170 12.020000 max 0.201411 43.953738如果您计划对变量进行上限处理,确保在替换异常值前后比较您模型的性能或分析结果。
它是如何工作的...
pandas 的 clip() 函数用于将值限制在指定的上下限。在这个菜谱中,我们使用均值和标准差找到了这些界限,然后剪切变量,使得所有观测值都位于这些界限内。worst smoothness 变量的最小值实际上大于我们在 步骤 4 中找到的下限,因此在其分布的左侧没有替换任何值。然而,有值大于 步骤 4 中的上限,这些值被替换为上限。这意味着转换变量的最小值与原始变量的最小值相同,但最大值不同。
我们使用 feature-engine 同时替换多个变量的异常值。Winsorizer() 可以根据均值和标准差、IQR 接近规则、MAD 或使用百分位数来识别异常值。我们可以通过 capping_method 参数修改这种行为。
通过改变我们乘以 IQR、标准差或 MAD 的因子,可以使得识别异常值的方法更加或更加保守。在 Winsorizer() 中,我们可以通过 fold 参数控制方法的强度。
当 tails 设置为 "both" 时,Winsorizer() 在变量的分布两端找到并替换了异常值。要替换任一端的异常值,我们可以将 "left" 或 "right" 传递给 tails 参数。
Winsorizer() 方法采用了 scikit-learn 的 fit() 方法来学习参数,以及 transform() 方法来修改数据集。通过 fit(),转换器学习并存储了每个变量的界限。通过 transform(),它替换了异常值,返回 pandas DataFrame。
参见
feature-engine 有 ArbitraryOutlierCapper(),可以在任意最小和最大值处限制变量:feature-engine.readthedocs.io/en/latest/api_doc/outliers/ArbitraryOutlierCapper.html。
应用 Winsorizing
Winsorizing 或 winsorization,包括用下一个最大(或最小)观测值的幅度替换极端、不太知名的观测值,即异常值。它与之前菜谱中描述的程序类似,将异常值拉回到可接受的范围内,但并不完全相同。Winsorization 涉及在分布两端替换相同数量的异常值,这使得 Winsorization 成为一个对称过程。这保证了 Winsorized mean,即替换异常值后的均值,仍然是变量中心趋势的稳健估计器。
实际上,为了在两端移除相似数量的观测值,我们会使用百分位数。例如,第 5 百分位数是低于 5%观测值的值,第 95 百分位数是高于 5%观测值的值。使用这些值作为替换可能会在两端替换相似数量的观测值,但这并不保证。如果数据集中包含重复值,获得可靠的百分位数具有挑战性,并且可能导致每个尾端值的不均匀替换。如果发生这种情况,则 winsorized 平均值不是中心趋势的良好估计量。在本配方中,我们将应用 winsorization。
如何操作...
我们将把乳腺癌数据集的所有变量限制在其第 5 和第 95 百分位数:
-
让我们导入所需的 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 -
从 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, ) -
在字典中捕获每个变量的第 5 和第 95 百分位数:
q05 = X_train.quantile(0.05).to_dict() q95 = X_train.quantile(0.95).to_dict() -
现在我们将所有变量的值替换为相应的百分位数,超出这些百分位数:
train_t = X_train.clip(lower=q05, upper=q95) test_t = X_test.clip(lower=q05, upper=q95) -
让我们显示 winsorization 之前一个变量的最小值、最大值和平均值:
var = 'worst smoothness' X_train[var].agg(["min", "max", "mean"])我们可以在以下输出中看到值:
min 0.071170 max 0.222600 mean 0.132529 Name: worst smoothness, dtype: float64 -
显示 winsorization 后相同变量的最小值、最大值和平均值:
train_t[var].agg([„min", „max"])在以下输出中,我们可以看到最小值和最大值对应于百分位数。然而,平均值与变量的原始平均值相当相似:
min 0.096053 max 0.173215 mean 0.132063 Name: worst smoothness, dtype: float64
注意
如果您想将 winsorization 作为 scikit-learn 管道的一部分使用,可以使用feature-engine库的Winsorizer(),设置如下:
capper = Winsorizer(
capping_method="quantiles",
tail="both",
fold=0.05,
)
在此之后,按照将异常值拉回到可接受 范围内配方中描述的fit()和transform()方法进行操作。
值得注意的是,尽管使用了百分位数,但该程序并没有精确地替换分布两边的相同数量的观测值。如果您打算 winsorize 您的变量,请在 winsorization 前后比较您分析的结果。
它是如何工作的...
我们使用 pandas 的quantiles()获取数据集中所有变量的第 5 和第 95 百分位数,并将其与to_dict()结合,以保留这些百分位数在字典中,其中键是变量,值是百分位数。然后我们将这些字典传递给 pandas 的clip(),用百分位数替换小于或大于这些百分位数的值。通过使用字典,我们一次限制了多个变量。
参考以下内容
更多关于 winsorization 如何影响对称和不对称替换中的平均值和标准差详情,请查看原始文章:
Dixon W. 从截尾正态样本中简化的估计。数学统计年鉴,1960 年。www.jstor.org/stable/2237953
第六章:从日期和时间变量中提取特征
日期和时间变量包含有关日期、时间或两者的信息,在编程中,我们将其统称为 datetime 特征。出生日期、事件发生的时间以及最后付款的日期和时间都是 datetime 变量的例子。
由于其本质,datetime 特征通常具有高基数。这意味着它们包含大量唯一的值,每个值对应于特定的日期和/或时间组合。我们通常不会以原始格式使用 datetime 变量进行机器学习模型。相反,我们通过从这些变量中提取多个特征来丰富数据集。这些新特征通常具有较低的基数,并允许我们捕捉到有意义的 信息,如趋势、季节性和重要事件和趋势。
在本章中,我们将探讨如何通过利用 pandas 的 dt 模块来从日期和时间中提取特征,然后使用 feature-engine 自动化此过程。
本章将涵盖以下食谱:
-
使用
pandas从日期中提取特征 -
使用
pandas从时间中提取特征 -
捕获
datetime变量之间的经过时间 -
在不同时区使用时间
-
使用
feature-engine自动化datetime特征提取
技术要求
在本章中,我们将使用 pandas、numpy 和 feature-engine Python 库。
使用 pandas 从日期中提取特征
datetime 变量的值可以是日期、时间或两者兼有。我们将首先关注包含日期的变量。我们很少使用原始日期与机器学习算法结合。相反,我们提取更简单的特征,如年份、月份或星期几,这些特征使我们能够捕捉到季节性、周期性和趋势等洞察。
pandas Python 库非常适合处理日期和时间。通过使用 pandas 的 dt 模块,我们可以访问 pandas Series 的 datetime 属性以提取许多特征。然而,为了利用此功能,变量需要转换为支持这些操作的数据类型,例如 datetime 或 timedelta。
注意
当我们从 CSV 文件加载数据时,datetime 变量可以转换为对象。为了提取本章中将要讨论的日期和时间特征,有必要将变量重新转换为 datetime。
在这个食谱中,我们将学习如何通过使用 pandas 来提取日期特征。
准备工作
以下是我们可以使用 pandas 直接从 datetime 变量的 date 部分提取的一些特征:
-
pandas.Series.dt.year -
pandas.Series.dt.quarter -
pandas.Series.dt.month -
pandas.Series.dt.isocalendar().week -
pandas.Series.dt.day -
pandas.Series.dt.day_of_week -
pandas.Series.dt.weekday -
pandas.Series.dt.dayofyear -
pandas.Series.dt.day_of_year
我们可以使用 pandas 获得的特征来创建更多的特征,例如学期或是否是周末。我们将在下一节中学习如何做到这一点。
如何做到这一点...
为了继续这个食谱,让我们导入 pandas 和 numpy,并创建一个样本 DataFrame:
-
让我们导入库:
import numpy as np import pandas as pd -
我们将首先创建从
2024-05-17中午开始,然后每天增加 1 天的 20 个datetime值。然后,我们将这些值捕获在一个DataFrame实例中,并显示前五行:rng_ = pd.date_range( "2024-05-17", periods=20, freq="D") data = pd.DataFrame({"date": rng_}) data.head()在下面的输出中,我们可以看到我们在 步骤 2 中创建的包含日期的变量:

图 6.1 – 仅包含日期的 datetime 变量的 DataFrame 的顶部行
注意
我们可以通过执行 data["date"].dtypes 来检查变量的数据格式。如果变量被转换为对象类型,我们可以通过执行 data["date_dt"] = pd.to_datetime(data["date"]) 来将其转换为 datetime 格式。
-
让我们将日期的年份部分提取到一个新列中,并显示结果 DataFrame 的前五行:
data["year"] = data["date"].dt.year data.head()在下面的输出中,我们可以看到新的
year变量:

图 6.2 – 从日期中提取的年份变量的 DataFrame 的前五行
-
让我们将日期的季度提取到一个新列中,并显示前五行:
data["quarter"] = data["date"].dt.quarter data[["date", "quarter"]].head()在下面的输出中,我们可以看到新的
quarter变量:

图 6.3 – 从日期中提取的季度变量的 DataFrame 的前五行
-
使用
quarter,我们现在可以创建semester特征:data["semester"] = np.where(data["quarter"] < 3, 1, 2)
注意
您可以使用 pandas 的 unique() 方法来探索新变量的不同值,例如,通过执行 df["quarter"].unique() 或 df["semester"].unique()。
-
让我们将日期的
month部分提取到一个新列中,并显示 DataFrame 的前五行:data["month"] = data["date"].dt.month data[["date", "month"]].head()在下面的输出中,我们可以看到新的
month变量:

图 6.4 – 包含新月份变量的 DataFrame 的前五行
-
让我们将日期的周数(一年有 52 周)提取出来:
data["week"] = data["date"].dt.isocalendar().week data[["date", "week"]].head()在下面的输出中,我们可以看到
week变量:

图 6.5 – 包含新周变量的 DataFrame 的前五行
-
让我们将月份的日期提取出来,它可以取
1到31之间的值,作为一个新列:data["day_mo"] = data["date"].dt.day data[["date", "day_mo"]].head()在下面的输出中,我们可以看到
day_mo变量:

图 6.6 – DataFrame 顶部行,包含表示月份天数的变量
-
让我们提取星期几,其值在
0到6之间(从星期一到星期日),在新的列中,然后显示顶部行:data["day_week"] = data["date"].dt.dayofweek data[["date", "day_mo", "day_week"]].head()我们在以下输出中看到
day_week变量:

图 6.7 – DataFrame 顶部行,包含表示一周天数的变量
-
使用步骤 9中的变量,我们可以创建一个二元变量,表示是否为周末:
data["is_weekend"] = ( data[«date»].dt.dayofweek > 4).astype(int) data[["date", "day_week", "is_weekend"]].head()我们在以下输出中看到新的
is_weekend变量:

图 6.8 – 包含新变量 is_weekend 的 DataFrame 的前五行
注意
我们可以通过使用feature-engine来自动提取所有这些特征。查看本章中的使用 feature-engine 自动化日期时间特征提取配方以获取更多详细信息。
通过这样,我们已经使用pandas从datetime变量的日期部分提取了许多新特征。这些特征对数据分析、可视化和预测建模很有用。
它是如何工作的...
在本配方中,我们通过使用pandas的dt模块从datetime变量中提取了许多与日期相关的特征。首先,我们创建了一个包含日期的变量的样本 DataFrame。我们使用pandas的date_range()从任意日期开始创建一个值范围,并按1天的时间间隔增加。通过periods参数,我们指明了要创建的值范围的数量——即日期的数量。通过freq参数,我们指明了日期之间的步长大小。在我们的例子中,我们使用了D代表天数。最后,我们使用pandas的DataFrame()将日期范围转换为一个 DataFrame。
为了提取日期的不同部分,我们使用了pandas的dt来访问pandas Series 的datetime属性,然后利用不同的属性。我们使用year、month和quarter将年份、月份和季度捕获到 DataFrame 的新列中。为了找到学期,我们使用 NumPy 的where()结合新创建的quarter变量创建了一个布尔值。NumPy 的where()扫描quarter变量的值;如果它们小于3,则返回第一个学期的1值;否则,返回第二个学期的2值。
为了提取日期和周的不同表示形式,我们使用了isocalender().week、day和dayofweek属性。利用周几,我们进一步创建了一个二元变量来表示是否为周末。我们使用where()函数扫描周几,如果值大于4,即周六和周日,函数返回True,否则返回False。最后,我们将这个布尔向量转换为整数,以得到一个由 1 和 0 组成的二元变量。有了这个,我们就从日期中创建了多个特征,这些特征可以用于数据分析与预测建模。
还有更多...
使用pandas的dt模块,我们可以直接从日期中提取更多特征。例如,我们可以提取月份、季度或年份的开始和结束,是否为闰年,以及一个月中的天数。这些函数允许你做到这一点:
-
pandas.Series.dt.is_month_start -
pandas.Series.dt.is_month_end -
pandas.Series.dt.is_quarter_start -
pandas.Series.dt.is_quarter_end -
pandas.Series.dt.is_year_start -
pandas.Series.dt.is_year_end -
pandas.Series.dt.is_leap_year -
pandas.Series.dt.days_in_month
我们也可以使用pd.dt.days_in_month返回特定月份的天数,以及一年中的某一天(从1到365)使用pd.dt.dayofyear。
想要了解更多详情,请访问pandas的datetime文档:pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#time-date-components.
参考以下内容
想要了解如何使用pandas的date_ranges()创建不同的datetime范围,请访问pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases.
想要了解更多关于pandas的dt的信息,请访问pandas.pydata.org/pandas-docs/stable/reference/series.html#datetime-properties.
使用 pandas 从时间中提取特征
一些事件在一天中的特定时间发生得更频繁——例如,欺诈活动更有可能在夜间或清晨发生。空气污染物浓度也随着一天中的时间变化,在交通高峰时段达到峰值,此时街道上有更多车辆。因此,从时间中提取特征对于数据分析和预测建模非常有用。在本例中,我们将通过使用pandas和 NumPy 来提取datetime变量的不同时间部分。
准备工作
我们可以使用以下pandas的datetime属性提取小时、分钟和秒:
-
pandas.Series.dt.hour -
pandas.Series.dt.minute -
pandas.Series.dt.second
如何做到这一点...
在本例中,我们将提取time变量的hour、minute和second部分。让我们首先导入库并创建一个样本数据集:
-
让我们导入
pandas和numpy:import numpy as np import pandas as pd -
让我们从创建 20 个
datetime观测值开始,从2024-05-17午夜开始,然后以 1 小时、15 分钟和 10 秒的增量增加。接下来,我们将时间范围捕获到 DataFrame 中,并显示前五行:rng_ = pd.date_range( "2024-05-17", periods=20, freq="1h15min10s") df = pd.DataFrame({"date": rng_}) df.head()在以下输出中,我们看到步骤 2中的变量,包含日期部分和时间部分,值以 1 小时、15 分钟和 10 秒的间隔增加:

图 6.9 – 包含日期时间变量的样本 DataFrame 的前五行
-
让我们提取
hour、minute和second部分,并将它们捕获到三个新列中,然后显示 DataFrame 的前五行:df["hour"] = df["date"].dt.hour df["min"] = df["date"].dt.minute df["sec"] = df["date"].dt.second df.head()在以下输出中,我们看到我们在步骤 3中提取的三个
time特征:

图 6.10 – 从时间派生出的 DataFrame 的前五行
注意
记住,pandas的dt需要一个datetime对象来工作。您可以使用pandas的to_datetime()函数将对象变量的数据类型更改为datetime。
-
让我们执行与步骤 3中相同的操作,但现在是在一行代码中:
df[["h", "m", "s"]] = pd.DataFrame( [(x.hour, x.minute, x.second) for x in df["date"]] ) df.head()在以下输出中,我们看到新创建的变量:

图 6.11 – 从时间派生出的 DataFrame 的前五行
注意
您可以使用pandas的unique()检查新变量的唯一值,例如,通过执行df['hour'].unique()。
-
最后,让我们创建一个二进制变量,标记早上 6 点至中午 12 点之间发生的事件:
df["is_morning"] = np.where( (df[«hour»] < 12) & (df[«hour»] > 6), 1, 0 ) df.head()我们在以下输出中看到
is_morning变量:

图 6.12 – 从时间派生出的新变量的 DataFrame 的前几行
因此,我们从datetime变量的时间部分提取了多个特征。这些特征可用于数据分析预测建模。
它是如何工作的...
在本配方中,我们创建了捕获时间表示的特征。首先,我们创建了一个包含datetime变量的样本 DataFrame。我们使用pandas的date_range()函数创建了一个从任意日期开始,以 1 小时、15 分钟和 10 秒为间隔的 20 个值的范围。我们使用1h15min10s字符串作为freq参数的频率项,以指示所需的增量。接下来,我们使用pandas的DataFrame()将日期范围转换为 DataFrame。
为了提取不同时间部分,我们使用了 pandas 的 dt 来访问 hour、minute 和 second 时间属性。从 time 中提取 hour 后,我们使用它通过 NumPy 的 where() 创建一个新特征,以表示是否是上午。NumPy 的 where() 检查 hour 变量;如果其值小于 12 且大于 6,则分配值为 1;否则,分配值为 0。通过这些操作,我们在 DataFrame 中添加了几个可用于数据分析和训练机器学习模型的特征。
更多...
我们还可以使用以下 pandas 属性提取微秒和纳秒:
-
pandas.Series.dt.microsecond -
pandas.Series.dt.nanosecond
更多详情,请访问 pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#time-date-components。
捕捉 datetime 变量之间的经过时间
我们可以像在前两个菜谱中做的那样,单独从每个 datetime 变量中提取强大的特征。我们可以通过组合多个 datetime 变量来创建额外的特征。一个常见的例子是通过比较 出生日期 和 事件日期 来提取事件发生时的 年龄。
在本菜谱中,我们将学习如何通过利用 pandas 和 feature-engine 来捕捉两个 datetime 变量之间的时间。
如何操作...
为了继续本菜谱,我们将创建一个包含两个 datatime 变量的 DataFrame:
-
让我们从导入
pandas、numpy和datetime开始:import datetime import numpy as np import pandas as pd -
我们将首先创建两个具有 20 个值的
datetime变量;第一个变量的值从2024-05-17开始,以1小时的间隔增加,第二个变量以1个月的间隔增加。然后,我们将变量捕捉到 DataFrame 中,添加列名,并显示前几行:date = "2024-05-17" rng_hr = pd.date_range(date, periods=20, freq="h") rng_month = pd.date_range(date, periods=20, freq="ME") df = pd.DataFrame( {"date1": rng_hr, "date2": rng_month}) df.head()我们在以下输出中看到 步骤 2 中的 DataFrame 的前五行:

图 6.13 – 包含两个 datetime 变量的 DataFrame 的前五行
-
让我们在新特征中捕捉两个变量之间的天数差异,然后显示 DataFrame 的前几行:
df["elapsed_days"] = ( df["date2"] - df["date1"]).dt.days df.head()我们在以下输出中看到天数差异:

图 6.14 – 包含捕捉两个 datetime 特征时间差的新的变量的 DataFrame 的前几行
-
让我们捕捉两个
datetime变量之间的周数差异,然后显示 DataFrame 的前几行:df["weeks_passed"] = ( (df[«date2»] - df[«date1»]) / np.timedelta64(1, "W")) df.head()我们在以下屏幕截图中看到变量之间的周数差异:

图 6.15 – 一个 DataFrame,其中包含两个日期时间变量之间的时间差,以天数和周数表示
-
现在,让我们计算变量之间的时间差(以分钟和秒为单位),然后显示 DataFrame 的前几行:
df["diff_seconds"] = ( df[«date2»] - df[«date1»])/np.timedelta64(1, «s») df["diff_minutes"] = ( df[«date2»] - df[«date1»])/ np.timedelta64(1,»m») df.head()我们在以下输出中看到新变量:

图 6.16 – 一个 DataFrame,其中包含两个日期时间变量之间的时间差,以不同的时间单位表示
-
最后,让我们计算一个变量与当前日期之间的差异,以天数表示,然后显示 DataFrame 的前五行:
df["to_today"] = ( datetime.datetime.today() - df["date1"]) df.head()我们可以在以下输出中的 DataFrame 的最后一列找到新变量:

图 6.17 – 一个 DataFrame,其中包含包含 date1 与执行此代码的日期之间差异的新变量
注意
您计算机上的to_today变量将与此书中的不同,这是由于当前日期(写作时)与您执行代码时的差异。
那就是全部!我们现在已经通过比较两个datetime变量来创建新特征,丰富了我们的数据集。
它是如何工作的...
在这个菜谱中,我们捕捉了两个datetime变量之间时间差异的不同表示。为了继续这个菜谱,我们创建了一个包含两个变量的样本 DataFrame,每个变量从任意日期开始,有 20 个日期。第一个变量以1小时的间隔增加,而第二个变量以1个月的间隔增加。我们使用pandas的date_range()创建了这些变量,我们在本章前两个菜谱中讨论了它。
要确定变量之间的差异——即确定它们之间的时间——我们直接从一个datetime变量减去另一个——即从一个pandas Series 减去另一个。两个pandas Series 之间的差异返回了一个新的pandas Series。为了捕获天数差异,我们使用了pandas的dt,然后是days。要将时间差转换为月份,我们使用了 NumPy 的timedelta(),表示我们想要以周为单位传递W到方法的第二个参数。为了捕获秒和分钟的差异,我们分别传递了s和m字符串到timedelta()。
注意
NumPy 的timedelta的参数是一个数字,例如在我们的例子中是-1,表示单位数,以及一个datetime单位,如天(D)、周(W)、小时(h)、分钟(m)或秒(s)。
最后,我们捕捉了一个datetime变量与今天日期之间的差异。我们通过使用内置的datetime Python 库中的today()获得了今天(写作时)的日期和时间。
还有更多...
我们可以通过使用 feature-engine 的转换器 DatetimeSubstraction() 自动化创建捕获变量之间时间的特征。
-
让我们导入
pandas和feature-engine的转换器:import pandas as pd from feature_engine.datetime import ( DatetimeSubtraction ) -
让我们重新创建我们在 如何做 it… 部分的 步骤 2 中描述的示例数据集:
date = "2024-05-17" rng_hr = pd.date_range(date, periods=20, freq="h") rng_month = pd.date_range(date, periods=20, freq="ME") df = pd.DataFrame( {"date1": rng_hr, "date2": rng_month}) -
让我们设置
DatetimeSubstraction()以返回第二个日期和第一个日期之间以天为单位的时间差:ds = DatetimeSubtraction( variables="date2", reference="date1", output_unit="D", )
注意
我们可以通过在 variables 和 reference 参数中传递变量列表来获取两个以上变量的差异。
-
让我们创建并显示新的特征:
dft = ds.fit_transform(df) dft.head()在以下输出中,我们看到捕获两个
datetime变量之间时间差的变量:

图 6.18 – 包含两个日期时间变量之间差异的新变量的 DataFrame
更多详情,请查看 feature-engine.trainindata.com/en/latest/api_doc/datetime/DatetimeSubtraction.html。
参见
要了解更多关于 NumPy 的 timedelta,请访问 numpy.org/devdocs/reference/arrays.datetime.html#datetime-and-timedelta-arithmetic。
在不同时区处理时间
一些组织在国际上运营;因此,他们收集关于事件的信息可能记录在事件发生地区的时区旁边。为了能够比较发生在不同时区的事件,我们通常必须将所有变量设置在同一个时区。在本教程中,我们将学习如何统一 datetime 变量的时区,以及如何使用 pandas 将变量重新分配到不同的时区。
如何做...
要继续本教程,我们将创建一个包含两个不同时区变量的样本 DataFrame:
-
让我们导入
pandas:import pandas as pd -
让我们创建一个包含不同时区值的单个变量的 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) -
让我们在 DataFrame 中添加另一个
datetime变量,它也包含不同时区的值:df['time2'] = pd.concat([ pd.Series( pd.date_range( start='2024-07-01 09:00', freq='h', periods=3, tz='Europe/Berlin')), pd.Series( pd.date_range( start='2024-08-01 09:00', freq='h', periods=3, tz='US/Central')) ], axis=0)如果我们现在执行
df,我们将看到具有不同时区变量的 DataFrame,如下面的输出所示:

图 6.19 – 包含两个不同时区日期时间变量的 DataFrame
注意
时区用 +02 和 -05 的值表示,分别表示与协调世界时(UTC)的时间差。
-
要处理不同的时区,我们通常将变量设置在同一个时区,在这种情况下,我们选择了 UTC:
df['time1_utc'] = pd.to_datetime( df['time1'], utc=True) df['time2_utc'] = pd.to_datetime( df['time2'], utc=True)
如果我们现在执行df,我们将看到新的变量,它们与 UTC 相比有00小时的差异:

图 6.20 – 包含 UTC 中新的变量的 DataFrame
-
让我们计算变量之间的天数差异,然后显示 DataFrame 的前五行:
df['elapsed_days'] = ( df[‹time2_utc›] - df[‹time1_utc›]). dt.days df['elapsed_days'].head()在以下输出中,我们可以看到变量之间的时间差异:
0 21 1 21 2 21 0 -40 1 -40 datetime variables to the London and Berlin time zones, and then display the resulting variables:df['time1_london'] = df[
‹time1_utc›].dt.tz_convert('Europe/London')
df['time2_berlin'] = df[
‹time1_utc›].dt.tz_convert('Europe/Berlin')
df[['time1_london', 'time2_berlin']]
We see the variables in their respective time zones in the following output:

图 6.21 – 将变量重新格式化为不同的时区
在更改时区时,不仅时区的值会改变——即,如图像中的+01和+02值——而且小时的值也会改变。
它是如何工作的...
在这个示例中,我们更改了时区,并在不同时区的变量之间执行操作。首先,我们创建了一个包含两个变量的 DataFrame,这些变量的值从一个任意日期开始,每小时增加;这些变量设置在不同的时区。为了将不同的时区变量合并到一个 DataFrame 列中,我们利用pandas的concat()函数连接了pandas的date_range()返回的序列。我们将axis参数设置为0,表示我们想要将序列垂直连接到一个列中。我们已经在本章前面的示例中广泛介绍了pandas的date_range()的参数;有关更多详细信息,请参阅使用 pandas 从日期中提取特征和使用 pandas 从时间中提取特征的示例。
为了将变量的时区重置为中央时区,我们使用了pandas的to_datetime(),传递utc=True。最后,我们通过从一个序列减去另一个序列并捕获天数差异来确定变量之间的时间差异。为了重新分配不同的时区,我们使用了pandas的tz_convert(),将新的时区作为参数指定。
相关内容
要了解更多关于pandas的to_datetime()的信息,请访问pandas.pydata.org/pandas-docs/stable/reference/api/pandas.to_datetime.html。
要了解更多关于pandas的tz_convert()的信息,请访问pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.dt.tz_convert.html。
使用 Feature-engine 自动化日期时间特征提取
feature-engine是一个适用于与pandas DataFrame 一起工作的特征工程和选择的 Python 库。DatetimeFeatures()类可以通过使用pandas的dt自动从日期和时间中提取特征。DatetimeFeatures()允许您提取以下特征:
-
月份
-
季度
-
学期
-
年份
-
周
-
周几
-
月份中的日
-
年份中的日
-
周末
-
月份开始
-
月份结束
-
季度开始
-
季度结束
-
年度开始
-
年度结束
-
闰年
-
一个月中的天数
-
小时
-
分钟
-
秒
在这个菜谱中,我们将通过利用 feature-engine 自动从日期和时间创建特征。
如何操作...
为了展示 feature-engine 的功能,我们将创建一个包含 datetime 变量的样本 DataFrame:
-
让我们先导入
pandas和DatetimeFeatures():import pandas as pd from feature_engine.datetime import DatetimeFeatures -
让我们创建一个包含 20 个值的
datetime变量,从2024-05-17凌晨开始,然后以1天的增量递增。然后,我们将这个变量存储在一个 DataFrame 中:rng_ = pd.date_range( '2024-05-17', periods=20, freq='D') data = pd.DataFrame({'date': rng_}) -
我们将首先设置转换器以提取所有支持的
datetime特征:dtfs = DatetimeFeatures( variables=None, features_to_extract= "all", )
备注
DatetimeFeatures() 自动查找 datetime 类型的变量,或者当 variables 参数设置为 None 时可以解析为 datetime 的变量。或者,您可以传递一个包含您想要提取 date 和 time 特征的变量名称的列表。
-
让我们添加
date和time特征到数据中:dft = dtfs.fit_transform(data)
备注
默认情况下,DatetimeFeatures() 从每个 datetime 变量中提取以下特征:month,year,day_of_week,day_of_month,hour,minute,和 second。我们可以通过 features_to_extract 参数修改此行为,就像我们在 步骤 3 中所做的那样。
-
让我们将新变量的名称记录在列表中:
vars_ = [v for v in dft.columns if "date" in v]
备注
DatetimeFeatures() 使用原始变量名称(在这种情况下为 date)后跟一个下划线和创建的特征类型来命名新变量,例如,date_day_of_week 包含从 date 变量中提取的星期几。
如果我们执行 vars_,我们将看到创建的特征名称:
['date_month',
'date_quarter',
'date_semester',
'date_year',
'date_week',
'date_day_of_week',
'date_day_of_month',
'date_day_of_year',
'date_weekend',
'date_month_start',
'date_month_end',
'date_quarter_start',
'date_quarter_end',
'date_year_start',
'date_year_end',
'date_leap_year',
'date_days_in_month',
'date_hour',
'date_minute',
dft[vars_].head(). We can’t show the resulting DataFrame in the book because it is too big.
Note
We can create specific features by passing their names to the `features_to_extract` parameter.
For example, to extract `week` and `year`, we set the transformer like this: `dtfs = DatetimeFeatures(features_to_extract=["week", "year"])`. We can also extract all supported features by setting the `features_to_extract` parameter to `"all"`.
`DatetimeFeatures()` can also create features from variables in different time zones. Let’s learn how to correctly set up the transformer in this situation.
1. Let’s create a sample DataFrame with a variable’s values in different time zones:
```
df = pd.DataFrame()
df["time"] = pd.concat(
[
pd.Series(
pd.date_range(
start="2024-08-01 09:00",
freq="h",
periods=3,
tz="Europe/Berlin"
)
),
pd.Series(
pd.date_range(
start="2024-08-01 09:00",
freq="h",
periods=3, tz="US/Central"
)
),
],
axis=0,
)
```py
If we execute `df`, we will see the DataFrame from *Step 6*, as shown in the following output:

Figure 6.22 – A DataFrame with a variable’s values in different time zones
1. We’ll set the transformer to extract three specific features from this variable after setting it to the UTC:
```
dfts = DatetimeFeatures(
features_to_extract=
["day_of_week", "hour","minute"],
drop_original=False,
utc=True,
)
```py
2. Let’s create the new features:
```
dft = dfts.fit_transform(df)
```py
`DatetimeFeatures()` will set all variables into UTC before deriving the features. With `dft.head()`, we can see the resulting DataFrame:

Figure 6.23 – A DataFrame with the original and new variables
With that, we’ve created multiple date and time-related features in a few lines of code. `feature-engine` offers a great alternative to manually creating features per feature with `pandas`. In addition, `DatetimeFeatures()` can be integrated into scikit-learn’s `Pipeline` and `GridSearchCV`, among other classes.
How it works...
`DatetimeFeatures()` extracts several date and time features from `datetime` variables automatically by utilizing `pandas`’ `dt` under the hood. It works with variables whose original data types are `datetime`, as well as with object-like and categorical variables, provided that they can be parsed into a `datetime` format.
`DatetimeFeatures()` extracts the following features by default: `month`, `year`, `day_of_week`, `day_of_month`, `hour`, `minute` and `second`. We can make the transformer return all the features it supports by setting the parameter `features_to_extract` to `all`. In addition, we can extract a specific subset of features by passing the feature names in a list, as we did in *Step 7*.
`DatetimeFeatures()` automatically finds `datetime` variables or variables that can be parsed as `datetime` in the DataFrame passed to the `fit()` method. To extract features from a selected variable or group of variables, we can pass their name in a list to the `variables` parameter when we set up the transformer.
With `fit()`, `DatetimeFeatures()` does not learn any parameters; instead, it checks that the variables entered by the user are, or can be, parsed into a `datetime` format. If the user does not indicate variable names, `DatetimeFeatures()` finds the `datetime` variables automatically. With `transform()`, the transformer adds the date and time-derived variables to the DataFrame.
第七章:执行特征缩放
许多机器学习算法对变量尺度很敏感。例如,线性模型的系数取决于特征的尺度——也就是说,改变特征尺度将改变系数的值。在线性模型以及依赖于距离计算的算法(如聚类和主成分分析)中,值范围较大的特征往往会支配值范围较小的特征。因此,将特征放在相似的尺度上允许我们比较特征的重要性,并可能帮助算法更快收敛,从而提高性能和训练时间。
通常,缩放技术将变量除以某个常数;因此,重要的是要强调,当我们重新缩放变量时,变量分布的形状不会改变。如果你想改变分布形状,请查看第三章,转换 数值变量。
在本章中,我们将描述不同的方法来设置特征在相似的尺度上。
本章将涵盖以下食谱:
-
标准化特征
-
缩放到最大值和最小值
-
使用中位数和分位数进行缩放
-
执行均值归一化
-
实现最大绝对缩放
-
缩放到向量单位长度
技术要求
本章中我们使用的库主要有用于缩放的 scikit-learn(sklearn),用于处理数据的pandas,以及用于绘图的matplotlib。
标准化特征
标准化是将变量中心在0并标准化方差为1的过程。为了标准化特征,我们从每个观测值中减去均值,然后将结果除以标准差:

前一个转换的结果被称为z 分数,表示给定观测值与平均值相差多少个标准差。
当模型需要变量以零为中心且数据不是稀疏的(稀疏数据的中心化会破坏其稀疏性)时,标准化通常很有用。然而,标准化对异常值敏感,并且如果变量高度偏斜,z 分数不会保持对称属性,正如我们在下一节中讨论的。
准备工作
使用标准化,变量分布不会改变;改变的是它们值的幅度,正如我们在以下图中看到的:

图 7.1 – 标准化前后正态分布和偏态变量的分布。
z 分数(底部面板的x轴)表示一个观测值与均值偏离多少个标准差。当 z 分数为1时,观测值位于均值右侧 1 个标准差处,而当 z 分数为-1时,样本位于均值左侧 1 个标准差处。
在正态分布的变量中,我们可以估计一个值大于或小于给定 z 分数的概率,并且这种概率分布是对称的。观测值小于 z 分数-1的概率等同于值大于1的概率(底部左面板中的水平线)。这种对称性是许多统计测试的基础。在偏态分布中,这种对称性不成立。如图 7.1 底部右面板所示(水平线),值小于-1的概率与大于1的概率不同。
注意
均值和标准差对异常值敏感;因此,在使用标准化时,特征可能彼此之间有不同的缩放比例。
在实践中,我们经常在忽略分布形状的情况下应用标准化。然而,请记住,如果您使用的模型或测试对数据的分布有假设,您可能需要在标准化之前转换变量,或者尝试不同的缩放方法。
如何实现...
在本食谱中,我们将对加利福尼亚住房数据集的变量应用标准化:
-
让我们先导入所需的 Python 包、类和函数:
import pandas as pd from sklearn.datasets import fetch_california_housing from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler -
让我们将加利福尼亚住房数据集从 scikit-learn 加载到 DataFrame 中,并删除
Latitude和Longitude变量:X, y = fetch_california_housing( return_X_y=True, as_frame=True) X.drop(labels=["Latitude", "Longitude"], axis=1, inplace=True) -
现在,让我们将数据分为训练集和测试集:
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=0) -
接下来,我们将设置 scikit-learn 中的
StandardScaler()函数并将其拟合到训练集中,以便它学习每个变量的均值和标准差:scaler = StandardScaler().set_output( transform="pandas") scaler.fit(X_train)
注意
Scikit-learn 缩放器,就像任何 scikit-learn 转换器一样,默认返回 NumPy 数组。要返回pandas或polars DataFrame,我们需要使用set_output()方法指定输出容器,就像我们在步骤 4中所做的那样。
-
现在,让我们使用训练好的 scaler 对训练集和测试集进行标准化:
X_train_scaled = scaler.transform(X_train) X_test_scaled = scaler.transform(X_test)StandardScaler()在fit()过程中存储从训练集中学习到的均值和标准差。让我们可视化学习到的参数。 -
首先,我们将打印出由
scaler学习到的平均值:scaler.mean_在以下输出中,我们可以看到每个变量的平均值:
scaler:scaler.scale_
array([1.89109236e+00, 1.25962585e+01, 2.28754018e+00, 4.52736275e-01, 1.14954037e+03, 6.86792905e+00])
Let’s compare the transformed data with the original data to understand the changes. -
让我们打印出测试集中原始变量的描述性统计信息:
X_test.describe()在以下输出中,我们可以看到变量的平均值与零不同,方差各不相同:

图 7.2 – 缩放前的变量的描述性统计参数
-
现在我们来打印转换变量的描述性统计值:
X_test_scaled.describe()在以下输出中,我们可以看到变量的均值现在集中在
0,方差约为1:

图 7.3 – 缩放变量的描述性统计参数,显示均值为 0 和方差约为 1
注意
AveRooms、AveBedrms 和 AveOccup 变量高度偏斜,这可能导致测试集中的观察值远大于或远小于训练集中的值,因此我们看到方差偏离 1。这是可以预料的,因为标准化对异常值和非常偏斜的分布很敏感。
在 准备就绪 部分中,我们提到分布的形状不会随着标准化而改变。通过执行 X_test.hist() 然后执行 X_test_scaled.hist() 来验证这一点,并比较转换前后的变量分布。
它是如何工作的...
在这个示例中,我们通过使用 scikit-learn 对加利福尼亚住房数据集的变量进行了标准化。我们将数据分为训练集和测试集,因为标准化的参数应该从训练集中学习。这是为了避免在预处理步骤中将测试集的数据泄露到训练集中,并确保测试集对所有特征转换过程保持无知的。
为了标准化这些特征,我们使用了 scikit-learn 的 StandardScaler() 函数,该函数能够学习并存储在转换中使用的参数。使用 fit(),缩放器学习每个变量的均值和标准差,并将它们存储在其 mean_ 和 scale_ 属性中。使用 transform(),缩放器对训练集和测试集中的变量进行了标准化。StandardScaler() 的默认输出是 NumPy 数组,但通过 set_output() 参数,我们可以将输出容器更改为 pandas DataFrame,就像我们在 步骤 4 中所做的那样,或者通过设置 transform="polars" 来更改为 polars。
注意
StandardScaler() 默认会减去均值并除以标准差。如果我们只想对分布进行中心化而不进行标准化,我们可以在初始化转换器时设置 with_std=False。如果我们想在 步骤 4 中将方差设置为 1,而不对分布进行中心化,我们可以通过设置 with_mean=False 来实现。
缩放到最大值和最小值
将变量缩放到最小值和最大值会压缩变量的值在0到1之间。要实现这种缩放方法,我们从所有观测值中减去最小值,然后将结果除以值范围——即最大值和最小值之间的差值:

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

图 7.4 – 缩放到最小值和最大值前后的正态和偏斜变量的分布
这种缩放方法将变量的最大值标准化为单位大小。将变量缩放到最小值和最大值通常是标准化的首选替代方案,适用于标准差非常小的变量,以及我们希望在稀疏数据中保留零条目时,例如在独热编码变量或来自计数的变量(如词袋)中。然而,此过程不会将变量中心化到零,因此如果算法有此要求,这种方法可能不是最佳选择。
注意
将变量缩放到最小值和最大值对异常值敏感。如果训练集中存在异常值,缩放会将值压缩到一端。相反,如果测试集中存在异常值,变量在缩放后将会显示大于1或小于0的值,具体取决于异常值是在左尾还是右尾。
如何操作...
在这个示例中,我们将把加利福尼亚住房数据集的变量缩放到0到1之间的值:
-
让我们先导入
pandas和所需的类和函数:import pandas as pd from sklearn.datasets import fetch_california_housing from sklearn.model_selection import train_test_split from sklearn.preprocessing import MinMaxScaler -
让我们从 scikit-learn 中加载加利福尼亚住房数据集到一个
pandasDataFrame 中,并丢弃Latitude和Longitude变量:X, y = fetch_california_housing( return_X_y=True, as_frame=True) X.drop(labels=["Latitude", "Longitude"], axis=1, inplace=True) -
让我们将数据分为训练集和测试集:
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=0) -
让我们设置缩放器并将其拟合到训练集,以便它学习每个变量的最小值和最大值以及值范围:
scaler = MinMaxScaler().set_output( transform="pandas"") scaler.fit(X_train) -
最后,让我们使用训练好的缩放器对训练集和测试集中的变量进行缩放:
X_train_scaled = scaler.transform(X_train) X_test_scaled = scaler.transform(X_test)
注意
MinMaxScaler()将最大值和最小值以及值范围分别存储在其data_max_、min_和data_range_属性中。
我们可以通过执行X_test_scaled.min()来验证变换后变量的最小值,它将返回以下输出:
MedInc 0.000000
HouseAge 0.000000
AveRooms 0.004705
AveBedrms 0.004941
Population 0.000140
AveOccup -0.000096
X_test_scaled.max(), we see that the maximum values of the variables are around 1:
MedInc 1.000000
HouseAge 1.000000
AveRooms 1.071197
AveBedrms 0.750090
Population 0.456907
AveOccup 2.074553
dtype: float64
Note
If you check the maximum values of the variables in the train set after the transformation, you’ll see that they are exactly `1`. Yet, in the test set, we see values greater and smaller than `1`. This occurs because, in the test set, there are observations with larger or smaller magnitudes than those in the train set. In fact, we see the greatest differences in the variables that deviate the most from the normal distribution (the last four variables in the dataset). This behavior is expected because scaling to the minimum and maximum values is sensitive to outliers and very skewed distributions.
Scaling to the minimum and maximum value does not change the shape of the variable’s distribution. You can corroborate that by displaying the histograms before and after the transformation.
How it works...
In this recipe, we scaled the variables of the California housing dataset to values between `0` and `1`.
`MinMaxScaler()` from scikit-learn learned the minimum and maximum values and the value range of each variable when we called the `fit()` method and stored these parameters in its `data_max_`, `min_`, and `data_range_` attributes. By using `transform()`, we made the scaler remove the minimum value from each variable in the train and test sets and divide the result by the value range.
Note
`MinMaxScaler()` will scale all variables by default. To scale only a subset of the variables in the dataset, you can use `ColumnTransformer()` from scikit-learn or `SklearnTransformerWrapper()` from `Feature-engine`.
`MinMaxScaler()` will scale the variables between `0` and `1` by default. However, we have the option to scale to a different range by adjusting the tuple passed to the `feature_range` parameter.
By default, `MinMaxScaler()` returns NumPy arrays, but we can modify this behavior to return `pandas` DataFrames with the `set_output()` method, as we did in *Step 4*.
Scaling with the median and quantiles
When scaling variables to the median and quantiles, the median value is removed from the observations, and the result is divided by the **Inter-Quartile Range** (**IQR**). The IQR is the difference between the 3rd quartile and the 1st quartile, or, in other words, the difference between the 75th percentile and the 25th percentile:

This method is known as **robust scaling** because it produces more robust estimates for the center and value range of the variable. Robust scaling is a suitable alternative to standardization when models require the variables to be centered and the data contains outliers. It is worth noting that robust scaling will not change the overall shape of the variable distribution.
How to do it...
In this recipe, we will implement scaling with the median and IQR by utilizing scikit-learn:
1. Let’s start by importing `pandas` and the required scikit-learn classes and functions:
```
导入 pandas 库作为 pd
从 sklearn.datasets 导入 fetch_california_housing
从 sklearn.model_selection 导入 train_test_split
从 sklearn.preprocessing 导入 RobustScaler
```py
2. Let’s load the California housing dataset into a `pandas` DataFrame and drop the `Latitude` and `Longitude` variables:
```
X, y = fetch_california_housing(
return_X_y=True, as_frame=True)
X.drop(labels=[ "Latitude", "Longitude"], axis=1,
inplace=True)
```py
3. Let’s divide the data into train and test sets:
```
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=0)
```py
4. Let’s set up scikit-learn’s `RobustScaler()`and fit it to the train set so that it learns and stores the median and IQR:
```
scaler = RobustScaler().set_output(
transform="pandas")
scaler.fit(X_train)
```py
5. Finally, let’s scale the variables in the train and test sets with the trained scaler:
```
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
```py
6. Let’s print the variable median values learned by `RobustScaler()`:
```
scaler.center_
```py
We see the parameters learned by `RobustScaler()` in the following output:
```
RobustScaler():
```py
scaler.scale_
array([2.16550000e+00, 1.90000000e+01, 1.59537022e+00, 9.41284380e-02, 9.40000000e+02, 8.53176853e-01])
```
这种缩放过程不会改变变量的分布。请使用直方图比较变换前后变量的分布。
```py
How it works...
To scale the features using the median and IQR, we created an instance of `RobustScaler()`. With `fit()`, the scaler learned the median and IQR for each variable from the train set. With `transform()`, the scaler subtracted the median from each variable in the train and test sets and divided the result by the IQR.
After the transformation, the median values of the variables were centered at `0`, but the overall shape of the distributions did not change. You can corroborate the effect of the transformation by displaying the histograms of the variables before and after the transformation and by printing out the main statistical parameters through `X_test.describe()` and `X_test_scaled.b()`.
Performing mean normalization
In mean normalization, we center the variable at `0` and rescale the distribution to the value range, so that its values lie between `-1` and `1`. This procedure involves subtracting the mean from each observation and then dividing the result by the difference between the minimum and maximum values, as shown here:

Note
Mean normalization is an alternative to standardization. In both cases, the variables are centered at `0`. In mean normalization, the variance varies, while the values lie between `-1` and `1`. On the other hand, in standardization, the variance is set to `1` and the value range varies.
Mean normalization is a suitable alternative for models that need the variables to be centered at zero. However, it is sensitive to outliers and not a suitable option for sparse data, as it will destroy the sparse nature.
How to do it...
In this recipe, we will implement mean normalization with `pandas`:
1. Let’s import `pandas` and the required scikit-learn class and function:
```
导入 pandas 库作为 pd
从 sklearn.datasets 导入 fetch_california_housing
从 sklearn.model_selection 导入 train_test_split
```py
2. Let’s load the California housing dataset from scikit-learn into a `pandas` DataFrame, dropping the `Latitude` and `Longitude` variables:
```
X, y = fetch_california_housing(
return_X_y=True, as_frame=True)
X.drop(labels=[
"Latitude", "Longitude"], axis=1, inplace=True)
```py
3. Let’s divide the data into train and test sets:
```
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=0)
```py
4. Let’s learn the mean values from the variables in the train set:
```
means = X_train.mean(axis=0)
```py
Note
We set `axis=0` to take the mean of the rows – that is, of the observations in each variable. If we set `axis=1` instead, `pandas` will calculate the mean value per observation across all the columns.
By executing `print(mean)`, we display the mean values per variable:
MedInc 3.866667
HouseAge 28.618702
AveRooms 5.423404
AveBedrms 1.094775
Population 1425.157323
AveOccup 3.040518
dtype: float64
1. Now, let’s determine the difference between the maximum and minimum values per variable:
```
ranges = X_train.max(axis=0)-X_train.min(axis=0)
```py
By executing `print(ranges)`, we display the value ranges per variable:
```
MedInc 14.500200
HouseAge 51.000000
AveRooms 131.687179
AveBedrms 33.733333
Population 35679.000000
AveOccup 598.964286
dtype: float64
```py
Note
The `pandas` `mean()`, `max()`, and `min()` methods return a `pandas` series.
1. Now, we’ll apply mean normalization to the train and test sets by utilizing the learned parameters:
```
X_train_scaled = (X_train - means) / ranges
X_test_scaled = (X_test - means) / ranges
```py
Note
In order to transform future data, you will need to store these parameters, for example, in a `.txt` or `.``csv` file.
*Step 6* returns `pandas` DataFrames with the transformed train and test sets. Go ahead and compare the variables before and after the transformations. You’ll see that the distributions did not change, but the variables are centered at `0`, and their values lie between `-1` and `1`.
How it works…
To implement mean normalization, we captured the mean values of the numerical variables in the train set using `mean()` from `pandas`. Next, we determined the difference between the maximum and minimum values of the numerical variables in the train set by utilizing `max()` and `min()` from `pandas`. Finally, we used the `pandas` series returned by these functions containing the mean values and the value ranges to normalize the train and test sets. We subtracted the mean from each observation in our train and test sets and divided the result by the value ranges. This returned the normalized variables in a `pandas` DataFrame.
There’s more...
There is no dedicated scikit-learn transformer to implement mean normalization, but we can combine the use of two transformers to do so.
To do this, we need to import `pandas` and load the data, just like we did in *Steps 1* to *3* in the *How to do it...* section of this recipe. Then, follow these steps:
1. Import the scikit-learn transformers:
```
从 sklearn.preprocessing 导入(
StandardScaler, RobustScaler
)
```py
2. Let’s set up `StandardScaler()` to learn and subtract the mean without dividing the result by the standard deviation:
```
scaler_mean = StandardScaler(
with_mean=True, with_std=False,
).set_output(transform="pandas")
```py
3. Now, let’s set up `RobustScaler()` so that it does not remove the median from the values but divides them by the value range – that is, the difference between the maximum and minimum values:
```
scaler_minmax = RobustScaler(
with_centering=False,
with_scaling=True,
quantile_range=(0, 100)
).设置输出为 transform="pandas"
```py
Note
To divide by the difference between the minimum and maximum values, we need to specify `(0, 100)` in the `quantile_range` argument of `RobustScaler()`.
1. Let’s fit the scalers to the train set so that they learn and store the mean, maximum, and minimum values:
```
scaler_mean.fit(X_train)
scaler_minmax.fit(X_train)
```py
2. Finally, let’s apply mean normalization to the train and test sets:
```
X_train_scaled = scaler_minmax.transform(
scaler_mean.transform(X_train)
)
X_test_scaled = scaler_minmax.transform(
scaler_mean.transform(X_test)
)
```py
We transformed the data with `StandardScaler()` to remove the mean and then transformed the resulting DataFrame with `RobustScaler()` to divide the result by the range between the minimum and maximum values. We described the functionality of `StandardScaler()` in this chapter’s *Standardizing the features* recipe and `RobustScaler()` in the *Scaling with the median and quantiles* recipe of this chapter.
Implementing maximum absolute scaling
Maximum absolute scaling scales the data to its maximum value – that is, it divides every observation by the maximum value of the variable:

As a result, the maximum value of each feature will be `1.0`. Note that maximum absolute scaling does not center the data, and hence, it’s suitable for scaling sparse data. In this recipe, we will implement maximum absolute scaling with scikit-learn.
Note
Scikit-learn recommends using this transformer on data that is centered at `0` or on sparse data.
Getting ready
Maximum absolute scaling was specifically designed to scale sparse data. Thus, we will use a bag-of-words dataset that contains sparse variables for the recipe. In this dataset, the variables are words, the observations are documents, and the values are the number of times each word appears in the document. Most entries in the data are `0`.
We will use a dataset consisting of a bag of words, which is available in the UCI Machine Learning Repository (https://archive.ics.uci.edu/ml/datasets/Bag+of+Words), which is licensed under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/legalcode).
I downloaded and prepared a small bag of words representing a simplified version of one of those datasets. You will find this dataset in the accompanying GitHub repository: [`github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/tree/main/ch07-scaling`](https://github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/tree/main/ch07-scaling).
How to do it...
Let’s begin by importing the required packages and loading the dataset:
1. Let’s import the required libraries and the scaler:
```
导入 matplotlib.pyplot 库作为 plt
导入 pandas 库作为 pd
从 sklearn.preprocessing 导入 MaxAbsScaler
```py
2. Let’s load the bag-of-words dataset:
```
data = pd.read_csv("bag_of_words.csv")
```py
If we execute `data.head()`, we will see the DataFrame consisting of the words as columns, the documents as rows, and the number of times each word appeared in a document as values:

Figure 7.5 – DataFrame with the bag of words
Note
Although we omit this step in the recipe, remember that the maximum absolute values should be learned from a training dataset only. Split the dataset into train and test sets when carrying out your analysis.
1. Let’s set up `MaxAbsScaler()` and fit it to the data so that it learns the variables’ maximum values:
```
scaler = MaxAbsScaler().set_output(
transform="pandas")
scaler.fit(data)
```py
2. Now, let’s scale the variables by utilizing the trained scaler:
```
data_scaled = scaler.transform(data)
```py
Note
`MaxAbsScaler ()` stores the maximum values in its `max_abs_` attribute.
1. Let’s display the maximum values stored by the scaler:
```
scaler.max_abs_
array([ 7., 6., 2., 2., 11., 4., 3., 6., 52., 2.])
```py
To follow up, let’s plot the distributions of the original and scaled variables.
2. Let’s make a histogram with the bag of words before the scaling:
```
data.hist(bins=20, figsize=(20, 20))
plt.show()
```py
In the following output, we see histograms with the number of times each word appears in a document:

Figure 7.6 – Histograms with different word counts
1. Now, let’s make a histogram with the scaled variables:
```
data_scaled.hist(bins=20, figsize=(20, 20))
plt.show()
```py
In the following output, we can corroborate the change of scale of the variables, but their distribution shape remains the same:

Figure 7.7 – Histograms of the word counts after the scaling
With scaling to the maximum absolute value, we linearly scale down the magnitude of the features.
How it works...
In this recipe, we scaled the sparse variables of a bag of words to their absolute maximum values by using `MaxAbsScaler()`. With `fit()`, the scaler learned the maximum absolute values for each variable and stored them in its `max_abs_` attribute. With `transform()`, the scaler divided the variables by their absolute maximum values, returning a `pandas` DataFrame.
Note
Remember that you can change the output container to a NumPy array or a `polars` DataFrame through the `set_output()` method of the scikit-learn library’s transformers.
There’s more...
If you want to center the variables’ distribution at `0` and then scale them to their absolute maximum, you can do so by combining the use of two scikit-learn transformers within a pipeline:
1. Let’s import the required libraries, transformers, and functions:
```
import pandas as pd
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import (
MaxAbsScaler, StandardScaler)
from sklearn.pipeline import Pipeline
```py
2. Let’s load the California housing dataset and split it into train and test sets:
```
X, y = fetch_california_housing(
return_X_y=True, as_frame=True)
X.drop( labels=[ "纬度",
"经度"], axis=1, inplace=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=0)
```py
3. Let’s set up `StandardScaler()` from scikit-learn so that it learns and subtracts the mean but does not divide the result by the standard deviation:
```
scaler_mean = StandardScaler(
with_mean=True, with_std=False)
```py
4. Now, let’s set up `MaxAbsScaler()` with its default parameters:
```
scaler_maxabs = MaxAbsScaler()
```py
5. Let’s include both scalers within a pipeline that returns pandas DataFrames:
```
scaler = Pipeline([
("scaler_mean", scaler_mean),
("scaler_max", scaler_maxabs),
]).set_output(transform="pandas")
```py
6. Let’s fit the scalers to the train set so that they learn the required parameters:
```
scaler.fit(X_train)
```py
7. Finally, let’s transform the train and test sets:
```
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
```py
The pipeline applies `StandardScaler()` and `MaxAbsScaler()` in sequence to first remove the mean and then scale the resulting variables to their maximum values.
Scaling to vector unit length
Scaling to the vector unit length involves scaling individual observations (not features) to have a unit norm. Each sample (that is, each row of the data) is rescaled independently of other samples so that its norm equals one. Each row constitutes a **feature vector** containing the values of every variable for that row. Hence, with this scaling method, we rescale the feature vector.
The norm of a vector is a measure of its magnitude or length in a given space and it can be determined by using the Manhattan (*l1*) or the Euclidean (*l2*) distance. The Manhattan distance is given by the sum of the absolute components of the vector:

The Euclidean distance is given by the square root of the square sum of the component of the vector:

Here, and are the values of variables *1*, *2*, and *n* for each observation. Scaling to unit norm consists of dividing each feature vector’s value by either *l1* or *l2*, so that after the scaling, the norm of the feature vector is *1*. To be clear, we divide each of and by *l1* or *l2*.
This scaling procedure changes the variables’ distribution, as illustrated in the following figure:

Figure 7.8 – Distribution of a normal and skewed variable before and after scaling each observation’s feature vector to its norm
Note
This scaling technique scales each observation and not each variable. The scaling methods that we discussed so far in this chapter aimed at shifting and resetting the scale of the variables’ distribution. When we scale to the unit length, however, we normalize each observation individually, contemplating their values across all features.
Scaling to the unit norm can be used when utilizing kernels to quantify similarity for text classification and clustering. In this recipe, we will scale each observation’s feature vector to a unit length of `1` using scikit-learn.
How to do it...
To begin, we’ll import the required packages, load the dataset, and prepare the train and test sets:
1. Let’s import the required Python packages, classes, and functions:
```
import numpy as np
import pandas as pd
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import Normalizer
```py
2. Let’s load the California housing dataset into a `pandas` DataFrame:
```
X, y = fetch_california_housing(
return_X_y=True, as_frame=True)
X.drop(labels=[
"纬度", "经度"], axis=1, inplace=True)
```py
3. Let’s divide the data into train and test sets:
```
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=0)
```py
4. Let’s set up the scikit-learn library’s `Normalizer()` transformer to scale each observation to the Manhattan distance or `l1`:
```
scaler = Normalizer(norm='l1')
```py
Note
To normalize to the Euclidean distance, you need to set the norm to `l2` using `scaler =` `Normalizer(norm='l2')`.
1. Let’s transform the train and test sets – that is, we’ll divide each observation’s feature vector by its norm:
```
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
```py
We can calculate the length (that is, the Manhattan distance of each observation’s feature vector) using `linalg()` from NumPy.
2. Let’s calculate the norm (Manhattan distance) before scaling the variables:
```
np.round(np.linalg.norm(X_train, ord=1, axis=1), 1)
```py
As expected, the norm of each observation varies:
```
array([ 255.3, 889.1, 1421.7, ..., 744.6, 1099.5,
1048.9])
```py
3. Let’s now calculate the norm after the scaling:
```
np.round(np.linalg.norm(
X_train_scaled, ord=1, axis=1), 1)
```py
Note
You need to set `ord=1` for the Manhattan distance and `ord=2` for the Euclidean distance as arguments of NumPy’s `linalg()`function, depending on whether you scaled the features to the `l1` or `l2` norm.
We see that the Manhattan distance of each feature vector is `1` after scaling:
array([1., 1., 1., ..., 1., 1., 1.])
Based on the scikit-learn library’s documentation, this scaling method can be useful when using a quadratic form such as the dot-product or any other kernel to quantify the similarity of a pair of samples.
How it works...
In this recipe, we scaled the observations from the California housing dataset to their feature vector unit norm by utilizing the Manhattan or Euclidean distance. To scale the feature vectors, we created an instance of `Normalizer()` from scikit-learn and set the norm to `l1` for the Manhattan distance. For the Euclidean distance, we set the norm to `l2`. Then, we applied the `fit()` method, although there were no parameters to be learned, as this normalization procedure depends exclusively on the values of the features for each observation. Finally, with the `transform()` method, the scaler divided each observation’s feature vector by its norm. This returned a NumPy array with the scaled dataset. After the scaling, we used NumPy’s `linalg.norm` function to calculate the norm (`l1` and `l2`) of each vector to confirm that after the transformation, it was `1`.
第八章:创建新特征
向数据集中添加新特征可以帮助机器学习模型学习数据中的模式和重要细节。例如,在金融领域,可支配收入,即任何一个月的总收入减去获得债务,可能比仅仅的收入或获得债务对信用风险更为相关。同样,一个人在金融产品中的总获得债务,如车贷、房贷和信用卡,可能比单独考虑的任何债务对估计信用风险更为重要。在这些例子中,我们使用领域知识来构建新变量,这些变量是通过添加或减去现有特征创建的。
在某些情况下,一个变量可能没有与目标变量呈线性或单调关系,但多项式组合可能存在。例如,如果我们的变量与目标变量呈二次关系,
,我们可以通过平方原始变量将其转换为线性关系。我们还可以通过使用样条或决策树来转换预测变量,帮助线性模型更好地理解变量和目标之间的关系。
通过构建额外的特征来训练更简单的模型,例如线性或逻辑回归,其优势在于特征和模型都保持可解释性。我们可以向管理层、客户和监管机构解释驱动模型输出的原因,为我们的机器学习流程增加一层透明度。此外,简单的模型往往训练速度更快,部署和维护也更容易。
在本章中,我们将通过变换或结合变量使用数学函数、样条和决策树来创建新特征。
本章将涵盖以下食谱:
-
使用数学函数组合特征
-
将特征与参考变量进行比较
-
执行多项式展开
-
使用决策树组合特征
-
从周期性变量创建周期特征
-
创建样条特征
技术要求
在本章中,我们将使用 pandas、numpy、matplotlib、scikit-learn 和 feature-engine 这些 Python 库。
使用数学函数组合特征
通过结合现有变量和数学及统计函数可以创建新特征。以金融行业为例,我们可以通过汇总个人在单个金融产品中的债务(如车贷、房贷或信用卡债务)来计算一个人的总债务:
总债务 = 车贷债务 + 信用卡债务 + 房贷债务
我们还可以使用其他统计操作推导出其他有洞察力的特征。例如,我们可以确定客户在金融产品中的最大债务或用户在网站上的平均停留时间:
最大债务 = max(车贷余额, 信用卡余额, 按揭余额)
网站平均停留时间 = mean(主页停留时间, 关于页面停留时间, FAQ 页面停留时间)
在原则上,我们可以使用任何数学或统计运算来创建新的特征,例如乘积、平均值、标准差,或者最大或最小值。在这个食谱中,我们将使用 pandas 和 feature-engine 来实现这些数学运算。
注意
虽然,在食谱中,我们可以向您展示如何使用数学函数组合特征,但我们无法公正地展示在决定应用哪个函数时领域知识的运用,因为每个领域都有所不同。所以,我们将这部分留给你。
准备工作
在这个食谱中,我们将使用来自 scikit-learn 的乳腺癌数据集。特征是通过乳腺细胞的数字化图像计算得出的,描述了细胞核的平滑度、凹陷度、对称性和紧凑度等特征。每一行包含关于组织样本中细胞核形态的信息。目标变量表示组织样本是否对应于癌细胞。目标是根据细胞核的形态预测组织样本属于良性还是恶性的乳腺细胞。
为了熟悉数据集,请在 Jupyter 笔记本或 Python 控制台中运行以下命令:
from sklearn.datasets import load_breast_cancer
data = load_breast_cancer()
print(data.DESCR)
上述代码块应该打印出数据集的描述及其变量的解释。
如何做到这一点...
在这个食谱中,我们将通过使用多个数学运算来组合变量来创建新的特征:
-
让我们先加载必要的库、类和数据:
import pandas as pd from feature_engine.creation import MathFeatures from sklearn.datasets import load_breast_cancer -
接下来,将乳腺癌数据集加载到
pandasDataFrame 中:data = load_breast_cancer() df = pd.DataFrame(data.data, columns=data.feature_names)在以下代码行中,我们将通过使用多个数学运算来组合变量创建新的特征。
-
让我们先创建一个包含我们想要组合的特征子集的列表:
features = [ «mean smoothness", «mean compactness", «mean concavity", «mean concave points", «mean symmetry", ]步骤 3 中的特征代表了图像中细胞核的平均特征。获取所有检查特征的均值可能是有用的。
-
让我们计算特征的均值并显示结果特征:
df["mean_features"] = df[features].mean(axis=1) df["mean_features"].head()下面的输出显示了 步骤 3 中特征的均值:
0 0.21702 1 0.10033 2 0.16034 3 0.20654 4 0.14326 Name: mean_features, dtype: float64 -
同样,为了捕捉细胞核的一般变异性,让我们确定平均特征的标准差,然后显示结果特征:
df["std_features"] = df[features].std(axis=1) df["std_features"].head()下面的输出显示了 步骤 3 中特征的标准差:
0 0.080321 1 0.045671 2 0.042333 3 0.078097 4 0.044402 Name: std_features, dtype: float64
注意
当我们根据领域知识构建新的特征时,我们确切地知道我们想要如何组合变量。我们也可以通过多个运算组合特征,然后评估它们是否具有预测性,例如使用特征选择算法或从机器学习模型中推导特征重要性。
-
让我们创建一个包含我们想要使用的数学函数的列表来组合特征:
math_func = [ "sum", "prod", "mean", "std", "max", "min"] -
现在,让我们应用 步骤 6 中的函数来组合 步骤 3 中的特征,将结果变量捕获到一个新的 DataFrame 中:
df_t = df[features].agg(math_func, axis="columns")如果我们执行
df_t.head(),我们将看到包含新创建特征的 DataFrame:

图 8.1 – 包含新创建特征的 DataFrame
注意
pandas 的 agg 函数可以应用多个函数来组合特征。它可以接受一个包含函数名称的字符串列表,就像我们在 步骤 7 中做的那样;一个包含 NumPy 函数(如 np.log)的列表;以及你创建的 Python 函数。
我们可以使用 feature-engine 自动创建与使用 pandas 创建的相同特征。
-
让我们使用输出特征的名称创建一个列表:
new_feature_names = [ "sum_f", "prod_f", "mean_f", „std_f", „max_f", „min_f"] -
让我们设置
MathFeatures()来将 步骤 6 中的函数应用于 步骤 3 中的特征,使用 步骤 8 中的字符串来命名新特征:create = MathFeatures( variables=features, func=math_func, new_variables_names=new_feature_names, ) -
让我们将新特征添加到原始 DataFrame 中,将结果捕获到一个新变量中:
df_t = create.fit_transform(df)我们可以通过执行
df_t[features + new_feature_names].head()来显示输入和输出特征:

图 8.2 – 包含输入特征和新创建变量的 DataFrame
虽然 pandas 的 agg 函数返回一个包含操作结果的 DataFrame,但 feature-engine 会更进一步,通过将新特征连接到原始 DataFrame 上。
它是如何工作的...
pandas 有许多内置操作,可以将数学和统计计算应用于一组变量。为了在数学上组合特征,我们首先创建了一个包含我们想要组合的特征名称的列表。然后,我们使用 pandas 的 mean() 和 std() 函数确定了这些特征的平均值和标准差。我们还可以应用 sum()、prod()、max() 和 min() 方法中的任何一个,这些方法分别返回这些特征的总和、乘积、最大值和最小值。为了在列上执行这些操作,我们在方法中添加了 axis=1 参数。
使用 pandas 的 agg() 函数,我们可以同时应用多个数学函数。它接受一个字符串列表作为参数,对应于要应用的功能和函数应该应用的 axis,可以是 1(列)或 0(行)。因此,pandas 的 agg() 函数返回一个应用数学函数到特征组的 pandas DataFrame。
最后,我们通过结合变量和使用 feature-engine 创建了相同的特征。我们使用了 MathFeatures() 转换器,它接受要组合的特征和要应用的功能作为输入;它还提供了指示结果特征名称的选项。当我们使用 fit() 时,转换器没有学习参数,而是检查变量确实是数值的。transform() 方法触发了底层使用 pandas.agg,应用数学函数来创建新的变量。
参见
要了解更多关于 pandas 支持的数学运算,请访问 pandas.pydata.org/pandas-docs/stable/reference/frame.html#computations-descriptive-stats。
要了解更多关于 pandas aggregate 的信息,请查看 pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.aggregate.html。
比较特征与参考变量
在之前的菜谱 使用数学函数组合特征 中,我们通过将数学或统计函数(如总和或平均值)应用于一组变量来创建新特征。然而,一些数学运算(如减法或除法)是在特征之间进行的。这些操作对于推导比率(如 负债收入比)很有用:
负债收入比 = 总负债 / 总收入
这些操作也有助于计算差异,例如 可支配收入:
可支配收入 = 收入 - 总负债
在这个菜谱中,我们将学习如何通过使用 pandas 和 feature-engine 对变量进行减法或除法来创建新特征。
注意
在这个菜谱中,我们将向您展示如何通过减法和除法创建特征。我们希望这些与金融部门相关的例子能对如何使用领域知识来决定要组合哪些特征以及如何进行一些启发。
如何做到这一点...
让我们先加载必要的 Python 库和来自 scikit-learn 的乳腺癌数据集:
-
加载必要的库、类和数据:
import pandas as pd from feature_engine.creation import RelativeFeatures from sklearn.datasets import load_breast_cancer -
将乳腺癌数据集加载到
pandasDataFrame 中:data = load_breast_cancer() df = pd.DataFrame(data.data, columns=data.feature_names)在乳腺癌数据集中,一些特征捕捉了乳腺细胞细胞核的最差和平均特征。例如,对于每个图像(即每行),我们都有观察到的所有核的最差紧密度和所有核的平均紧密度。一个捕捉最差值和平均值之间差异的特征可以预测恶性。
-
让我们捕捉两个特征之间的差异,即细胞核的
最差紧密度和平均紧密度,并将它们存储在一个新变量中并显示其值:df["difference"] = df["worst compactness"].sub( df["mean compactness"]) df["difference"].head()在以下输出中,我们可以看到这些特征值之间的差异:
0 0.38800 1 0.10796 2 0.26460 3 0.58240 4 0.07220 Name: difference, dtype: float64
注意
我们可以通过执行 df["difference"] = df["worst compactness"] - (``df["mean compactness"]) 来进行相同的计算。
同样,细胞核最坏和平均特征之间的比率可能表明恶性。
-
让我们创建一个新特征,该特征是核的最坏和平均半径之间的比率,然后显示其值:
df["quotient"] = df["worst radius"].div( df["mean radius"]) df["quotient"].head()在以下输出中,我们可以看到特征之间的比率值:
0 1.410784 1 1.214876 2 1.197054 3 1.305604 4 1.110892 Name: quotient, dtype: float64
注意
我们可以通过执行一个替代命令来计算比率,df["quotient"] = df["worst radius"] / (``df["mean radius"])。
我们还可以捕获每个核形态特征与核的平均半径或平均面积之间的比率。让我们首先将这些变量的子集捕获到列表中。
-
让我们列出分子中的特征:
features = [ "mean smoothness", «mean compactness", "mean concavity", "mean symmetry" ] -
让我们列出分母中的特征:
reference = ["mean radius", "mean area"]
注意
我们可以通过在 pandas 中将 步骤 5 中的特征除以 步骤 6 中的某个特征来创建特征,通过执行 df[features].div(df["mean radius"])。对于减法,我们将执行 df[features].sub(df["mean radius"])。
-
让我们设置
feature-engine库的RelativeFeatures(),使其从 步骤 5 的每个特征中减去或除以 步骤 6 的特征:creator = RelativeFeatures( variables=features, reference=reference, func=["sub", "div"], )
注意
从 步骤 5 和 步骤 6 中减去特征在生物学上没有意义,但我们将这样做以演示 RelativeFeatures() 转换器的使用。
-
让我们将新特征添加到 DataFrame 中,并将结果捕获在一个新变量中:
df_t = creator.fit_transform(df) -
让我们将新特征的名称捕获到一个列表中:
all_feat = creator.feature_names_in_ new_features = [ f for f in df_t.columns if f not in all_feat]
注意
feature_names_in_ 是 scikit-learn 和 feature-engine 转换器中的一个常见属性,它存储用于拟合转换器的 DataFrame 中的变量名称。换句话说,它存储了输入特征的名称。当使用 transform() 时,转换器会检查新输入数据集中的特征是否与训练期间使用的特征匹配。在 步骤 9 中,我们利用这个属性来查找在转换后添加到数据中的额外变量。
如果我们执行 print(new_features),我们将看到一个包含由 ReferenceFeatures() 创建的特征名称的列表。请注意,这些特征包含数学方程式左侧和右侧的变量,以及应用于它们的函数以创建新特征:
['mean smoothness_sub_mean radius',
'mean compactness_sub_mean radius',
'mean concavity_sub_mean radius',
'mean symmetry_sub_mean radius',
'mean smoothness_sub_mean area',
'mean compactness_sub_mean area',
'mean concavity_sub_mean area',
'mean symmetry_sub_mean area',
'mean smoothness_div_mean radius',
'mean compactness_div_mean radius',
'mean concavity_div_mean radius',
'mean symmetry_div_mean radius',
'mean smoothness_div_mean area',
'mean compactness_div_mean area',
'mean concavity_div_mean area',
'mean symmetry_div_mean area']
最后,我们可以通过执行 df_t[new_features].head() 来显示结果变量的前五行:

图 8.3 – 包含新创建特征的 DataFrame
feature-engine 将新特征作为原始 DataFrame 右侧的列添加,并自动将这些特征的变量名添加到其中。通过这样做,feature-engine 自动化了我们本应使用 pandas 做的大量手动工作。
它是如何工作的...
pandas有许多内置操作来比较一个特征或一组特征与一个参考变量。在这个菜谱中,我们使用了 pandas 的sub()和div()函数来确定两个变量或一组变量与一个参考特征之间的差异或比率。
要从一个变量中减去另一个变量,我们对第一个变量应用了sub()函数到一个pandas序列上,将第二个变量的pandas序列作为参数传递给sub()函数。这个操作返回了一个第三个pandas序列,包含第一个和第二个变量之间的差值。要除以另一个变量,我们使用了div()函数,它的工作方式与sub()相同——即,它将左侧的变量除以div()函数作为参数传递的变量。
然后,我们通过使用Feature-engine的ReferenceFeatures()函数,自动将几个变量与两个参考变量通过减法或除法组合起来。ReferenceFeatures()转换器接受要组合的变量、参考变量以及用于组合它们的函数。当使用fit()时,转换器不会学习参数,而是检查变量是否为数值。执行transform()会将新特征添加到 DataFrame 中。
注意
ReferenceFeatures()还可以为与第二组参考变量相关的一组变量添加、乘法、取模或求幂。您可以在其文档中了解更多信息:feature-engine.readthedocs.io/en/latest/api_doc/creation/RelativeFeatures.html。
参见
要了解更多关于pandas支持的二进制操作,请访问pandas.pydata.org/pandas-docs/stable/reference/frame.html#binary-operator-functions。
执行多项式展开
简单模型,如线性回归和逻辑回归,如果我们向它们提供正确的特征,可以捕捉到复杂的模式。有时,我们可以通过将数据集中的变量与自身或其他变量组合来创建强大的特征。例如,在下面的图中,我们可以看到目标y与变量x有二次关系,如图左侧面板所示,线性模型无法准确捕捉这种关系:

图 8.4 – 一个线性模型拟合以预测目标,y,从特征,x,该特征与目标之间存在二次关系,在平方 x 之前和之后。在左侧面板:模型通过使用原始变量提供较差的拟合;在右侧面板,模型基于原始变量的平方提供更好的拟合
此线性模型在平方x之前和之后都与目标存在二次关系。然而,如果我们平方x,换句话说,如果我们创建特征的二次多项式,线性模型可以准确地从x的平方预测目标y,正如我们在右侧面板中看到的那样。
另一个经典例子是,一个简单的特征可以使一个简单的模型,如逻辑回归,理解数据中的潜在关系,这就是XOR情况。在以下图表的左侧面板中,我们看到目标类别是如何分布在x1和x2的值上的(类别用不同的颜色阴影突出显示):

图 8.5 – XOR 关系的插图以及如何通过组合特征实现完整的类别分离
如果两个特征都是正的,或者两个特征都是负的,那么类别是 1,但如果特征具有不同的符号,那么类别是 0(左侧面板)。逻辑回归将无法从每个单独的特征中识别出这种模式,因为我们可以在中间面板中看到,特征值之间存在显著的类别重叠 – 在这种情况下,x1。然而,将 x1 乘以 x2 创建了一个特征,这使得逻辑回归能够准确地预测类别,因为 x3,正如我们在右侧面板中看到的那样,允许类别被清楚地分离。
使用类似的逻辑,相同或不同变量的多项式组合可以返回新的变量,这些变量传达了额外的信息并捕捉了特征交互,从而为线性模型提供了有用的输入。在大型数据集中,分析每个可能的变量组合并不总是可能的。但我们可以使用例如scikit-learn自动创建多个多项式变量,并让模型决定哪些变量是有用的。在本菜谱中,我们将学习如何使用 scikit-learn 通过多项式组合创建多个特征。
准备就绪
多项式展开用于自动化新特征的创建,捕捉特征交互,以及原始变量与目标之间的潜在非线性关系。要创建多项式特征,我们需要确定哪些特征要组合以及使用哪个多项式度数。
注意
在确定要组合的特征或多项式组合的次数并不容易,记住高次多项式将导致大量新特征的生成,可能会导致过拟合。一般来说,我们保持次数较低,最多为 2 或 3。
scikit-learn中的PolynomialFeatures()转换器会自动创建特征的多项式组合,其次数小于或等于用户指定的次数。
为了轻松跟进这个配方,让我们首先了解当使用PolynomialFeatures()创建三个变量的二次和三次多项式组合时的输出。
三个变量(a,b,c)的二次多项式组合将返回以下新特征:
1, a, b, c, ab, ac, bc, a2, b2, c2
从前面的特征来看,a,b,c是原始变量;ab,ac,bc是这些特征的乘积;而a2,b2,c2是原始特征的平方值。PolynomialFeatures()还会返回一个偏置项1,在创建特征时我们可能不会包含它。
注意
生成特征——ab,ac,和bc——被称为交互或2 度的特征交互。度数反映了组合的变量数量。结果组合了最多两个变量,因为我们指定了二次多项式为允许的最大组合。
三个变量(a,b,c)的三次多项式组合将返回以下新特征:
1, a, b, c, ab, ac, bc, abc, a2b, a2c, b2a, b2c, c2a, c2b, a3, b3, c3
在返回的特征中,除了二次多项式组合返回的特征外,我们现在还有特征自身的三次组合(a3,b3,和c3),每个特征与第二个特征线性组合的平方值(a2b,a2c,b2a,b2c,c2a,和c2b),以及三个特征的乘积(abc)。注意我们包含了所有可能的 1 度、2 度和 3 度交互以及偏置项1。
现在我们已经了解了scikit-learn实现的多项式展开的输出,让我们进入配方。
如何做到这一点...
在这个配方中,我们将使用一个玩具数据集通过多项式展开来创建特征,以便熟悉生成的变量。使用真实数据集的多项式展开创建特征与我们在本配方中将要讨论的相同:
-
让我们导入所需的库、类和数据:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn import set_config from sklearn.preprocessing import PolynomialFeatures -
让我们设置
scikit-learn库的set_outputAPI 全局,以便所有转换器在transform()方法的输出结果中返回一个 DataFrame:set_config(transform_output="pandas") -
让我们创建一个包含一个变量的 DataFrame,其值为 1 到 10:
df = pd.DataFrame(np.linspace( 0, 10, 11), columns=["var"]) -
让我们设置
PolynomialFeatures()以创建单变量的所有可能组合,直到三次多项式,并从结果中排除偏置项——也就是说,我们将排除值1:poly = PolynomialFeatures( degree=3, interaction_only=False, include_bias=False) -
现在,让我们创建多项式组合:
dft = poly.fit_transform(df)如果我们执行
dft,我们将看到一个包含原始特征的 DataFrame,其后是其值的平方,然后是其值的立方:

图 8.6 – 单个变量的三次多项式展开的 DataFrame
如果 PolynomialFeatures() 返回一个 NumPy 数组而不是 DataFrame,并且你想获得数组中特征的名称,你可以通过执行 poly.get_feature_names_out() 来实现,它返回 array(['var', 'var²', 'var³'], dtype=object)。
-
现在,让我们将新的特征值与原始变量进行绘图:
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 – 多项式展开产生的特征与原始变量之间的关系
-
让我们在玩具数据集中添加两个额外的变量,其值从 1 到 10:
df["col"] = np.linspace(0, 5, 11) df["feat"] = np.linspace(0, 5, 11) -
接下来,让我们将数据集中的三个特征与多项式展开到二次方,但这次,我们只返回由至少两个不同变量组合产生的特征,即交互特征:
poly = PolynomialFeatures( degree=2, interaction_only=True, include_bias=False) dft = poly.fit_transform(df)如果我们执行
dft,我们将看到多项式展开产生的特征,这些特征包含原始特征,以及三个变量的所有可能组合,但没有二次项,因为我们设置转换器只返回特征之间的交互:

图 8.8 – 使用多项式展开创建特征的结果,仅保留变量之间的交互
注意
尝试创建特征的第三次方多项式组合,只返回交互或所有可能特征,以更好地理解 PolynomialFeatures() 的输出。
通过这样,我们已经学会了如何通过将现有变量与自身或其他特征结合来创建新特征。使用真实数据集通过多项式展开创建特征在本质上是一致的。
如果你只想组合特征的一个子集,你可以通过使用ColumnTransformer()来选择要组合的特征,就像我们在本食谱后面的“还有更多...”部分中将要展示的那样,或者通过使用来自feature-engine的SklearnTransformerWrapper(),正如你在附带的 GitHub 仓库中可以看到的那样:github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch08-creation/Recipe3-PolynomialExpansion.ipynb。
它是如何工作的...
在这个食谱中,我们通过使用特征与其自身或三个变量之间的多项式组合来创建特征。为了创建这些多项式特征,我们使用了scikit-learn中的PolynomialFeatures()。默认情况下,PolynomialFeatures()生成一个新的特征矩阵,该矩阵包含数据中所有特征的所有多项式组合,其度数小于或等于用户指定的degree。通过将degree设置为3,我们创建了所有可能的三次或更小的多项式组合。为了保留特征与其自身的组合,我们将interaction_only参数设置为False。为了避免返回偏差项,我们将include_bias参数设置为False。
注意
将interaction_only参数设置为True仅返回交互项 – 即由两个或更多变量的组合产生的变量。
fit()方法根据指定的参数确定了所有可能的特征组合。在这个阶段,转换器没有执行实际的数学计算。transform()方法使用特征执行数学计算以创建新的变量。通过get_feature_names()方法,我们可以识别展开的项 – 即每个新特征是如何计算的。
在步骤 2中,我们将scikit-learn库的set_output API 的pandas DataFrame 设置为transform()方法的结果。scikit-learn 转换器默认返回NumPy数组。新的set_output API 允许我们将结果容器的类型更改为pandas或polars DataFrame。每次设置转换器时,我们都可以单独设置输出 – 例如,使用poly = PolynomialFeatures().set_output(transform="pandas")。或者,就像在这个食谱中做的那样,我们可以设置全局配置,然后每次设置新的转换器时,它将返回一个pandas DataFrame。
还有更多...
让我们在乳腺癌数据集的变量子集上执行多项式展开来创建特征:
-
首先,导入必要的库、类和数据:
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 -
然后,加载数据并将其分为训练集和测试集:
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 ) -
创建一个要组合的特征列表:
features = [ "mean smoothness", "mean compactness", "mean concavity"] -
设置
PolynomialFeatures()以创建所有可能的三次或更小的组合:poly = PolynomialFeatures( degree=3, interaction_only=False, include_bias=False) -
设置列转换器以仅从步骤 3中指定的列创建特征:
ct = ColumnTransformer([("poly", poly, features)]) -
创建多项式特征:
train_t = ct.fit_transform(X_train) test_t = ct.transform(X_test)
就这样。通过执行ct.get_feature_names_out(),我们获得了新特征的名称。
注意
ColumnTransformer()将poly这个词添加到结果变量中,这是我们在步骤 5中给ColumnTransformer()步骤取的名字。我不是特别喜欢这种行为,因为它使得数据分析变得更难,因为你需要跟踪变量名的变化。为了避免变量名变化,你可以使用feature-engine的SklearnTransformerWrapper()。
将特征与决策树相结合
在 2009 年知识发现与数据挖掘(KDD)竞赛的获胜方案中,作者通过决策树将两个或多个变量组合起来创建新特征。在检查变量时,他们注意到一些特征与目标有很高的互信息,但相关性较低,这表明与目标的关系不是线性的。虽然这些特征在基于树的算法中使用时是预测性的,但线性模型无法利用它们。因此,为了在线性模型中使用这些特征,他们用训练在单个特征或两个或三个变量组合上的决策树输出的特征替换了这些特征,从而返回了与目标具有单调关系的新的特征。
简而言之,将特征与决策树相结合对于创建与目标具有单调关系的特征是有用的,这对于使用线性模型进行准确预测是有用的。该过程包括使用特征子集训练决策树——通常是每次一个、两个或三个——然后使用树的预测作为新的特征。
注意
你可以在这篇文章中找到关于此过程和 2009 年 KDD 数据竞赛整体获胜解决方案的更多详细信息:proceedings.mlr.press/v7/niculescu09/niculescu09.pdf。
好消息是,我们可以使用feature-engine自动化使用树的特性创建,在这个菜谱中,我们将学习如何做到这一点。
如何操作...
在这个菜谱中,我们将使用加利福尼亚住房数据集将特征与决策树相结合:
-
首先,我们导入
pandas以及所需的函数、类和数据集:import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import fetch_california_housing from sklearn.model_selection import train_test_split from feature_engine.creation, import DecisionTreeFeatures -
让我们将加利福尼亚住房数据集加载到
pandasDataFrame 中,并删除Latitude和Longitude变量:X, y = fetch_california_housing( return_X_y=True, as_frame=True) X.drop(labels=[ "Latitude", "Longitude"], axis=1, inplace=True) -
将数据集分为训练集和测试集:
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=0) -
检查特征与目标之间的皮尔逊相关系数,这是线性关系的度量:
for var in X_train.columns: pearson = np.corrcoef(X_train[var], y_train)[0, 1] pearson = np.round(pearson, 2) print( f"corr {var} vs target: {pearson}")在以下输出中,我们可以看到,除了
MedInc之外,大多数变量与目标之间没有显示出强烈的线性关系;相关系数小于 0.5:corr MedInc vs target: 0.69 corr HouseAge vs target: 0.1 corr AveRooms vs target: 0.16 corr AveBedrms vs target: -0.05 corr Population vs target: -0.03 feature-engine library’s DecisionTreeFeatures() selects the best tree by using cross-validation. -
创建一个超参数网格以优化每个决策树:
param_grid = {"max_depth": [2, 3, 4, None]}feature-engine库的DecisionTreeFeatures()允许我们添加由在单个或多个特征上训练的决策树预测出的特征。我们可以以多种方式指导转换器组合特征。我们将从创建两个变量之间的所有可能组合开始。 -
列出我们想要用作输入的两个功能:
variables = ["AveRooms", "AveBedrms"] -
将
DecisionTreeFeatures()设置为创建来自步骤 6中特征的所有可能组合:dtf = DecisionTreeFeatures( variables=variables, features_to_combine=None, cv=5, param_grid=param_grid, scoring="neg_mean_squared_error", regression=True, )
注意
我们将regression设置为True,因为在这个数据集中的目标是连续的。如果你有一个二元目标或正在执行分类,请将其设置为False。确保选择一个适合你模型的评估指标(scoring)。
-
拟合转换器,以便它在输入特征上训练决策树:
dtf.fit(X_train, y_train) -
如果你想知道哪些特征被用于训练决策树,你可以像这样检查它们:
dtf.input_features_在以下输出中,我们可以看到
DecisionTreeFeatures()已经训练了三个决策树——两个使用单个特征AveRooms和AveBedrms,一个使用这两个特征:['AveRooms', 'AveBedrms', ['AveRooms', 'AveBedrms']]
注意
DecisionTreeFeatures()还存储决策树。你可以通过执行dtf.estimators_来检查它们。
-
现在,将这些特征添加到训练和测试集中:
train_t = dtf.transform(X_train) test_t = dtf.transform(X_test) -
列出新特征的名称(转换器将单词
tree附加到特征名称上):tree_features = [ var for var in test_t.columns if "tree" in var ] -
最后,显示添加到测试集中的特征:
test_t[tree_features].head()在以下输出中,我们可以看到由步骤 8中训练的决策树产生的新特征的前五行:

图 8.9 – 包含从决策树中派生出的特征的测试集的一部分
-
为了检查这种转换的力量,计算新特征与目标之间的皮尔逊相关系数:
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如果你想要组合特定的特征而不是获取变量之间的所有可能组合,你可以通过指定元组中的输入特征来实现。
-
创建一个包含我们想要用作决策树输入的不同特征的元组元组:
features = (('Population'), ('Population','AveOccup'), ('Population', 'AveOccup', 'HouseAge')) -
现在,我们需要将这些元组传递给
DecisionTreeFeatures()的features_to_combine参数:dtf = DecisionTreeFeatures( variables=None, features_to_combine=features, cv=5, param_grid=param_grid, scoring="neg_mean_squared_error" ) dtf.fit(X_train, y_train) -
我们在上一步骤中拟合了转换器,因此我们可以继续将特征添加到训练和测试集中:
train_t = dtf.transform(X_train) test_t = dtf.transform(X_test) -
显示新功能:
tree_features = [ var for var in test_t.columns if "tree" in var] test_t[tree_features].head()在以下输出中,我们可以看到来自测试集中决策树预测的新特征:

图 8.10 – 包含从决策树中提取的特征的测试集的一部分
为了总结这个菜谱,我们将比较使用原始特征训练的 Lasso 线性回归模型和使用从决策树中提取的特征训练的模型的性能。
-
从
scikit-learn导入Lasso和cross_validate函数:from sklearn.linear_model import Lasso from sklearn.model_selection import cross_validate -
设置 Lasso 回归模型:
lasso = Lasso(random_state=0, alpha=0.0001) -
使用交叉验证和原始数据训练和评估模型,然后打印出结果的r-squared:
cv_results = cross_validate(lasso, X_train, y_train, cv=3) mean = cv_results['test_score'].mean() std = cv_results['test_score'].std() print(f"Results: {mean} +/- {std}")在下面的输出中,我们可以看到使用原始特征训练的 Lasso 回归模型的r-square 值:
Results: 0.5480403481478856 +/- 0.004214649109293269 -
最后,使用从决策树中提取的特征训练 Lasso 回归模型,并使用交叉验证进行评估:
variables = ["AveRooms", "AveBedrms", "Population"] train_t = train_t.drop(variables, axis=1) cv_results = cross_validate(lasso, train_t, y_train, cv=3) mean = cv_results['test_score'].mean() std = cv_results['test_score'].std() print(f"Results: {mean} +/- {std}")在下面的输出中,我们可以看到基于树衍生特征训练的 Lasso 回归模型的性能更好;其r-square 值大于步骤 20中的值:
Results: 0.5800993721099441 +/- 0.002845475651622909
我希望我已经给你展示了结合决策树和特征以及如何使用feature-engine做到这一点的力量。
它是如何工作的...
在这个菜谱中,我们基于在一个或多个变量上训练的决策树的预测创建了新的特征。我们使用Feature-engine中的DecisionTreeFeatures()来自动化使用交叉验证和超参数优化训练决策树的过程。
DecisionTreeFeatures()在底层使用网格搜索来训练决策树。因此,你可以传递一个超参数网格来优化树,或者转换器将仅优化深度,这在任何情况下都是决策树中最重要的参数。你还可以通过scoring参数更改你想要优化的度量,并通过cv参数更改你想要使用的交叉验证方案。
DecisionTreeFeatures()最令人兴奋的特性是其推断特征组合以创建树衍生特征的能力,这通过features_to_combine参数进行调节。如果你向这个参数传递一个整数——比如说,例如3,DecisionTreeFeatures()将创建所有可能的 1、2 和 3 个特征的组合,并使用这些组合来训练决策树。你也可以传递一个整数的列表——比如说,[2,3]——在这种情况下,DecisionTreeFeatures()将创建所有可能的 2 和 3 个特征的组合。你还可以通过传递元组中的特征组合来指定你想要组合的特征以及如何组合,就像我们在步骤 14中所做的那样。
使用fit(),DecisionTreeFeatures()找到特征组合并训练决策树。使用transform(),DecisionTreeFeatures()将决策树产生的特征添加到 DataFrame 中。
注意
如果你正在训练回归或多类分类,新特征将是连续目标的预测或类别。如果你正在训练二元分类模型,新特征将来自类别 1 的概率。
在添加新特征后,我们通过分析皮尔逊相关系数来比较它们与目标之间的关系,该系数返回线性关联的度量。我们发现从树中派生的特征具有更大的相关系数。
参见
如果你想了解更多关于互信息是什么以及如何计算它的信息,请查看这篇文章:www.blog.trainindata.com/mutual-information-with-python/.
从周期性变量创建周期性特征
一些特征是周期性的——例如,一天中的小时,一年中的月份,一周中的天数。它们都从一个特定的值(比如说,一月)开始,上升到另一个特定的值(比如说,十二月),然后从头开始。一些特征是数字的,比如小时,而一些可以用数字表示,比如月份,其值为 1 到 12。然而,这种数字表示并没有捕捉到变量的周期性或循环性质。例如,十二月(12)比六月(6)更接近一月(1);然而,这种关系并没有被特征的数值表示所捕捉。但如果我们用正弦和余弦函数,这两种自然周期函数来转换这些变量,我们就可以改变它。
使用正弦和余弦函数对周期性特征进行编码允许线性模型利用特征的周期性并减少其建模误差。在本食谱中,我们将从捕获时间周期性的周期性变量中创建新特征。
准备工作
三角函数,如正弦和余弦,是周期性的,其值在每个 2π周期内循环于-1 和 1 之间,如下所示:

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

将变量的值除以其最大值将使其在 0 和 1 之间归一化(假设最小值为 0),然后乘以 2π将变量重新缩放到 0 和 2π之间。
我们应该使用正弦函数吗?还是应该使用余弦函数?问题是,我们需要两者并用以无歧义地编码变量的所有值。由于正弦和余弦函数在 0 和 1 之间循环,它们将对于多个 x 值取 0。例如,0 的正弦值为 0,π的正弦值也为 0。所以,如果我们只用正弦函数来编码变量,我们就无法再区分 0 和π的值了。然而,由于正弦和余弦函数相位不同,0 的余弦值为 1,而π的余弦值为-1。因此,通过使用两个函数来编码变量,我们现在能够区分 0 和 1,正弦函数和余弦函数分别以(0,1)和(0,-1)作为值。
如何做到这一点...
在这个菜谱中,我们将首先使用正弦和余弦将玩具 DataFrame 中的hour变量进行变换,以了解新的变量表示。然后,我们将使用feature-engine自动化从多个周期性变量中创建特征:
-
首先导入必要的库:
import numpy as np import pandas as pd import matplotlib.pyplot as plt -
创建一个包含一个变量 –
hour– 且值在 0 到 23 之间的玩具 DataFrame:df = pd.DataFrame([i for i in range(24)], columns=["hour"]) -
接下来,在将变量值归一化到 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
-
在小时和其正弦变换值之间制作散点图:
plt.scatter(df["hour"], df["hour_sin"]) plt.ylabel("Sine of hour") plt.xlabel("Hour") plt.title("Sine transformation")在下面的图中,我们可以看到小时值的范围在-1 和 1 之间,就像变换后的正弦函数一样:

图 8.13 – 小时与其正弦变换值的散点图
-
现在,在小时和其余弦变换之间制作散点图:
plt.scatter(df["hour"], df["hour_cos"]) plt.ylabel("Cosine of hour") plt.xlabel("Hour") plt.title("Cosine transformation")在下面的图中,我们可以看到小时值的范围在-1 和 1 之间,就像变换后的余弦函数一样:

图 8.14 – 小时与其余弦变换值的散点图
最后,我们可以重建小时的周期性,现在它被两个新的特征所捕捉。
-
绘制正弦值与小时余弦值的对比图,并使用颜色图叠加小时的原值:
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 – 小时的三角变换散点图
注意
这个图表的代码实现和思路来源于 scikit-learn 的文档:scikit-learn.org/stable/auto_examples/applications/plot_cyclical_feature_engineering.html#trigonometric-features。
现在我们已经了解了变换的性质和效果,让我们使用正弦和余弦变换自动从多个变量中创建新特征。我们将使用feature-engine库的CyclicalFeatures()。
-
导入
CyclicalFeatures():from feature_engine.creation import CyclicalFeatures -
让我们创建一个包含
hour、month和week变量的玩具 DataFrame,这些变量的值分别在 0 到 23、1 到 12 和 0 到 6 之间:df = pd.DataFrame() df["hour"] = pd.Series([i for i in range(24)]) df["month"] = pd.Series([i for i in range(1, 13)]*2) df["week"] = pd.Series([i for i in range(7)]*4)如果我们执行
df.head(),我们将看到玩具 DataFrame 的前五行:

图 8.16 – 具有三个周期特征的玩具 DataFrame
-
设置转换器以从这些变量中创建正弦和余弦特征:
cyclic = CyclicalFeatures( variables=None, drop_original=False, )
注意
通过将variables设置为None,CyclicalFeatures()将从所有数值变量中创建三角特征。要创建变量子集的三角特征,我们可以将变量名列表传递给variables参数。在创建周期特征后,我们可以使用drop_original参数保留或删除原始变量。
-
最后,将特征添加到 DataFrame 中,并将结果捕获在新变量中:
dft = cyclic.fit_transform(df)如果我们执行
dft.head(),我们将看到原始特征和新特征:

图 8.17 – 具有周期特征以及通过正弦和余弦函数创建的特征的 DataFrame
就这样 – 我们通过使用正弦和余弦变换自动从多个变量中创建特征,并将它们直接添加到原始 DataFrame 中。
它是如何工作的…
在这个菜谱中,我们使用正弦和余弦函数从变量的归一化值中获取值来编码周期特征。首先,我们将变量值归一化到 0 和 2π之间。为此,我们用pandas.max()获取变量最大值,将变量值除以变量最大值,以将变量缩放到 0 和 1 之间。然后,我们使用numpy.pi将这些值乘以 2π。最后,我们使用np.sin和np.cos分别应用正弦和余弦变换。
为了自动化对多个变量的此过程,我们使用了Feature-engine库的CyclicalFeatures()。通过fit(),转换器学习了每个变量的最大值,通过transform(),它将正弦和余弦变换产生的特征添加到 DataFrame 中。
注意
理论上,为了应用正弦和余弦变换,我们需要将原始变量缩放到 0 到 1 之间。如果最小值是 0,则除以变量的最大值只会得到这种缩放。scikit-learn 的文档和Feature-engine当前的实现将变量除以其最大值(或任意周期),并没有过多关注变量是否从 0 开始。在实践中,如果你将小时特征除以 23 或 24,或者将月份特征除以 12 或 11,你不会在结果变量中看到很大的差异。目前正在讨论是否应该更新 Feature-engine 的实现,因此默认行为可能会在本书出版时发生变化。有关更多详细信息,请参阅文档。
创建样条特征
线性模型期望预测变量和目标之间存在线性关系。然而,如果我们首先转换特征,我们可以使用线性模型来模拟非线性效应。在“执行多项式展开”的食谱中,我们看到了如何通过创建多项式函数的特征来揭示线性模式。在本食谱中,我们将讨论样条的使用。
样条用于在数学上再现灵活的形状。它们由分段低次多项式函数组成。要创建样条,我们必须在x的几个值上放置节点。这些节点表示函数片段的连接点。然后,我们在两个连续节点之间拟合低次多项式。
有几种样条类型,例如平滑样条、回归样条和 B 样条。scikit-learn 支持使用 B 样条来创建特征。根据多项式度和节点数来拟合样条值,对于特定变量的过程超出了本食谱的范围。有关更多详细信息,请参阅本食谱的“也见”部分中的资源。在本食谱中,我们将了解样条是什么以及我们如何使用它们来提高线性模型的表现。
准备工作
让我们了解样条是什么。在下面的图中,左侧我们可以看到一个一阶样条。它由两个线性片段组成 – 一个从 2.5 到 5,另一个从 5 到 7.5。有三个节点 – 2.5,5 和 7.5。在 2.5 和 7.5 之间的区间外,样条取值为 0。这是样条的特征;它们只在某些值之间为非负。在图的右侧面板中,我们可以看到三个一阶样条。我们可以通过引入更多的节点来构建我们想要的任意数量的样条:

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

图 8.19 – 二次样条
我们可以使用样条来建模非线性函数,我们将在下一节中学习如何做到这一点。
如何做到这一点…
在这个菜谱中,我们将使用样条来建模正弦函数。一旦我们了解样条是什么以及我们如何通过线性模型拟合非线性关系,我们将在实际数据集中使用样条进行回归:
注意
将样条建模正弦函数的想法来自 scikit-learn 的文档:scikit-learn.org/stable/auto_examples/linear_model/plot_polynomial_interpolation.html。
-
让我们先导入必要的库和类:
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.linear_model import Ridge from sklearn.preprocessing import SplineTransformer -
创建一个包含-1 和 11 之间 20 个值的训练集
X,以及目标变量y,它是X的正弦值:X = np.linspace(-1, 11, 20) y = np.sin(X) -
绘制
X和y之间的关系图:plt.plot(X, y) plt.ylabel("y") plt.xlabel("X")在下面的图中,我们可以看到
X的正弦函数:

图 8.20 – 预测变量和目标变量之间的关系,其中 y = sine(x)
-
通过使用岭回归预测
y来自X,并获取模型的预测结果:linmod = Ridge(random_state=10) linmod.fit(X.reshape(-1, 1), y) pred = linmod.predict(X.reshape(-1, 1)) -
现在,绘制
X和y之间的关系图,并叠加预测结果: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")在下面的图中,我们可以看到线性模型对
X和y之间的非线性关系拟合得非常差:

图 8.21 – X 和 y 之间的线性拟合
-
现在,设置
SplineTransformer()以从X中获取样条特征,通过使用X值内的三次多项式和五个等距结点:spl = SplineTransformer(degree=3, n_knots=5) -
获取样条特征,并将 NumPy 数组转换为
pandas数据框,添加样条基函数的名称:X_t = spl.fit_transform(X.reshape(-1, 1)) X_df = pd.DataFrame( X_t, columns=spl.get_feature_names_out(["var"]) )通过执行
X_df.head(),我们可以看到样条特征:

图 8.22 – 包含样条的数据框
注意
SplineTransformer()返回一个特征矩阵,由n_splines = n_knots + degree – 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 的值绘制的图
-
现在,通过使用从
X获取的样条特征,利用岭回归拟合线性模型以预测y,然后获取模型的预测结果:linmod = Ridge(random_state=10) linmod.fit(X_t, y) pred = linmod.predict(X_t) -
现在,绘制
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 之间真实关系的线性模型的预测
注意
增加结点的数量或多项式的度数会增加样条曲线的灵活性。尝试从更高次的多项式创建样条,看看岭回归预测如何变化。
现在我们已经了解了样条特征是什么以及我们如何使用它们来预测非线性效应,让我们在一个真实的数据集上尝试一下。
-
从
scikit-learn导入一些额外的类和函数:from sklearn.datasets import fetch_california_housing from sklearn.compose import ColumnTransformer from sklearn.model_selection import cross_validate -
加载加利福尼亚住房数据集并删除两个变量,我们不会用它们进行建模:
X, y = fetch_california_housing( return_X_y=True, as_frame=True) X.drop(["Latitude", "Longitude"], axis=1, inplace=True) -
首先,我们将拟合一个岭回归来根据现有变量预测房价,通过使用交叉验证,然后获取模型的性能以设置基准:
linmod = Ridge(random_state=10) cv = cross_validate(linmod, X, y) mean_, std_ = np.mean( cv[«test_score"]), np.std(cv["test_score"]) print(f"Model score: {mean_} +- {std_}")在下面的输出中,我们可以看到模型性能,其中数值是 R 平方:
SplineTransformer() to obtain spline features from four variables by utilizing third-degree polynomials and 50 knots, and then fit the pipeline to the data:spl = SplineTransformer(degree=3, n_knots=50)
ct = ColumnTransformer(
[("splines", spl, [
"平均房间数"、"平均卧室数"、"人口",
"平均占用人数"
)],
remainder="passthrough",
)
ct.fit(X, y)
注意
记住,我们需要使用ColumnTransformer()来从数据的一组变量中获取特征。通过remainder=passthrough,我们确保那些不作为样条模板的变量——即MedInc和HouseAge——也被返回到结果 DataFrame 中。要检查这一步产生的特征,请执行ct.get_feature_names_out()。
-
现在,拟合一个岭回归来根据
MedInc、HouseAge和样条特征预测房价,使用交叉验证,然后获取模型的性能:cv = cross_validate(linmod, ct.transform(X), y) mean_, std_ = np.mean( cv[«test_score"]), np.std(cv["test_score"]) print(f"Model score: {mean_} +- {std_}")在下面的输出中,我们可以看到模型性能,其中数值是 R 平方:
Model score: 0.5553526813919297 +- 0.02244513992785257
如我们所见,通过使用样条代替一些原始变量,我们可以提高线性回归模型的性能。
它是如何工作的…
在这个菜谱中,我们基于样条创建了新的特征。首先,我们使用一个玩具变量,其值从-1 到 11,然后我们从真实数据集中获取样条。在这两种情况下,过程是相同的——我们使用了scikit-learn中的SplineTransformer()。SplineTransformer()转换器接受多项式的degree属性和结点的数量(n_knots)作为输入,并返回更好地拟合数据的样条。结点默认放置在X的等距值上,但通过knots参数,我们可以选择将它们均匀分布到X的十分位数上,或者我们可以传递一个包含应用作结点的X的特定值的数组。
注意
节点的数量、间距和位置由用户任意设置,这些是影响样条曲线形状的最主要参数。当在回归模型中使用样条曲线时,我们可以通过交叉验证的随机搜索来优化这些参数。
使用 fit() 函数,转换器计算样条曲线的节点。使用 transform() 函数,它返回 B 样条曲线的数组。转换器返回 n_splines=n_knots + degree – 1。
记住,像大多数 scikit-learn 转换器一样,SplineTransformer() 现在也有选项返回 pandas 和 polars DataFrames,除了 NumPy 数组之外,这种行为可以通过 set_output() 方法进行修改。
最后,我们使用 ColumnTransformer() 从特征子集中导出样条曲线。因为我们把 remainder 设置为 passthrough,所以 ColumnTransformer() 将未用于获取样条曲线的特征连接到结果矩阵中。通过这种方式,我们使用样条曲线、MedInc 和 HouseAge 变量拟合了岭回归,并成功提高了线性模型的表现。
参见
要了解更多关于 B 样条背后的数学知识,请查看以下文章:
-
Perperoglou 等人. R 中样条函数过程综述 (
bmcmedresmethodol.biomedcentral.com/articles/10.1186/s12874-019-0666-3)。BMC Med Res Methodol 19, 46 (2019)。 -
Eilers 和 Marx. 使用 B 样条和惩罚进行灵活平滑 (
projecteuclid.org/journals/statistical-science/volume-11/issue-2/Flexible-smoothing-with-B-splines-and-penalties/10.1214/ss/1038425655.full)。 -
要了解如何使用 B 样条对时间序列数据进行建模的示例,请查看
scikit-learn库文档中的以下页面:scikit-learn.org/stable/auto_examples/applications/plot_cyclical_feature_engineering.html#periodic-spline-features。
第九章:使用 Featuretools 从关系数据中提取特征
在前面的章节中,我们处理了以行和列组织的数据,其中列是变量,行是观察值,每个观察值都是独立的。在本章中,我们将专注于从关系数据集中创建特征。在关系数据集中,数据结构跨越各种表,可以通过唯一标识符进行连接。这些唯一标识符表明不同表之间存在的关系。
关系数据的经典例子是零售公司持有的数据。一张表包含有关客户的信息,例如姓名和地址。第二张表包含有关客户所进行的购买的信息,例如每次购买的物品类型和数量。第三张表包含有关客户与公司网站互动的信息,例如会话持续时间、使用的移动设备和访问的页面。客户、购买和会话通过唯一标识符进行识别。这些唯一标识符使我们能够将这些表放在一起,这样我们就可以获取有关客户购买或会话的信息。
如果我们想了解更多关于我们拥有的客户类型(即客户细分)或者预测他们是否会购买产品,我们可以创建在客户层面跨不同表汇总或总结信息的特征。例如,我们可以创建捕捉客户在购买中花费的最大金额、他们所进行的购买次数、会话之间的时间或平均会话持续时间的特征。我们可以创建的特征数量以及我们可以在表中跨表汇总数据的不同方式是丰富的。在本章中,我们将讨论使用featuretools Python 库创建关系数据聚合视图的一些常见方法。我们将从设置各种数据表及其关系并自动创建特征开始,然后我们将详细介绍我们可以创建的不同特征。
在本章中,我们将涵盖以下食谱:
-
设置实体集并自动创建特征
-
使用通用和累积操作创建特征
-
组合数值特征
-
从日期和时间中提取特征
-
从文本中提取特征
-
使用聚合原语创建特征
技术要求
在本章中,我们将使用pandas、matplotlib和featuretools开源 Python 库。您可以使用pip安装featuretools:
pip install featuretools
此外,您还可以使用conda:
conda install -c conda-forge featuretools
这些命令安装了基本的 featuretools 功能,但我们可以安装用于使用 dask 作为后端而不是 pandas 的附加组件。有关如何安装 featuretools 附加组件(包括 graphviz)的更多信息,请参阅他们的文档:docs.featuretools.com/en/v0.16.0/getting_started/install.html。
我们将使用来自 UCI 机器学习仓库的 Online Retail II 数据集,该数据集可在 archive.ics.uci.edu/ml/datasets/Online+Retail+II 找到,并受 Creative Commons Attribution 4.0 国际(CC BY 4.0)许可协议的约束:creativecommons.org/licenses/by/4.0/legalcode。该数据的相应引用如下:Chen, Daqing(2019)。Online Retail II。UCI Machine Learning Repository (doi.org/10.24432/C5CG6D)。
我已下载并修改了如本笔记本所示的数据:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch09-featuretools/prepare-retail-dataset.ipynb
你可以在附带的 GitHub 仓库中找到修改后的数据集:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch09-featuretools/retail.csv
设置实体集和自动创建特征
关系型数据集或数据库包含分散在多个表中的数据,表之间的关系由一个唯一标识符决定,它告诉我们如何连接这些表。为了使用 featuretools 自动创建特征,我们首先需要输入不同的数据表,并在所谓的 featuretools 中建立它们之间的关系,以便库可以自动根据这些关系创建特征。
我们将使用包含客户、发票和产品信息的数据集。首先,我们将设置一个实体集,突出这三个项目之间的关系。这个实体集将是本章剩余菜谱的起点。接下来,我们将通过在客户、发票和产品级别汇总数据,利用 featuretools 的默认参数自动创建特征。
在本菜谱中,你将学习如何正确设置实体集并自动为每个实体提取一系列特征。在接下来的菜谱中,我们将深入探讨我们可以使用 featuretools 创建的不同类型的特征。
准备就绪
在这个食谱中,我们将使用来自 UCI 机器学习仓库的在线零售 II数据集。在这个表中,有客户,他们是批量从零售公司购买的企业。客户通过一个唯一的customer_id标识符来识别。每位客户进行一次或多次购买,这些购买通过一个唯一的invoice标识符标记,包含发票号。在每张发票中,都有客户购买的一个或多个项目。公司销售的每个项目或产品也通过一个唯一的库存代码来识别。
因此,数据有以下关系:

图 9.1 – 显示数据中关系的图
每位客户进行了一次或多次购买,由发票号识别。每张发票包含一个或多个项目,由库存代码识别。每个项目可以被一个或多个客户购买,因此出现在多个发票中。考虑到这些关系,让我们继续进行食谱。
如何操作...
在这个食谱中,我们将使用数据设置一个实体集,然后突出显示数据集中的不同关系。最后,我们将通过在客户、发票和产品级别对数据集中的信息进行聚合来创建特征:
-
让我们导入所需的库:
import pandas as pd import featuretools as ft from woodwork.logical_types import Categorical -
让我们加载在准备就绪部分描述的零售数据集,并显示其前五行:
df = pd.read_csv( «retail.csv», parse_dates=[«invoice_date»]) df.head()在下面的屏幕截图中,我们可以看到客户(
customer_id)和发票(invoice)的唯一标识符,以及关于每张发票购买的项目(如项目的代码stock_code)、描述、数量、单价以及发票日期的附加信息:

图 9.2 – 在线零售 II 数据集
注意
使用pandas的unique()函数来识别唯一的项目、客户和发票的数量——例如,通过执行df["customer_id"].nunique()。
-
让我们用一个任意的名字初始化一个实体集,例如
data:es = ft.EntitySet(id="data") -
让我们在实体集中添加一个 DataFrame;我们给 DataFrame 起一个名字(
data)。我们需要为每一行添加一个唯一标识符,我们称之为rows,由于在这个数据集中我们没有唯一的行标识符,我们将通过设置make_index=True创建它作为额外的列。最后,我们指出invoice_date是datetime类型,customer_id应被视为Categorical:es = es.add_dataframe( dataframe=df, dataframe_name=»data», index="rows", make_index=True, time_index=»invoice_date», logical_types={ «customer_id»: Categorical}, ) -
接下来,我们添加原始
dataDataFrame 和invoices之间的关系。为此,我们指出原始或基本 DataFrame,我们在步骤 4中将其称为data,我们给新的 DataFrame 起一个名字,invoices,我们添加发票的唯一标识符,并将包含customer_id的列添加到这个 DataFrame 中:es.normalize_dataframe( base_dataframe_name=»data», new_dataframe_name=»invoices», index="invoice", copy_columns=[«customer_id»], )
注意
我们将customer_id变量复制到invoices数据中,因为我们想创建客户和发票之间的后续关系。
-
现在,我们添加第二个关系,即客户与发票之间的关系。为此,我们指示基础 DataFrame,我们在步骤 5中将其命名为
invoices,然后我们为新 DataFrame 命名,命名为customers,并添加唯一的客户标识符:es.normalize_dataframe( base_dataframe_name=»invoices», new_dataframe_name=»customers», index=»customer_id», ) -
我们可以在原始数据和产品之间添加第三个关系:
es.normalize_dataframe( base_dataframe_name=»data», new_dataframe_name=»items», index=»stock_code», ) -
让我们显示实体集中的信息:
es在以下输出中,我们看到实体集包含四个 DataFrame:原始数据、
invoicesDataFrame、customersDataFrame 以及产品或itemsDataFrame。实体还包含发票或项目与原始数据之间的关系,以及客户与发票之间的关系:Entityset: data DataFrames: data [Rows: 741301, Columns: 8] invoices [Rows: 40505, Columns: 3] customers [Rows: 5410, Columns: 2] items [Rows: 4631, Columns: 2] Relationships: data.invoice -> invoices.invoice invoices.customer_id -> customers.customer_id invoices DataFrame:es["invoices"].head()
We see in the following output that `featuretools` automatically created a DataFrame containing the invoice’s unique identifier, followed by the customer’s unique identifier and the first date registered for each invoice:

图 9.3 – 发票级别的 DataFrame 信息
-
现在我们显示
customersDataFrame:es["customers"].head()在以下输出中,我们可以看到
featuretools自动创建了一个包含客户唯一标识符的 DataFrame,随后是此客户的第一个发票日期:

图 9.4 – 客户级别的 DataFrame 信息
注意
继续执行es["items"].head()来显示包含产品的 DataFrame。您还可以使用pandas的shape函数评估不同 DataFrame 的大小。您会注意到每个 DataFrame 中的行数与唯一发票、客户和产品的数量相匹配。
-
我们还可以如下显示这些数据表之间的关系:
es.plot()
注意
要可视化数据关系,您需要安装graphviz。如果没有安装,请按照featuretools文档中的说明进行安装:featuretools.alteryx.com/en/stable/install.html#installing-graphviz。
在以下输出中,我们可以看到关系数据集及其关系:

图 9.5 – 包含发票、客户和产品的表之间的关系
在输入数据和它们之间的关系后,我们可以开始自动为我们的新 DataFrame(即客户、发票和产品)创建特征,使用featuretools的默认参数。
-
让我们通过在客户级别聚合数据来创建特征。为此,我们设置
featuretools,将customers指定为目标 DataFrame。在创建特征时,我们希望忽略具有唯一标识符的两个列:feature_matrix, feature_defs = ft.dfs( entityset=es, target_dataframe_name=»customers», ignore_columns={ «invoices»:[«invoice»], «invoices»:[«customer_id»], } )
注意
步骤 12 中的命令触发了 114 个具有不同客户级别数据聚合的特征的创建。feature_matrix 变量是一个包含特征值的 DataFrame,feature_defs 是一个包含新特征名称的列表。继续执行 feature_defs 或访问我们的配套 GitHub 仓库 (github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch09-featuretools/Recipe1-Setting-up-an-entitity-set.ipynb) 来检查创建的特征名称。你将在 如何工作… 部分找到更多关于这些特征的信息。
-
由于空间限制,我们无法在书中打印出所有特征,因此,让我们显示五个创建特征的名称:
feature_defs[5:10]在以下输出中,我们看到由
featuretools创建的 114 个特征中的 5 个:[<Feature: MIN(data.price)>, <Feature: MIN(data.quantity)>, <Feature: MODE(data.description)>, <Feature: MODE(data.stock_code)>, <Feature: NUM_UNIQUE(data.description)>]
注意
featuretools 库使用创建它们的函数来命名新特征,然后是用于执行聚合的 DataFrame,最后是聚合变量名称。因此,MIN(data.quantity) 等同于 df.groupby(["customer_id"])["quantity"].min(),如果你熟悉 pandas。我们将在 如何工作… 部分提供更多细节。
-
让我们显示包含五个创建特征的 DataFrame 的前五行:
feature_matrix[feature_matrix.columns[5:10]].head()在以下输出中,我们可以看到包含五个新特征值的前五行:

图 9.6 – 通过聚合客户级别的数据创建的五个特征的 DataFrame
-
同样,我们可以通过在发票级别聚合信息来自动创建特征:
feature_matrix, feature_defs = ft.dfs( entityset=es, target_dataframe_name=»invoices», ignore_columns = {«data»: [«customer_id»]}, max_depth = 1, ) -
上一步返回了 24 个特征——让我们显示它们的名称:
feature_defs我们可以在以下输出中看到特征的名称:
[<Feature: customer_id>, <Feature: COUNT(data)>, <Feature: MAX(data.price)>, <Feature: MAX(data.quantity)>, <Feature: MEAN(data.price)>, <Feature: MEAN(data.quantity)>, <Feature: MIN(data.price)>, <Feature: MIN(data.quantity)>, <Feature: MODE(data.description)>, <Feature: MODE(data.stock_code)>, <Feature: NUM_UNIQUE(data.description)>, <Feature: NUM_UNIQUE(data.stock_code)>, <Feature: SKEW(data.price)>, <Feature: SKEW(data.quantity)>, <Feature: STD(data.price)>, <Feature: STD(data.quantity)>, <Feature: SUM(data.price)>, <Feature: SUM(data.quantity)>, <Feature: DAY(first_data_time)>, <Feature: MONTH(first_data_time)>, <Feature: WEEKDAY(first_data_time)>, <Feature: YEAR(first_data_time)>]
注意
继续执行 feature_matrix.head() 来显示包含新特征的 DataFrame,或检查我们的配套 GitHub 仓库以查看结果。
总结一下,通过使用 步骤 16 中的代码并将目标 DataFrame 名称从 invoices 更改为 items,继续在产品级别自动创建特征。
如何工作...
在这个配方中,我们设置了一个包含数据和某些变量(唯一标识符)之间关系的实体集。之后,我们通过聚合数据集中每个唯一标识符的信息自动创建特征。我们使用了 featuretools 的两个主要类,EntitySet 和 dfs,来创建特征。让我们更详细地讨论这些内容。
EntitySet 类存储数据、变量的逻辑类型以及变量之间的关系。变量类型(是否为数值或分类)由 featuretools 自动分配。我们也可以在将 DataFrame 添加到实体集时设置特定的变量类型。在 步骤 4 中,我们将数据添加到实体集并将 customer_id 的逻辑类型设置为 Categorical。
注意
要检查 featuretools 推断的数据类型,您可以执行 es["data"].ww,其中 es 是实体集,data 是 DataFrame 的名称。
EntitySet 类具有 add_dataframe 方法,我们在 步骤 4 中使用此方法添加新的 DataFrame。在使用此方法时,我们需要指定唯一标识符,如果没有,则需要创建一个,就像我们在 步骤 4 中所做的那样,通过将 make_index 设置为 True。请注意,在 add_dataframe 的 index 参数中,我们传递了 "rows" 字符串。使用此配置,EntitySet 将包含每行的唯一标识符的 rows 列添加到 DataFrame 中,这是一个从 0 开始的新整数序列。
注意
而不是使用 add_dataframe 方法将 DataFrame 添加到实体集中,我们可以通过执行 es["df_name"]=df 来添加它,其中 "df_name" 是我们想要给 DataFrame 的名称,df 是我们想要添加的 DataFrame。
EntitySet 类具有 normalize_dataframe 方法,该方法用于从现有列的唯一值创建新的 DataFrame 和关系。此方法接受新 DataFrame 将关联的 DataFrame 的名称以及新 DataFrame 的名称。我们还需要在 index 参数中指明新 DataFrame 的唯一标识符。默认情况下,此方法创建一个包含唯一标识符的新 DataFrame,后面跟着一个包含每个唯一标识符首次注册的日期的 datetime 列。我们可以通过使用 copy_columns 参数向此 DataFrame 添加更多列,就像我们在 步骤 5 中所做的那样。向新 DataFrame 添加更多列对于如果我们想跟踪与该新 DataFrame 的关系是有用的,就像我们在 步骤 6 中所做的那样。
EntitySet 类还具有 plot() 方法,该方法显示实体集中的现有关系。在 图 9**.5 中,我们看到了我们的数据表之间的关系;invoices 和 items(产品)表与原始数据相关联,而 customers 表与 invoices 表相关联,而 invoices 表又与原始数据相关联。
注意
表之间的关系决定了特征的创建方式。invoices和items表与原始数据相关。因此,我们只能创建深度为 1 的特征。另一方面,customers表与发票相关,而发票又与数据相关。因此,我们可以创建深度为 2 的特征。这意味着新特征将包括整个数据集的聚合或首先对发票进行聚合,然后对客户进行后续聚合。我们可以通过dfs中的max_depth参数来调节要创建的特征。
在设置好数据和关系后,我们使用了featuretools中的dfs来自动创建特征。在用dfs创建特征时,我们需要设置目标 DataFrame——即应该创建特征的对应数据表。dfs类通过所谓的转换和聚合原语,通过转换和聚合现有变量来创建特征。
转换原语转换变量。例如,使用转换原语,dfs可以从日期时间变量中提取month、year、day和week值。
聚合原语对唯一标识符的信息进行聚合。它使用诸如平均值、标准差、最大值和最小值、总和以及数值变量的偏度系数等数学运算。对于分类变量,聚合原语使用众数和唯一项目的计数。对于唯一标识符,它们计算出现的次数。
考虑到转换和聚合原语的功能,让我们尝试理解在这个配方中创建的特征。我们使用了dfs的默认参数来创建默认特征。
注意
想要了解更多关于featuretools返回的默认特征的信息,请访问featuretools.alteryx.com/en/stable/generated/featuretools.dfs.html#featuretools.dfs。
我们首先为每位客户创建了特征。featuretools为每位客户返回了 114 个特征。由于customers数据与invoices数据相关,而invoices数据又与整个数据集相关,因此特征是通过在两个级别上聚合数据来创建的。首先,使用整个数据集对每个客户的数据进行聚合。接下来,首先对每个发票进行聚合,然后对预先聚合的数据再次进行聚合,以创建每个客户的特征。
featuretools 库使用用于聚合数据的函数来命名新特征 – 例如,COUNT、MEAN、STD 和 SKEW 等。接下来,它使用用于聚合的数据,并跟随聚合的变量。例如,MEAN(data.quantity) 特征包含从整个数据集中计算出的客户购买物品的平均数量,这相当于如果你熟悉 pandas,则是 df.groupby("customer_id")["quantity"].mean()。另一方面,MEAN(invoices.MEAN(data.quantity)) 特征首先获取每个发票的物品平均数量 – 即 df.groupby("invoice")["quantity"].mean() – 然后从生成的序列中获取平均值,考虑特定客户的发票。
对于分类特征,featuretools 确定众数和唯一值。例如,从 description 变量中,我们得到了 NUM_UNIQUE(data.description) 和 MODE(data.description) 特征。描述只是物品的名称。因此,这些特征突出了客户购买的独特物品数量以及客户购买次数最多的物品。
注意一些有趣的事情
在对分类特征进行聚合后,NUM_UNIQUE(data.description) 和 MODE(data.description) 变量是数值型的。featuretools 库通过使用这些新创建变量的数值聚合来创建更多特征。这样,MAX(invoices.NUM_UNIQUE(data.description)) 特征首先找到每个发票的独特物品数量,然后返回特定客户这些值中的最大值,考虑所有客户的发票。
从日期时间特征中,featuretools 默认提取日期组件。记住,customers DataFrame 包含 customer_id 变量和每个客户的第一个发票日期,正如我们在 步骤 10 的输出中看到的。从这个日期时间特征中,featuretools 创建了包含不同日期部分的 DAY(first_invoices_time)、MONTH(first_invoices_time)、WEEKDAY(first_invoices_time) 和 YEAR(first_invoices_time) 特征。
最后,featuretools 还返回了每个客户的发票总数(COUNT(invoices)) 和每客户的总行数(COUNT(data))。
参见
要了解更多关于 featuretools 的灵感来源,请查看 Kanter 和 Veeramachaneni 在 www.jmaxkanter.com/papers/DSAA_DSM_2015.pdf 发表的原始文章 Deep Feature Synthesis: Towards Automating Data Science Endeavors。
使用通用和累积操作创建特征
featuretools 库使用所谓的 转换原语 来创建特征。转换原语将一个或多个数据集中的列作为输入,并返回一个或多个列作为输出。它们应用于一个 单个 DataFrame。
featuretools库根据它们执行的操作类型或修改的变量类型将转换原语分为各种类别。例如,通用转换原语应用数学运算,如平方根、正弦和余弦。累积转换原语通过比较一行值与前一行的值来创建新特征。例如,累积和、累积平均值、累积最小值和最大值以及行值之间的差异属于这一类别。还有一个可以应用于日期时间变量的累积转换,即自上次以来转换,它确定两个连续时间戳之间的时间流逝。
在这个配方中,我们将使用featuretools中的通用和累积转换原语来创建特征。
准备工作
当我们想要改变变量的分布,就像我们在第三章中看到的,转换数值变量时,变量转换,如平方根或对数是有用的。其他数学推导,如正弦和余弦,有助于捕捉潜在的数据模式,正如我们在第八章中描述的从周期性变量创建周期性特征配方中所述,创建新特征。从那些章节中描述的转换中,featuretools支持平方根和对数转换以及正弦和余弦(但不需要在 0 和 2π之间归一化)。
通过累积转换,例如,我们可以通过将发票级别的每一行的项目数量相加来获取每个发票购买的项目总数。为了理解我们将在此配方中创建的特征,让我们首先使用pandas创建它们:
-
让我们导入
pandas和numpy:import numpy as np import pandas as pd -
让我们加载在技术 要求部分中描述的零售数据集:
df = pd.read_csv( «retail.csv», parse_dates=[«invoice_date»]) -
让我们捕捉两个数值变量,
价格和数量,到一个列表中:numeric_vars = ["quantity", "price"] -
让我们将累积函数的名称捕捉到一个列表中:
func = ["cumsum", "cummax", "diff"] -
让我们创建一个包含我们将创建的变量新名称的列表:
new_names = [f"{var}_{function}" for function in func for var in numeric_vars] -
让我们使用第 4 步中的累积函数,应用于第 3 步中的变量,并添加到 DataFrame 中:
df[new_names] = df.groupby( "invoice")[numeric_vars].agg(func)上一步返回了每个发票内的累积和、累积最大值以及行之间的差异。一旦遇到新的发票编号,它就会重新开始。
-
让我们展示一个特定发票的原有和新特征:
df[df["invoice"] == "489434" ][ numeric_vars + new_names]在以下输出中,我们可以看到
quantity_cumsum是数量的累积和,而price_diff是逐行后的价格差异:

图 9.7 - 显示对单个实体(发票)中的数值特征应用累积函数的 DataFrame
现在让我们将正弦和余弦转换应用于整个 DataFrame。
-
让我们创建一个新变量名称的列表:
new_names = [ f"{var}_{function}" for function in ["sin", "cos"] for var in numeric_vars] -
让我们使用正弦和余弦函数转换价格和数量:
df[new_names] = df[numeric_vars].agg( [np.sin, np.cos])步骤 9 中的转换被应用于整个数据集,无论发票号如何,这是可以的,因为它是从一行映射到同一行,而不是像累积函数那样从一行映射到下一行。您可以通过执行
df[new_names].head()来检查结果。
现在我们已经了解了我们想要创建的特征类型,让我们使用featuretools自动化这个过程。
如何做到这一点...
我们将对每个发票应用累积转换,对整个数据集应用通用转换:
-
首先,我们将导入
pandas、featuretools以及Categorical逻辑类型:import pandas as pd import featuretools as ft from woodwork.logical_types import Categorical -
让我们加载技术要求部分中描述的数据集:
df = pd.read_csv( «retail.csv», parse_dates=[«invoice_date»]) -
让我们设置一个实体集:
es = ft.EntitySet(id="data") -
让我们将 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, } )
注意
默认情况下,featuretools仅在生成的特征矩阵中保留分类、数值和布尔特征,该矩阵是在创建新特征之后生成的。invoice变量的类型没有被准确推断,所以如果我们想让featuretools在包含新特征的数据集中保留它,我们需要通过将其逻辑类型设置为我们在步骤 4 中做的那样来强制将其作为分类。要了解featuretools推断的数据类型,您可以执行es["data"].ww。
-
让我们创建一个新的与步骤 4 中的 DataFrame 相关联的 DataFrame:
es.normalize_dataframe( base_dataframe_name=»data», new_dataframe_name=»invoices», index="invoice", copy_columns=[«customer_id»], )
注意
关于步骤 4 和 5 的更多详细信息,请访问设置实体集和自动创建特征菜谱。
-
让我们列出我们将用于创建特征的累积转换:
cum_primitives = [ "cum_sum", "cum_max", "diff", "time_since_previous"]
注意
您可以在以下链接找到featuretools支持的累积转换:featuretools.alteryx.com/en/stable/api_reference.html#cumulative-transform-primitives
-
让我们列出要执行的一般转换:
general_primitives = ["sine", " cosine "]
注意
您可以在以下链接找到featuretools支持的通用转换:featuretools.alteryx.com/en/stable/api_reference.html#general-transform-primitives
-
最后,让我们创建特征。我们使用
dfs类,将原始 DataFrame 设置为目标 DataFrame——即我们将使用其变量作为新特征模板的那个 DataFrame。注意,我们向agg_primitives参数传递一个空列表;这是为了避免返回默认的聚合原语。我们将步骤 7 中的通用原语传递给trans_primitives参数,将步骤 6 中的累积原语传递给groupby_trans_primitives参数: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 触发特征的创建,这可能会根据数据的大小、聚合级别数量以及要创建的特征数量而花费一些时间。你可以在创建它们之前检查输出特征,通过将features_only参数设置为True。这将仅返回特征名称;你可以检查它们,确保它们显示了你需要的内容,然后通过将参数设置回False来触发特征合成。
-
让我们现在显示创建的特征名称:
feature_defs在以下输出中,我们看到我们创建的特征名称,包括价格和数量的正弦和余弦,以及按发票号分组后的这些变量的累积变换:
[<Feature: customer_id>, <Feature: invoice>, <Feature: stock_code>, <Feature: description>, <Feature: quantity>, <Feature: price>, <Feature: COSINE(price)>, <Feature: COSINE(quantity)>, <Feature: SINE(price)>, <Feature: SINE(quantity)>, <Feature: CUM_MAX(price) by invoice>, <Feature: CUM_MAX(quantity) by invoice>, <Feature: CUM_SUM(price) by invoice>, <Feature: CUM_SUM(quantity) by invoice>, <Feature: DIFF(price) by invoice>, <Feature: DIFF(quantity) by invoice>, <Feature: TIME_SINCE_PREVIOUS(invoice_date) by invoice>]
注意
价格和数量的正弦和余弦变换可能不会增加太多价值,因为这些不是周期性特征。我保留这些变换在配方中,是为了向你展示如何在需要时应用变换原语。
如前所述列表所示,新特征作为新列附加到原始 DataFrame 中。你可以通过执行feature_matrix.head()来显示最终的 DataFrame:

图 9.8 – 深度特征合成产生的 DataFrame,包含原始变量和新特征
关于创建的特征的更多详细信息,请查看它是如何工作的…部分。
它是如何工作的...
要使用featuretools创建一般和累积变换的特征,我们首先需要设置一个包含数据的实体集并定义其变量之间的关系。我们在设置实体集和自动创建特征配方中描述了如何设置实体集。
要应用累积和一般变换,我们使用了featuretools中的dfs类。一般变换应用于整个 DataFrame,而不按特定变量分组。要执行一般变换,我们将包含变换名称的字符串列表传递给dfs的trans_primitives参数。
我们在按invoice分组后应用了累积变换。为此,我们将包含累积变换名称的字符串列表传递给dfs的groupby_trans_primitives参数。featuretools库知道应该按发票分组,因为我们通过使用EntitySet中的normalize_dataframe方法在步骤 5中建立了这个唯一标识符。
最后,我们不想从invoicesDataFrame 中的变量创建特征;因此,我们将dfs设置为通过设置ignore_dataframes = ["``invoices"]来忽略此 DataFrame。
dfs类返回了两个变量,包含原始和新特征的 DataFrame 以及一个包含特征名称的列表。新特征以创建它们的操作命名,例如SINE、COSINE、CUM_MAX或DIFF,后跟应用转换的变量,以及当适用时用于分组的变量。
注意,featuretools会自动识别并选择应用转换的变量。正弦、余弦、累计总和、最大值和差分被应用于数值变量,而time_since_previous转换被应用于日期时间变量。
结合数值特征
在第八章 创建新特征中,我们看到了我们可以通过使用数学运算结合变量来创建新特征。featuretools库支持多种结合变量的操作,包括加法、除法、取模和乘法。在本食谱中,我们将学习如何使用featuretools结合这些特征。
如何操作...
让我们从导入库和准备数据集开始:
-
首先,我们将导入
pandas、featuretools和Categorical逻辑类型:import pandas as pd import featuretools as ft from woodwork.logical_types import Categorical -
让我们加载在技术 要求部分中描述的数据集:
df = pd.read_csv( «retail.csv», parse_dates=[«invoice_date»]) -
让我们设置一个实体集:
es = ft.EntitySet(id="data") -
让我们将 DataFrame 添加到实体集中:
es = es.add_dataframe( dataframe=df, dataframe_name=»data», index="rows", make_index=True, time_index=»invoice_date», logical_types={«customer_id»: Categorical}, ) -
让我们创建一个与步骤 4中的 DataFrame 相关联的新 DataFrame:
es.normalize_dataframe( base_dataframe_name=»data», new_dataframe_name=»invoices», index="invoice", copy_columns=[«customer_id»], )
注意
关于步骤 4和步骤 5的更多详细信息,请访问设置实体集和自动创建特征 食谱。
-
我们将乘以
quantity和price变量,分别反映购买的商品数量和单价,以获得总支付金额: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»], )
注意
我们将agg_primitives设置为空列表,以避免创建默认的原始操作。
-
现在我们来显示新特征的名称:
feature_defs在以下输出中,我们可以看到特征名称,其中最后一个对应于
price和quantity变量的组合:[<Feature: customer_id>, <Feature: stock_code>, <Feature: description>, <Feature: quantity>, <Feature: price>, <Feature: price * quantity>] -
最后,让我们检查在步骤 6中创建的新 DataFrame:
feature_matrix.head()在以下输出中,我们可以看到新特征被附加到原始 DataFrame 的右侧:

图 9.9 – 由价格与数量乘积产生的新特征的 DataFrame
与featuretools结合特征可能比df["price"].mul(df["quantity"])的pandas功能要复杂得多。真正的力量在于我们以这种方式创建新特征并随后在发票或客户级别进行聚合时。我们将在使用聚合 原始操作创建特征*的食谱中讨论聚合函数。
它是如何工作的...
为了乘法特征,我们使用了来自 featuretools 的 MultiplyNumeric 原始操作,它可以通过 dfs 使用 multiply_numeric 字符串访问。我们将前一个字符串传递给 trans_primitive 参数,然后使用 primitive_options 参数指定要乘法的变量。请注意,此外,我们还向 agg_primitives 参数传递了一个空列表,以避免返回默认的聚合原始操作,并且我们忽略了来自 invoices DataFrame 的特征。
要查看允许您组合变量的其他函数,请访问 featuretools.alteryx.com/en/stable/api_reference.html#binary-transform-primitives。在撰写本文时,我注意到 MultiplyNumeric 和 DivideNumeric 不在文档中。您始终可以通过检查源代码来双检查哪些函数受支持:github.com/alteryx/featuretools/tree/main/featuretools/primitives/standard/transform/binary。您还可以通过在设置实体集及其关系后运行以下命令来检查您可以在数据上执行哪些操作:ft.get_valid_primitives(es, target_dataframe_name="data", max_depth=2)。在这里,es 是由 步骤 5 得到的实体集。
从日期和时间中提取特征
在 第六章 中,从日期和时间变量中提取特征,我们讨论了如何通过从日期和时间变量的日期和时间部分提取特征来丰富我们的数据集,例如年份、月份、星期几、小时等等。我们可以利用 featuretools 自动提取这些特征。
featuretools 库支持使用其 日期时间转换原始操作 创建来自日期时间变量的各种特征。这些原始操作包括常见的变量,如年份、月份和日期,以及其他特征,如 是否是午餐时间 或 是否是工作日。此外,我们可以提取表示日期是否为联邦假日或银行假日(如英国所称)的特征,或者确定到特定日期的时间距离的特征。对于零售公司来说,圣诞节、黑色星期五或节礼日等日期的邻近性通常意味着销售额的增加,如果他们正在预测需求,这些将是有用的变量。
注意
有关可以创建的日期时间变量特征的更多详细信息,请访问 featuretools.alteryx.com/en/stable/api_reference.html#datetime-transform-primitives。
在这个菜谱中,我们将使用 featuretools 自动从日期时间变量创建多个特征。
如何做...
让我们从导入库和准备数据集开始:
-
首先,我们将导入
pandas、featuretools和一些特殊的 datetime 原始函数:import pandas as pd import featuretools as ft from featuretools.primitives import ( IsFederalHoliday, DistanceToHoliday) from woodwork.logical_types import Categorical -
让我们加载技术要求部分描述的数据集:
df = pd.read_csv( «retail.csv», parse_dates=[«invoice_date»]) -
让我们设置一个实体集:
es = ft.EntitySet(id="data") -
让我们将 DataFrame 添加到实体集中:
es = es.add_dataframe( dataframe=df, dataframe_name=»data», index="rows", make_index=True, time_index=»invoice_date», logical_types={«customer_id»: Categorical}, ) -
让我们创建一个新的 DataFrame,它与步骤 4中的 DataFrame 有关联:
es.normalize_dataframe( base_dataframe_name=»data», new_dataframe_name=»invoices», index="invoice", copy_columns=[«customer_id»], )
注意
更多关于步骤 4和步骤 5的详细信息,请访问设置实体集和自动创建特征配方。
-
让我们创建一个返回布尔向量的原始函数,指示日期是否与英国的银行假日(即非工作日)相符(即,非工作日):
is_bank_hol = IsFederalHoliday(country="UK")
注意
在设置用于确定银行假日的原始函数时,选择正确的国家非常重要。要查看支持的国家列表,请访问github.com/dr-prodigy/python-holidays#available-countries。
-
让我们检查这个原始函数中包含哪些银行假日:
hols = is_bank_hol.holidayUtil.federal_holidays.values() available_hols = list(set(hols))如果我们执行
available_hols,我们将看到支持英国银行假日的列表:['May Day', 'Good Friday', 'Wedding of William and Catherine', 'Coronation of Charles III', 'Christmas Day', 'Wedding of Charles and Diana', 'Christmas Day (observed)', 'State Funeral of Queen Elizabeth II', 'Silver Jubilee of Elizabeth II', 'Spring Bank Holiday', 'Diamond Jubilee of Elizabeth II', 'Boxing Day (observed)', 'Platinum Jubilee of Elizabeth II', "New Year's Day (observed)", 'Boxing Day', 'Golden Jubilee of Elizabeth II', 'Millennium Celebrations', "New Year's Day"] -
让我们创建另一个原始函数,用于确定到特定日期的天数 – 在这种情况下,到节礼日的距离:
days_to_boxing = DistanceToHoliday( holiday="Boxing Day", country="UK") -
现在,让我们创建一个包含字符串的列表,这些字符串可以识别从
datetime中获取的常见特征,并包括步骤 6和步骤 8中的原始函数:date_primitives = [ "day", "year", "month", "weekday", "days_in_month", "part_of_day", "hour", "minute", is_bank_hol, days_to_boxing ] -
让我们现在根据
invoice_date日期变量从步骤 9创建日期和时间特征:feature_matrix, feature_defs = ft.dfs( entityset=es, target_dataframe_name=»invoices», agg_primitives=[], trans_primitives=date_primitives, )
注意
在步骤 4中,我们将invoice_date变量作为时间变量输入。因此,featuretools将使用此变量来创建日期和时间相关特征。
-
让我们显示创建的特征的名称:
feature_defs在以下输出中,我们看到原始特征和时间特征的名字:
[<Feature: customer_id>, <Feature: DAY(first_data_time)>, <Feature: DAYS_IN_MONTH(first_data_time)>, <Feature: DISTANCE_TO_HOLIDAY( first_data_time, holiday=Boxing Day, country=UK)>, <Feature: HOUR(first_data_time)>, <Feature: IS_FEDERAL_HOLIDAY( first_data_time, , country=UK)>, <Feature: MINUTE(first_data_time)>, <Feature: MONTH(first_data_time)>, <Feature: PART_OF_DAY(first_data_time)>, <Feature: WEEKDAY(first_data_time)>, feature_matrix.head() 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. -
让我们显示包含三个新特征的 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 特征
注意到一些创建的特征是数值型的,例如HOUR或DAY,一些是布尔型的,例如IS_FEDERAL_HOLIDAY,还有一些是分类型的,例如PART_OF_DAY。要查看PART_OF_DAY的值,请执行feature_matrix["PART_OF_DAY(first_data_time)"].unique()。
它是如何工作的...
要从日期时间变量中创建特征,我们使用了 featuretools 的日期时间转换原语 (featuretools.alteryx.com/en/stable/api_reference.html#datetime-transform-primitives)。这些原语可以通过 dfs 使用我们在 步骤 6 到 9 中指定的字符串和函数通过 trans_primitive 参数访问。请注意,此外,我们还向 agg_primitives 参数传递了一个空列表,以避免返回应用于我们的日期时间特征的默认聚合原语。我们还忽略了来自 invoices DataFrame 的特征。
注意
我们将 agg_primitives 设置为空列表,并忽略了 invoices DataFrame,以保持输出简单并能够专注于日期时间特征。然而,请注意,featuretools 的真正威力在于从 datetime 创建原语,然后在不同的实体级别进一步聚合它们。
从文本中提取特征
在 第十一章 从文本变量中提取特征 中,我们将讨论我们可以利用 pandas 和 scikit-learn 从文本片段中提取的各种特征。我们还可以通过利用 featuretools 自动从文本中提取多个特征。
featuretools 库支持创建多个基本特征作为其默认功能的一部分,例如文本中的字符数、单词数、每个单词的平均字符数以及文本片段中的中位词长等。
注意
要查看默认文本原语的全列表,请访问 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。
在这个配方中,我们将首先利用 featuretools 的默认功能从文本变量中创建多个特征,然后突出显示如何使用 nlp_primitives 库中的原语。
准备工作
要跟随这个配方,您需要安装 nlp_primitives 库,您可以使用 pip 来完成:
pip install nlp_primitives
否则,您可以使用 conda:
conda install -c conda-forge nlp-primitives
注意
更多详情,请访问 nlp_primitives GitHub 仓库:github.com/alteryx/nlp_primitives
如何做到这一点...
让我们先导入库并准备好数据集:
-
首先,我们将导入
pandas、featuretools和逻辑类型:import pandas as pd import featuretools as ft from woodwork.logical_types import ( Categorical, NaturalLanguage) -
让我们将第 技术 要求 部分中描述的数据集加载进来:
df = pd.read_csv( «retail.csv», parse_dates=[«invoice_date»]) -
让我们设置一个实体集:
es = ft.EntitySet(id="data") -
让我们将 DataFrame 添加到实体集中,并突出显示
description变量是一个文本变量: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, } )
注意
为了使 featuretools 库的文本原语正常工作,我们需要通过使用 NaturalLanguage 逻辑类型来指示哪些变量是文本。
-
让我们创建一个新的 DataFrame,它与第 4 步中的 DataFrame 有关系:
es.normalize_dataframe( base_dataframe_name=»data», new_dataframe_name=»invoices», index="invoice", copy_columns=[«customer_id»], )
注意
更多关于 步骤 4 和 5 的详情,请访问 设置实体集和自动创建特征 的配方。
-
让我们创建一个字符串列表,对应于我们想要创建的文本特征:
text_primitives = [ "num_words", "num_characters", "MeanCharactersPerWord" , "PunctuationCount"] -
现在我们从
description变量中提取文本特征:feature_matrix, feature_defs = ft.dfs( entityset=es, target_dataframe_name=»data», agg_primitives=[], trans_primitives=text_primitives, ignore_dataframes=[«invoices»], ) -
让我们显示创建的特征的名称:
feature_defs在以下输出中,我们看到原始特征名称,然后是来自
description变量的创建特征:[<Feature: customer_id>, <Feature: invoice>, <Feature: stock_code>, <Feature: quantity>, <Feature: price>, <Feature: MEAN_CHARACTERS_PER_WORD(description)>, <Feature: NUM_CHARACTERS(description)>, <Feature: NUM_WORDS(description)>, feature_matrix.head(). -
让我们显示包含文本派生特征的 DataFrame 的一个切片:
text_f = [ "NUM_CHARACTERS(description)", "NUM_WORDS(description)", "PUNCTUATION_COUNT(description)", ] feature_matrix[text_f].head()在以下输出中,我们看到一个包含从文本创建的特征的 DataFrame:

图 9.11 – 从文本创建的特征 DataFrame
注意
featuretools 库移除了原始文本变量 description,并返回了新的特征。
要使用 nlp_primitives 包中的原语创建特征,您需要首先导入它们 – 例如,通过执行 from nlp_primitives import DiversityScore – 然后将原语添加到我们在第 6 步中创建的文本原语列表中。请注意,这些是复杂函数,因此创建特征可能需要一些时间。
它是如何工作的...
要从文本变量创建特征,我们使用了 featuretools 的默认文本原语。这些原语可以通过 dfs 通过传递一个字符串列表来访问,这些字符串对应于原语名称,例如第 6 步中的那些,传递给 trans_primitives 参数。
对于更高级的原语,您需要从 nlp_primitives 库中导入原语函数,然后将它们传递给 dfs 的 trans_primitives 参数。这样,dfs 就可以利用这些原语的功能从文本中创建新的特征。nlp_primitives 库在底层使用 nltk Python 库。
使用聚合原语创建特征
在本章中,我们通过将现有变量映射到新特征中,通过各种函数自动创建了特征。例如,我们从日期时间变量中提取日期和时间部分,计算文本中的单词、字符和标点符号的数量,将数值特征组合成新变量,并使用正弦和余弦等函数转换特征。为了创建这些特征,我们使用了转换原语。
featuretools 库也支持与发票相关的 price,一个聚合原语将取单个发票的所有价格观测值并返回一个单一值,例如该发票的平均价格或总和(即支付的总金额)。
注意
featuretools 的聚合功能相当于 pandas 中的 groupby,随后是 pandas 函数如 mean、sum、std 和 count 等。
一些聚合原语与数值变量一起工作,例如平均值、总和、最大值和最小值。其他聚合原语是特定于分类变量的,例如唯一值的数量和最频繁的值(众数)。
注意
要获取支持的聚合原语的完整列表,请访问 featuretools.alteryx.com/en/stable/api_reference.html#aggregation-primitives。
在这个菜谱中,我们首先将通过聚合现有变量来创建多个特征。之后,我们将结合使用转换和聚合原语,以突出 featuretools 的真正威力。
准备工作
在这个菜谱中,我们将使用来自 UCI 机器学习仓库的 Online Retail II 数据集。这个数据集包含有关产品(项目)、发票和客户的信息。为了跟随这个菜谱,了解这些实体的性质及其关系,以及如何正确设置 featuretools 的实体集,非常重要,这些我们在 设置实体集和自动创建特征 菜谱中已经描述过。在继续下一部分之前,请确保你已经查看了那个菜谱。
如何做到这一点...
让我们先导入库并准备数据集:
-
首先,我们将导入
pandas、featuretools和逻辑类型:import pandas as pd import featuretools as ft from woodwork.logical_types import ( Categorical, NaturalLanguage) -
让我们加载 技术 要求 部分中描述的数据集:
df = pd.read_csv( «retail.csv», parse_dates=[«invoice_date»]) -
让我们设置一个实体集:
es = ft.EntitySet(id="data") -
让我们将 DataFrame 添加到实体集中,并突出显示
description变量是一个文本变量,customer_id是分类变量,而invoice_date是一个日期时间特征: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, } ) -
让我们创建一个新的 DataFrame,它与 步骤 4 中的 DataFrame 有关系:
es.normalize_dataframe( base_dataframe_name=»data», new_dataframe_name=»invoices», index="invoice", copy_columns=[«customer_id»], ) -
现在,我们添加第二个关系,即客户与发票之间的关系。为此,我们指出基础 DataFrame,我们在 步骤 5 中将其称为
invoices,我们给新的 DataFrame 起个名字,customers,并添加一个唯一的客户标识符:es.normalize_dataframe( base_dataframe_name=»invoices», new_dataframe_name=»customers», index=»customer_id», )
注意
更多关于步骤 4到步骤 5的详细信息,请访问设置实体集和自动创建特征配方。
-
让我们创建一个字符串名称列表,以标识我们想要使用的聚合原语:
agg_primitives = ["mean", "max", "min", "sum"] -
让我们通过在客户级别聚合数据来创建特征。为此,我们设置
featuretools中的dfs类,将customers指定为目标 DataFrame,并将步骤 7中的聚合原语传递给trans_primitives参数,同时传递一个空列表以防止dfs返回默认的转换:feature_matrix, feature_defs = ft.dfs( entityset=es, target_dataframe_name=»customers», agg_primitives=agg_primitives, trans_primitives=[], ) -
让我们显示创建的特征名称:
feature_defs在以下输出中,我们可以看到在客户级别聚合的特征名称:
[<Feature: MAX(data.price)>, <Feature: MAX(data.quantity)>, <Feature: MEAN(data.price)>, <Feature: MEAN(data.quantity)>, <Feature: MIN(data.price)>, <Feature: MIN(data.quantity)>, <Feature: SUM(data.price)>, <Feature: SUM(data.quantity)>, <Feature: MAX(invoices.MEAN(data.price))>, <Feature: MAX(invoices.MEAN(data.quantity))>, <Feature: MAX(invoices.MIN(data.price))>, <Feature: MAX(invoices.MIN(data.quantity))>, <Feature: MAX(invoices.SUM(data.price))>, <Feature: MAX(invoices.SUM(data.quantity))>, <Feature: MEAN(invoices.MAX(data.price))>, <Feature: MEAN(invoices.MAX(data.quantity))>, <Feature: MEAN(invoices.MEAN(data.price))>, <Feature: MEAN(invoices.MEAN(data.quantity))>, <Feature: MEAN(invoices.MIN(data.price))>, <Feature: MEAN(invoices.MIN(data.quantity))>, <Feature: MEAN(invoices.SUM(data.price))>, <Feature: MEAN(invoices.SUM(data.quantity))>, <Feature: MIN(invoices.MAX(data.price))>, <Feature: MIN(invoices.MAX(data.quantity))>, <Feature: MIN(invoices.MEAN(data.price))>, <Feature: MIN(invoices.MEAN(data.quantity))>, <Feature: MIN(invoices.SUM(data.price))>, <Feature: MIN(invoices.SUM(data.quantity))>, <Feature: SUM(invoices.MAX(data.price))>, <Feature: SUM(invoices.MAX(data.quantity))>, <Feature: SUM(invoices.MEAN(data.price))>, <Feature: SUM(invoices.MEAN(data.quantity))>, <Feature: SUM(invoices.MIN(data.price))>, <Feature: SUM(invoices.MIN(data.quantity))>]
注意
请记住,featuretools使用创建它们的函数来命名特征,然后是用于计算的 DataFrame,最后是用于计算的变量。因此,MAX(data.price)是每个客户在数据集中看到的最高价格。另一方面,MEAN(invoices.MAX(data.price))是每个客户在特定发票中观察到的所有最高价格的平均值。也就是说,如果一个客户有六张发票,我们首先找到六张发票中的每张的最高价格,然后取这些值的平均值。
-
现在让我们显示包含原始数据和新特征的 DataFrame 的结果:
feature_matrix.head()在以下输出中,我们可以看到
dfs返回的 DataFrame 中的一些变量:

图 9.12 – 在客户级别聚合的一些特征结果的 DataFrame
由于空间限制,我们无法显示步骤 10的整个输出,所以请确保你在你的电脑上执行它,或者访问我们的配套 GitHub 仓库以获取更多详细信息:github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch09-featuretools/Recipe6-Creating-features-with-aggregation-primitives.ipynb.
为了跟进,让我们结合从使用转换原语和本配方中的聚合函数中学习到的内容。首先,我们将从现有的日期时间和文本变量中创建新特征;然后,我们将这些特征与数值变量一起在客户级别进行聚合。
-
让我们创建日期和文本原语列表:
trans_primitives = ["month", "weekday", "num_words"] -
让我们创建一个包含聚合原语的列表:
agg_primitives = ["mean"] -
现在让我们通过转换和聚合变量来自动创建特征:
feature_matrix, feature_defs = ft.dfs( entityset=es, target_dataframe_name=»customers», agg_primitives=agg_primitives, trans_primitives=trans_primitives, max_depth=3, )步骤 13中的代码触发了特征的创建以及它们在客户级别的后续聚合。
-
让我们显示新特征的名称:
feature_defs在以下输出中,我们可以看到创建的变量的名称:
[<Feature: MEAN(data.price)>, <Feature: MEAN(data.quantity)>, <Feature: MONTH(first_invoices_time)>, <Feature: WEEKDAY(first_invoices_time)>, <Feature: MEAN(invoices.MEAN(data.price))>, <Feature: MEAN(invoices.MEAN(data.quantity))>, <Feature: MEAN(data.NUM_WORDS(description))>, <Feature: MEAN(invoices.MEAN(data.NUM_ WORDS(description)))>] WORDS(description)))>]
注意,在我们的配方中,由于空间限制,我们尽量减少特征的创建,但你可以创建尽可能多的特征,并使用 featuretools 内置的功能显著丰富你的数据集。
它是如何工作的...
在这个配方中,我们将使用转换原语创建特征的方法与使用聚合原语创建特征的方法结合起来,这些方法我们在本章中进行了讨论。
要使用 featuretools 自动创建特征,我们首先需要将数据输入到实体集中,并建立数据之间的关系。我们在 设置实体集和自动创建特征 的配方中讨论了如何设置实体集。
为了聚合现有特征,我们使用了 dfs 类。我们创建了一个包含对应聚合原语字符串的列表,并将其传递给 dfs 的 agg_primitives 参数。为了聚合现有变量而不创建新特征,我们将一个空列表传递给 dfs 的 trans_primitives 参数。
customers DataFrame 是 invoice DataFrame 的子集,而 invoice DataFrame 又是原始数据的子集。因此,dfs 从原始数据和每个发票的预聚合数据中创建了聚合。因此,MEAN(data.price) 特征由客户购买的商品的平均价格组成,这个价格是通过整个数据计算得出的,而 MEAN(invoices.MEAN(data.price)) 首先计算每个发票的平均价格,然后取这些值的平均值来计算客户的价格。因此,如果一个客户有五张发票,featuretools 首先计算每张发票支付的平均价格,然后取这些值的平均值。因此,MEAN(data.price) 和 MEAN(invoices.MEAN(data.price)) 不是同一个特征。
注意
聚合原语对唯一标识符的信息进行聚合。聚合原语使用数学运算,如均值、标准差、最大值和最小值、总和以及数值变量的偏度系数。对于分类变量,聚合原语使用众数和唯一项的计数。对于唯一标识符,聚合原语计算出现的次数。
接下来,我们将新特征的创建与聚合相结合。为此,我们将对应转换原语的字符串列表传递给 dfs 的 trans_primitives 参数,并将对应聚合原语的字符串列表传递给 dfs 的 agg_primitives 参数。
步骤 13 的一个输出是一个新特性的列表。从这些特性中,我们可以识别出从每个客户的第一个发票日期创建的特性,例如 MONTH(first_invoices_time) 和 WEEKDAY(first_invoices_time)。我们还可以看到从文本特性中聚合而来的特性,例如 MEAN(data.NUM_WORDS(description)) 和 MEAN(invoices.MEAN(data.NUM_WORDS(description)))。最后,我们还可以看到现有数值变量的聚合,例如 MEAN(data.price) 和 MEAN(invoices.MEAN(data.price))。
注意
如果你想要将转换和聚合原语应用于特定的变量,你可以通过指定如这里所讨论的原语选项来实现:docs.featuretools.com/en/stable/guides/specifying_primitive_options.html。
第十章:使用 tsfresh 从时间序列创建特征
在本书的整个过程中,我们讨论了针对表格和关系数据集定制的特征工程方法和工具。在本章中,我们将把我们的重点转向时间序列数据。时间序列是在时间上按顺序连续观察到的观察值的序列。例如包括能源生产和需求、温度、空气污染物浓度、股价和销售收入。这些示例中的每一个都代表一个变量,它们的值随时间变化。
便宜且能够测量运动、移动、湿度、葡萄糖和其他参数的传感器的广泛应用,显著增加了时间标注数据的数量。这些时间序列可以用于各种分类任务。例如,通过分析在特定时间间隔内家庭的电力使用模式,我们可以推断出是否使用了特定的电器。同样,超声波传感器的信号可以帮助确定(气体)管道故障的概率,而声音波长的特征可以帮助预测听众是否会喜欢一首歌。时间序列数据对于回归任务也很有价值。例如,机械设备传感器的信号可以用来预测设备的剩余使用寿命。
要使用时间序列与传统的监督机器学习模型,如线性回归、逻辑回归或基于决策树的算法,我们需要将每个时间序列映射到一个定义良好的特征向量,以捕捉其特征。时间序列模式,包括趋势、季节性和周期性等,可以通过简单和复杂的数学运算的组合来捕捉。简单的计算包括,例如,计算时间序列的平均值和标准差。更复杂的方法包括确定相关性或熵,例如。此外,我们可以应用非线性时间序列分析函数来分解时间序列信号,例如傅里叶变换或小波变换,并使用这些函数的参数作为监督模型的特征。
从时间序列中创建特征可能非常耗时;我们需要应用各种信号处理和时间序列分析算法来识别和提取有意义的特征。tsfresh Python 包,代表 tsfresh,包含一个特征选择算法,该算法可以识别给定时间序列的最具预测性的特征。通过自动化复杂时间序列方法的应用,tsfresh弥合了信号处理专家和机器学习实践者之间的差距,使得从时间序列数据中提取有价值特征变得更加容易。
在本章中,我们将学习如何通过利用 tsfresh 自动从时间序列数据中创建数百个特征。随后,我们将讨论如何通过选择最相关的特征、从不同的时间序列中提取不同的特征以及将特征创建过程集成到 scikit-learn 流程中来对时间序列数据进行分类。
在本章中,我们将介绍以下食谱:
-
从时间序列中自动提取数百个特征
-
从时间序列数据中自动创建和选择预测特征
-
从不同的时间序列中提取不同的特征
-
通过特征选择创建特征子集
-
将特征创建嵌入到 scikit-learn 流程中
技术要求
在本章中,我们将使用开源的 tsfresh Python 库。您可以通过执行 pip install tsfresh 来使用 pip 安装 tsfresh。
注意
如果您使用的是旧版 Microsoft 操作系统,您可能需要更新 Microsoft C++ 编译工具,以便继续安装 tsfresh 包。请按照此线程中的步骤进行操作:stackoverflow.com/questions/64261546/how-to-solve-error-microsoft-visual-c-14-0-or-greater-is-required-when-inst。
我们将使用来自 UCI 机器学习仓库的 Occupancy Detection 数据集,该数据集可在 archive.ics.uci.edu/ml/datasets/Occupancy+Detection 找到,并受 Creative Commons Attribution 4.0 国际 (CC BY 4.0) 许可协议的许可:creativecommons.org/licenses/by/4.0/legalcode。该数据的相应引用如下:
Candanedo, Luis. (2016). Occupancy Detection. UCI Machine Learning Repository. doi.org/10.24432/C5X01N。
我已下载并修改了如本笔记本所示的数据:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch10-tsfresh/prepare-occupancy-dataset.ipynb
要获取修改后的数据集和目标变量,请查看以下链接中的文件 occupancy.csv 和 occupancy_target.csv:github.com/PacktPublishing/Python-Feature-engineering-Cookbook-Third-Edition/blob/main/ch10-tsfresh
占用检测数据集包含在每分钟间隔内采集了 135 小时的时间序列数据。变量测量了温度、湿度、
级别和办公室的能耗。使用摄像头录像来确定是否有人在办公室。目标变量显示办公室在任意一小时是否被占用。如果目标值为 1,则表示该小时办公室被占用;否则,值为 0。
时间序列数据集和目标变量数据集的行数不同。时间序列数据集包含每分钟间隔的 135 小时记录,即 8,100 行。目标变量只有 135 行,每个标签指示在 135 小时中的每个小时办公室是否被占用。
注意
查看本书 GitHub 仓库中的笔记本,以熟悉数据集的不同时间序列的绘图:github.com/PacktPublishing/Python-Feature-Engineering-Cookbook-Third-Edition/blob/main/ch10-tsfresh/prepare-occupancy-dataset.ipynb
从时间序列中自动提取数百个特征
时间序列是按时间顺序索引的数据点。分析时间序列序列使我们能够做出各种预测。例如,传感器数据可以用来预测管道故障,声音数据可以帮助识别音乐类型,健康历史或个人测量,如血糖水平,可以指示一个人是否生病,正如我们将在这个菜谱中展示的,光照使用模式、湿度和
级别可以确定办公室是否被占用。
使用传统的机器学习算法,如线性回归或随机森林来训练回归和分类模型,我们需要一个大小为 M x N 的数据集,其中 M 是行数,N 是特征数或列数。然而,对于时间序列数据,我们拥有的是 M 个时间序列的集合,每个时间序列都有多个按时间顺序索引的行。要在监督学习模型中使用时间序列,每个时间序列都需要映射到一个定义良好的特征向量 N,如下面的图所示:

图 10.1 – 显示从时间序列创建特征以进行分类或回归的过程的图表
这些特征向量,如图 10.1.1 所示,表示为
,
,和
,应该捕捉时间序列的特征。例如,
可能是时间序列的均值,而
是其方差。我们可以创建许多特征来描述数据点的分布、相关性、平稳性或熵等时间序列的特性。因此,特征向量 N 可以通过应用一系列特征化方法来构建,这些方法以时间序列作为输入,并返回一个或多个标量作为输出。均值或总和将时间序列序列作为输入,并返回一个标量作为输出,即时间序列的均值或其值的总和。我们还可以将线性趋势拟合到时间序列序列中,这将返回两个标量——一个表示斜率,另一个表示截距。
tsfresh对时间序列应用 63 种特征化方法,每种方法返回一个或多个标量,因此对于任何给定的时间序列,都会产生超过 750 个特征。在本配方中,我们将使用tsfresh将时间序列数据转换为 M x N 特征表,然后我们将使用该特征表来预测办公室占用情况。
准备工作
在本食谱中,我们将使用技术要求部分中描述的占用检测数据集。此数据集包含在办公室内每分钟间隔的温度、湿度、
水平和照明消耗的测量值。共有 135 小时的测量数据,每小时都有一个唯一的标识符。还有一个包含目标变量的数据集,该变量指示在 135 小时中的哪一小时办公室是有人使用的。让我们加载数据并绘制一些图表来了解其模式:
-
让我们加载
pandas和matplotlib:import matplotlib.pyplot as plt import pandas as pd -
加载数据集并显示前五行:
X = pd.read_csv( "occupancy.csv", parse_dates=["date"]) X.head()在以下图中,我们可以看到包含唯一标识符的数据集,随后是测量日期和时间以及五个时间序列的值,这些时间序列捕捉了温度、湿度、灯光和
水平:

图 10.2 – 包含时间序列数据的 DataFrame
-
让我们创建一个函数来绘制给定小时(
id列是 135 小时记录中每一小时的唯一标识符)的步骤 2中的时间序列图: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() -
让我们绘制一个小时办公室无人使用时的时间序列图:
plot_timeseries(2)在以下图中,我们可以看到记录的第二小时的时间序列值,当时办公室是空的:

图 10.3 – 数据收集第二小时办公室空时的时间序列值
注意到灯光是关闭的,这就是为什么我们在右上角light消耗的图表中看到一条平坦的直线。
-
现在,让我们绘制一个小时办公室有人使用时的时间序列数据:
plot_timeseries(15)在以下图中,我们可以看到记录的第十五小时的时间序列值,当时办公室是有人使用的:

图 10.4 – 数据收集第十五小时办公室有人使用时的时间序列值
注意这次灯光是开着的(右上角面板)。
在这个菜谱中,我们将从这些每个时间序列数据的一小时窗口中提取特征,捕捉它们特性的各个方面。从每个这些 60 分钟的时间序列段中,我们将使用tsfresh自动生成超过 750 个特征,确保数据的属性得到全面表示。
如何实现...
我们将首先自动从单个时间序列lights创建数百个特征,然后使用这些特征来预测办公室在任意给定小时是否被占用:
-
让我们导入所需的 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 ) -
加载技术要求部分中描述的数据集:
X = pd.read_csv("occupancy.csv", parse_dates=["date"]) -
将目标变量加载到
pandasSeries 中:y = pd.read_csv("occupancy_target.csv", index_col="id")["occupancy"] -
让我们使用
tsfresh为每个小时的能耗记录自动创建数百个特征。要从light变量创建特征,我们将包含此变量的 DataFrame 和每个时间序列的唯一标识符传递给tsfresh的extract_features函数:features = extract_features( X[[«id», «light»]], column_id="id")如果我们执行
features.shape,我们会得到(135, 789),这对应于结果 DataFrame 的大小,其中每一行代表一个小时的记录,每一列代表由tsfresh创建的一个特征。有 789 个特征可以描述任意给定小时的能耗。现在执行features.head()来查看结果 DataFrame 的概览。由于空间限制,我们无法在书中展示整个 DataFrame,所以我们将探索一些特征。 -
让我们将创建的五个特征的名称存储在一个数组中:
feats = features.columns[10:15]如果我们执行
feats,我们会看到五个特征的名称,分别对应每小时能耗的平均值、长度、标准差、变异系数和方差:Index(['light__mean', 'light__length', 'light__standard_deviation', 'light__variation_coefficient', 'light__variance'], dtype='object') -
现在,让我们显示步骤 5中前五个小时的特征值:
features[feats].head()在下面的 DataFrame 中,我们看到从前五个小时的能耗时间序列中提取的特征:

图 10.5 – 为每小时能耗创建的特征
在图 10.4.4 中查看平均能耗值,我们可以看到前一个小时灯是亮着的,接下来的四个小时则是关闭的。时间序列的长度为 60,因为我们每小时有 60 分钟的记录。
注意
tsfresh对时间序列应用 63 种特征创建方法。根据时间序列的特性,如长度或其变异性,某些方法可能会返回缺失值或无穷大值。例如,在图 10.4.4 中,我们看到在能耗恒定的那些小时无法计算变异系数。在这些情况下,方差也是0。实际上,对于我们的数据集,许多生成的特征只包含NaN值,或者像长度一样是常数,因此对于训练机器学习模型没有用。
-
tsfresh包含一个插补函数,用于插补包含NaN值的特征。让我们继续插补我们的特征:impute(features)tsfresh的impute函数将NaN、-Inf和Inf值分别替换为变量的中位数、最小值或最大值。让我们使用这些特征来训练一个逻辑回归模型并预测办公室是否被占用。
-
让我们先分离数据集为训练集和测试集:
X_train, X_test, y_train, y_test = train_test_split( features, y, test_size=0.1, random_state=42, ) -
现在,让我们设置并训练一个逻辑回归模型,然后评估其性能:
cls = LogisticRegression(random_state=10, C=0.01) cls.fit(X_train, y_train) print(classification_report( y_test, cls.predict(X_test)))在以下输出中,我们看到用于分类分析的常用评估指标值,这表明创建的特征对预测办公室占用情况是有用的:
precision recall f1-score support 0 1.00 1.00 1.00 11 1 1.00 1.00 1.00 3 accuracy 1.00 14 macro avg 1.00 1.00 1.00 14 weighted avg 1.00 1.00 1.00 14
注意
为了保持食谱简单,我没有优化模型超参数或调整概率阈值——这些是我们通常为了确保模型准确而做的事情。
-
最后,让我们从每个时间序列中提取特征,即
light、temperature、humidity和co2,这次,我们将在提取后立即插补特征:features = extract_features( X, column_id="id", impute_function=impute, column_sort="date", )
注意
在 步骤 10 中,我们指出我们想要根据包含测量时间和日期的时间戳对时间序列进行排序,通过将 date 变量传递给 column_sort 参数。当我们的时间序列不是等距的或不是按时间顺序排列时,这很有用。如果我们将此参数设置为 None,tsfresh 假设时间序列是有序且等距的。
步骤 10 的输出是一个包含 135 行的 DataFrame,包含 3,945 个特征(执行 features.shape 检查),这些特征描述了五个原始时间序列——温度、光照、湿度及其比率,以及
办公室。这些特征在 步骤 10 中进行了插补,因此您可以继续使用此 DataFrame 来训练另一个逻辑回归模型以预测办公室占用情况。
它是如何工作的...
在这个食谱中,我们使用了 tsfresh 从五个时间序列自动创建数百个特征,然后使用这些特征来训练一个逻辑回归模型以预测办公室是否被占用。
注意
要使用 tsfresh 创建特征,我们需要从其中提取特征的时间序列间隔必须用 id 变量标记。
要从时间序列创建特征,我们使用了 tsfresh 的 extract_features 函数。此函数接受包含时间序列的唯一标识符的 DataFrame 作为输入,并返回包含提取特征的 DataFrame 作为输出。
extract_features 有三个关键参数:column_id、column_sort 和 impute_function。column_id 接收用于提取特征的每个序列的唯一标识符列的名称。column_sort 用于在提取特征之前重新排序时间序列。当 column_sort 设置为 None 时,tsfresh 假设数据是按时间顺序排列的,并且时间戳是等距的。在 步骤 10 中,我们传递了 date 变量作为排序变量,这告诉 tsfresh 在提取特征之前如何排序数据。
注意
在我们的数据集中,将 column_sort 设置为 None 或传递 date 变量没有区别,因为我们的时间序列已经按时间顺序排列,并且时间戳是等距的。如果您的时序不是这种情况,请使用此参数正确创建特征。
最后,extract_features 函数也通过 impute_function 参数接受 impute 函数,以自动从创建的特征中移除无限和 NaN 值。将在接下来的菜谱中讨论 extract_features 的其他参数。
注意
更多关于 extract_features 函数的详细信息,请访问 tsfresh.readthedocs.io/en/latest/api/tsfresh.feature_extraction.html#module-tsfresh.feature_extraction.extraction。
impute 函数,它可以独立使用,就像我们在 步骤 7 中做的那样,或者在我们 步骤 10 中做的那样,在 extract_features 函数内部使用,分别用变量的中位数、最小值或最大值替换 NAN、-Inf 和 Inf 值。如果特征只包含 NaN 值,则它们被零替换。插补是在原地发生的——也就是说,在正在插补的同一个 DataFrame 中。
extract_features 函数返回一个包含数据中唯一标识符行数的 DataFrame。在我们的例子中,它返回了一个包含 135 行的 DataFrame。结果 DataFrame 的列对应于应用于每个 135 个 60 分钟时间序列的 63 种表征方法返回的 789 个值。
在 步骤 5 中,我们探索了一些结果特征,这些特征捕捉了时间序列的均值、方差和变异系数,以及它们的长度。让我们再探索一些结果特征。
一些创建的变量是自我解释的。例如,'light__skewness' 和 'light__kurtosis' 变量包含偏度和峰度系数,这些系数表征了数据分布。'light__has_duplicate_max','light__has_duplicate_min' 和 'light__has_duplicate' 变量指示时间序列是否在时间间隔内有重复值或重复的最小值或最大值。'light__quantile__q_0.1','light__quantile__q_0.2' 和 'light__quantile__q_0.3' 变量显示了时间序列的不同分位数。最后,'light__autocorrelation__lag_0','light__autocorrelation__lag_1' 和 'light__autocorrelation__lag_2' 变量显示了时间序列与其过去值的自相关,滞后 0,1 或 2 步——这些信息通常对预测很有用。
其他特征化方法返回的是从信号处理算法中获取的特征,例如,对于 Ricker 小波,连续小波变换返回的 'light__cwt_coefficients__coeff_0__w_2__widths_(2, 5, 10, 20)', 'light__cwt_coefficients__coeff_0__w_5__widths_(2, 5, 10, 20)', 'light__cwt_coefficients__coeff_0__w_10__widths_(2, 5, 10, 20)', 和 'light__cwt_coefficients__coeff_0__w_20__widths_(2, 5, 10, 20)' 等特征。
注意
由于方法众多,我们无法在本书中详细讨论每种特征化方法及其输出。您可以在 tsfresh.readthedocs.io/en/latest/api/tsfresh.feature_extraction.html 找到关于 tsfresh 支持的转换及其公式的更多详细信息。
一些由 tsfresh 自动创建的特征可能对于某些时间序列来说没有意义,甚至无法计算,因为它们需要一定的长度或数据变异性,或者时间序列必须满足某些分布假设。因此,特征的适用性将取决于时间序列的性质。
注意
您可以根据领域知识决定从您的时间序列中提取哪些特征,或者通过创建所有可能的特征,然后应用特征选择算法或进行数据分析来决定。实际上,从我们的数据集中,许多生成的特征要么是常数,要么只包含缺失数据。因此,我们可以通过从数据中去除这些特征来减少特征空间到信息特征。
参考资料链接
想要了解更多关于 tsfresh 的详细信息,请参阅文章 Christ M.,Braun N.,Neuffer J.,和 Kempa-Liehr A. (2018). *基于可扩展假设检验的时间序列特征提取 (tsfresh – 一个 Python 包). Neurocomputing 307 (2018). 页码 72-77. dl.acm.org/doi/10.1016/j.neucom.2018.03.067。
从时间序列数据自动创建和选择预测特征
在先前的菜谱中,我们使用tsfresh自动从时间序列变量中提取了数百个特征。如果我们有多个时间序列变量,我们很容易得到一个包含数千个特征的数据库。此外,许多生成的特征只有缺失数据或常数,因此对训练机器学习模型没有用。
当我们创建分类和回归模型来解决现实生活中的问题时,我们通常希望我们的模型只输入少量相关特征以产生可解释的机器学习输出。简单的模型有很多优点。首先,它们的输出更容易解释。其次,简单的模型存储成本更低,训练速度更快。它们也能更快地返回输出。
tsfresh包含一个基于非参数统计假设检验的高度可并行化的特征选择算法,该算法可以在特征创建过程的后面执行,以快速去除无关特征。特征选择过程使用不同的测试针对不同的特征。
tsfresh使用以下测试来选择特征:
-
如果特征和目标都是二元的,则使用费舍尔精确检验独立性
-
如果特征或目标中任一个是二元的,则使用柯尔莫哥洛夫-斯米尔诺夫检验
-
如果特征或目标都不是二元的,则使用肯德尔秩检验
这些测试的优势在于它们是非参数的,因此不对被测试变量的潜在分布做出假设。
这些测试的结果是一个 p 值向量,衡量每个特征与目标之间的关联显著性。然后根据 Benjamini-Yekutieli 过程评估这些 p 值,以决定保留哪些特征。
注意
关于tsfresh的特征选择过程的更多详细信息,请参阅文章 Christ, Kempa-Liehr, and Feindt,分布式和并行时间序列特征提取用于工业大数据应用。亚洲机器学习会议(ACML)2016,大数据学习研讨会(WLBD),汉密尔顿(新西兰),arXiv,arxiv.org/abs/1610.07717v1。
在这个菜谱中,我们将自动从各种时间序列中创建数百个特征,然后利用tsfresh选择最相关的特征。
如何操作...
我们将首先自动从一条时间序列,照明,创建和选择特征,然后我们将自动化处理多条时间序列:
-
让我们导入所需的 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 -
加载数据集和目标变量,如技术 要求部分所述:
X = pd.read_csv("occupancy.csv", parse_dates=["date"]) y = pd.read_csv("occupancy_target.csv", index_col="id")["occupancy"] -
让我们为每个小时的
照明使用记录自动创建数百个特征,并对生成的特征进行插补:features = extract_features( X[[«id», «light»]], column_id="id", impute_function=impute, )上一步的输出是一个包含 135 行和 789 列的 DataFrame,对应于从每个小时的照明消耗中创建的特征。
注意
有关步骤 3或占用检测数据集的更多详细信息,请查看从时间序列自动提取数百个特征菜谱。
-
现在,让我们根据我们在本菜谱介绍中提到的非参数测试来选择特征:
features = select_features(features, y)如果我们执行
len(features),我们将看到值135,这意味着在步骤 3中创建的 789 个特征中,只有 135 个具有统计学意义。继续执行features.head()以显示结果的 DataFrame 的前五行。 -
由于空间原因,我们只显示前五个特征:
feats = features.columns[0:5] features[feats].head()在以下 DataFrame 中,我们可以看到前五个小时光消耗的前五个特征的价值:

图 10.6 – 从每小时的光消耗中创建的五个选定特征的 DataFrame
请查看“如何工作…”部分,以获取对步骤 4生成的 DataFrame 的更详细分析。
-
现在,我们将使用步骤 4中的特征来训练一个逻辑回归模型并预测办公室是否被占用。让我们首先将数据集分为训练集和测试集:
X_train, X_test, y_train, y_test = train_test_split( features, y, test_size=0.1, random_state=42, ) -
让我们设置并训练一个逻辑回归模型,然后评估其性能:
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)))在以下输出中,我们可以看到分类分析中常用评估指标的价值。这些表明选定的特征对预测办公室占用是有用的:
precision recall f1-score support 0 1.00 0.91 0.95 11 1 0.75 1.00 0.86 3 accuracy 0.93 14 macro avg 0.88 0.95 0.90 14 extract_relevant_features, and, like this, combine *steps 3* and *4*. We’ll do that to create and select features automatically for the five time series in our dataset:features = extract_relevant_features(
X,
y,
column_id="id",
column_sort="date",
)
注意
extract_relevant_features的参数与extract_features的参数非常相似。请注意,然而,前者将自动执行插补以能够进行特征选择。我们在从时间序列自动提取数百个特征菜谱中讨论了extract_features的参数。
步骤 8的输出是一个包含 135 行和 968 个特征的 DataFrame,来自tsfresh默认返回的原始 3,945 个特征(您可以通过执行features.shape来检查这一点)。继续使用此 DataFrame 来训练另一个逻辑回归模型以预测办公室占用。
如何工作...
在这个菜谱中,我们从时间序列中创建了数百个特征,然后根据非参数统计测试选择了最相关的特征。特征创建和选择过程由tsfresh自动执行。
为了创建特征,我们使用了tsfresh的extract_features函数,我们在从时间序列自动提取数百个特征菜谱中对其进行了详细描述。
为了选择特征,我们使用了来自 tsfresh 的 select_features 函数。这个函数根据特征和目标的不同性质应用不同的统计测试。简而言之,如果特征和目标是二元的,它通过费舍尔精确检验测试它们之间的关系。如果特征或目标是二元的,而另一个变量是连续的,它通过使用 Kolmogorov-Smirnov 检验测试它们之间的关系。如果特征和目标都不是二元的,它使用 Kendall 排序检验。
这些测试的结果是一个向量,每个特征都有一个 p 值。接下来,tsfresh 应用 Benjamini-Yekutieli 程序,旨在降低假发现率,根据 p 值选择要保留的特征。这个特征选择程序有一些优点,主要优点是统计测试计算速度快,因此选择算法可扩展且可并行化。另一个优点是测试是非参数的,因此适用于线性和非线性模型。
然而,评估每个特征单独的特征选择方法无法移除冗余特征。实际上,tsfresh 自动创建的许多特征将高度相关,例如那些捕捉到不同量级的光消费的特征。因此,它们将显示相似的 p 值并被保留。但在实践中,我们只需要一个或少数几个来捕捉时间序列的信息。我建议在 tsfresh 选择程序之后跟进其他能够识别特征交互的特征选择方法。
最后,在 步骤 8 中,我们通过使用 extract_relevant_features 函数将特征创建步骤 (步骤 3) 与特征选择步骤 (步骤 4) 结合起来。extract_relevant_features 函数将 extract_features 函数应用于每个时间序列以创建特征,并进行插补。接下来,它应用 select_features 函数以返回一个包含每个唯一标识符的行和一个为每个时间序列选择的特征的 DataFrame。请注意,对于不同的时间序列,可以选择不同的特征。
参见
tsfresh 中的选择算法提供了一种快速的方法来移除无关特征。然而,它并不能找到分类或回归任务的最佳特征子集。可以在 tsfresh 算法之后应用其他特征选择方法来进一步减少特征空间。
想要了解更多关于特征选择算法的细节,请查看 Soledad Galli 在 Leanpub 上出版的书籍《Python 机器学习中的特征选择》:leanpub.com/feature-selection-in-machine-learning/。
从不同的时间序列中提取不同的特征
tsfresh 基于时间序列的特征和分布提取许多特征,例如它们的关联属性、平稳性和熵。它还应用非线性时间序列分析函数,例如通过傅里叶或小波变换分解时间序列信号。根据时间序列的性质,这些变换中的一些比其他的有意义。例如,波长分解方法对于由信号或传感器产生的时间序列可能是有意义的,但并不总是对表示销售或股价的时间序列有用。
在这个食谱中,我们将讨论如何优化特征提取过程,从每个时间序列中提取特定特征,然后使用这些特征来预测办公室的占用情况。
如何操作...
tsfresh 通过包含方法名称作为键的字典访问将用于创建特征的函数,如果它们需要参数,则参数作为值。tsfresh 还包含一些预定义的字典。我们将首先探索这些预定义的字典,这些字典可以通过 settings 模块访问:
-
让我们导入所需的 Python 库、函数和
settings模块: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 -
加载数据集和 技术要求 部分中描述的目标变量:
X = pd.read_csv("occupancy.csv", parse_dates=["date"]) y = pd.read_csv("occupancy_target.csv", index_col="id")["occupancy"]tsfresh包含三个主要的字典来控制特征创建输出:settings.ComprehensiveFCParameters、settings.EfficientFCParameters和settings.MinimalFCParameters。在这里,我们将探索返回最少特征的字典。您可以重复这些步骤来探索其他字典。 -
显示在字典返回最少特征时将应用的特征创建方法:
minimal_feat = settings.MinimalFCParameters() minimal_feat.items()在 步骤 3 的输出中,我们看到一个以特征提取方法名称为键,以这些方法使用的参数(如果有)为值的字典:
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})
注意
好吧,通过调整 步骤 3 中的代码,继续探索其他两个预定义的字典,settings.ComprehensiveFCParameters 和 settings.EfficientFCParameters。
-
现在,让我们使用 步骤 3 中的字典从
light时间序列中提取仅这些特征,然后显示生成的 DataFrame 的形状:features = extract_features( X[[«id», «light»]], column_id="id", default_fc_parameters=minimal_feat, ) features.shape步骤 4 的输出是
(135, 10),这意味着为 135 小时的光照消耗数据中每个数据点只创建了 10 个特征。 -
让我们显示生成的 DataFrame:
features.head()在以下 DataFrame 中,我们看到前五小时光照消耗生成的特征值:

图 10.7 – 为每小时光照消耗创建的 DataFrame
现在,我们将使用这些特征来训练一个逻辑回归模型,以预测办公室是否被占用。
-
让我们先从将数据集分为训练集和测试集开始:
X_train, X_test, y_train, y_test = train_test_split( features, y, test_size=0.1, random_state=42, ) -
现在,让我们设置并训练一个逻辑回归模型,然后评估其性能:
cls = LogisticRegression(random_state=10, C=0.01) cls.fit(X_train, y_train) print(classification_report( y_test, cls.predict(X_test)))在以下输出中,我们看到的是用于分类分析的常用评估指标。这些指标表明所选特征对预测办公室占用率是有用的:
precision recall f1-score support 0 1.00 0.91 0.95 11 1 0.75 1.00 0.86 3 accuracy 0.93 14 macro avg 0.88 0.95 0.90 14 weighted avg 0.95 0.93 0.93 14
注意
因为光照消耗是办公室占用率的一个非常好的指标,通过非常简单的特征,我们可以获得一个预测性的逻辑回归模型。
现在,让我们学习如何为不同的时间序列指定创建不同特征的方法。
-
让我们创建一个包含我们想要用于从
light时间序列创建特征的名称的字典。我们将方法名称作为键,如果方法需要参数,我们将它作为额外的字典传递给相应的键;否则,我们传递None作为值:light_feat = { «sum_values": None, "median": None, «standard_deviation": None, "quantile": [{"q": 0.2}, {"q": 0.7}], } -
现在,让我们创建一个包含从
co2时间序列创建的特征的字典:co2_feat = { «root_mean_square": None, «number_peaks": [{"n": 1}, {"n": 2}], } -
让我们将这些字典合并到一个新的字典中:
kind_to_fc_parameters = { «light»: light_feat, "co2": co2_feat, } -
最后,让我们使用第 10 步的字典从两个时间序列中创建特征:
features = extract_features( X[[«id», «light», «co2»]], column_id="id", kind_to_fc_parameters=kind_to_fc_parameters, )第 11 步的输出是一个包含 135 行和 8 个特征的 DataFrame。如果我们执行
features.columns,我们将看到创建的特征的名称:Index(['light__sum_values', 'light__median', 'light__standard_deviation', 'light__quantile__q_0.2', 'light__quantile__q_0.7', 'co2__root_mean_square', 'co2__number_peaks__n_1', 'co2__number_peaks__n_2'], dtype='object')
注意,在第 11 步的输出中,从light和co2时间序列中分别创建了不同的变量。
它是如何工作的...
在这个菜谱中,我们从我们的时间序列数据中提取了特定的特征。首先,我们根据tsfresh附带的一个预定义字典创建了特征。接下来,我们创建了自己的字典,指定为不同的时间序列创建不同的特征。
tsfresh包附带了一些预定义的字典,可以通过settings模块访问。MinimalFCParameters字典用于根据时间序列分布的基本统计参数(如平均值、中位数、标准差、方差、值的总和、计数(或长度)、最小值和最大值)创建 10 个简单特征。在第 3 步中,我们展示了这个字典,其中方法名称作为键,由于这些方法不需要额外的参数,每个键的值都是None。
tsfresh有两个额外的预定义字典。EfficientFCParameters用于应用计算速度快的方法,而ComprehensiveFCParameters返回所有可能的特征,并且是extract_features函数默认使用的。
注意
更多关于预定义字典的详细信息,请查看tsfresh的文档:tsfresh.readthedocs.io/en/latest/text/feature_extraction_settings.html
通过在 tsfresh 的 extract_features 函数的 default_fc_parameters 参数中使用这些预定义的字典,我们可以从一个或多个时间序列中创建特定特征,就像我们在 步骤 4 中做的那样。请注意,default_fc_parameters 指示 extract_features 从 所有 时间序列中创建相同的特征。如果我们想从不同的时间序列中提取不同的特征怎么办?
为了为不同的时间序列创建不同的特征,我们可以使用 tsfresh 的 extract_features 函数的 kind_to_fc_parameters 参数。该参数接受一个字典的字典,指定应用于每个时间序列的方法。
在 步骤 8 中,我们创建了一个字典来指定从 light 时间序列创建特定特征。请注意,"sum_values" 和 "mean" 方法使用 None 作为值,但 quantile 方法需要额外的参数,对应于应从时间序列返回的分位数。在 步骤 9 中,我们创建了一个字典来指定从 co2 时间序列创建特征。在 步骤 10 中,我们将这两个字典合并为一个,以时间序列的名称作为键,特征创建字典作为值。然后,我们将这个字典传递给 tsfresh 的 extract_features 函数的 kind_to_fc_parameters 参数。如果使用领域知识创建特征,或者只创建少量特征,这种方式指定特征是合适的。
如果我们想为各种时间序列创建多个特征,是否需要手动将每个方法键入字典中?实际上并不需要。在下面的食谱中,我们将学习如何根据 Lasso 选定的特征指定要创建哪些特征。
创建通过特征选择识别的特征子集
在 从时间序列数据自动创建和选择预测特征 食谱中,我们学习了如何使用 tsfresh 选择相关特征。我们还讨论了 tsfresh 选择过程的局限性,并建议采用替代特征选择方法来识别预测特征,同时避免冗余。
在本食谱中,我们将使用 tsfresh 创建和选择特征。之后,我们将通过利用 Lasso 正则化进一步减少特征空间。然后,我们将学习如何从选定的特征名称创建字典,以触发仅从未来的时间序列创建这些特征。
如何操作...
让我们先导入必要的库并准备数据集:
-
让我们导入所需的库和函数:
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 -
加载在 技术要求 部分描述的 占用检测 数据集:
X = pd.read_csv("occupancy.csv", parse_dates=["date"]) y = pd.read_csv( "occupancy_target.csv", index_col="id")["occupancy"] -
从我们的五个时间序列中创建和选择特征,然后显示结果 DataFrame 的形状:
features = extract_relevant_features( X, y, column_id="id", column_sort="date", ) features.shape步骤 3 的输出是
(135, 968),表示从五个原始时间序列中返回了 968 个特征,对应于每小时的记录。
注意
我们在 从时间序列数据自动创建和选择预测特征 菜谱中讨论了 步骤 3 的函数。
让我们通过选择具有 Lasso 正则化的特征进一步减少特征空间。
-
设置具有 Lasso 正则化的逻辑回归,这是
"l1"惩罚。我还随意设置了一些额外的参数:cls = LogisticRegression( penalty="l1", solver=»liblinear", random_state=10, C=0.05, max_iter=1000, ) -
让我们设置一个转换器来保留那些逻辑回归系数不等于 0 的特征:
selector = SelectFromModel(cls) -
训练逻辑回归模型并选择特征:
selector.fit(features, y) -
现在,将所选特征捕获到一个变量中:
features = selector.get_feature_names_out()如果我们执行
features,我们将看到所选特征的名字: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) -
为了只从时间序列中提取 步骤 6 的特征,我们需要在字典中捕获特征创建方法名称和相应的参数。我们可以使用
tsfresh自动完成此操作:kind_to_fc_parameters = settings.from_columns( selector.get_feature_names_out(), )如果我们执行
kind_to_fc_parameters,我们将看到从 步骤 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} } -
现在,我们可以使用 步骤 8 的字典与
extract_features函数一起创建我们数据集中的特征:features = extract_features( X, column_id="id", column_sort="date", kind_to_fc_parameters=kind_to_fc_parameters, )
新的 DataFrame,可以通过执行 features.head() 来显示,仅包含由 Lasso 选出的 12 个特征。请在你自己的计算机上验证这个结果。
它是如何工作的...
在这个菜谱中,我们从 5 个时间序列创建了 968 个特征。接下来,我们使用 Lasso 正则化将特征空间减少到 12 个特征。最后,我们在字典中捕获了所选特征的规格,以便在未来的操作中,我们只创建来自我们时间序列的特征。
要自动创建和选择 tsfresh 的特征,我们使用了 extract_relevant_features 函数,我们已在 从时间序列数据自动创建和选择预测特征 菜谱中详细描述了该函数。
Lasso 正则化具有将逻辑回归模型的一些系数减少到 0 的内在能力。系数为 0 的特征对办公室占用预测的贡献为零,因此可以被移除。SelectFromModel() 类可以识别并移除这些特征。我们使用具有 Lasso 正则化的逻辑回归模型设置了一个 SelectFromModel() 实例来找到模型系数。通过 fit(),SelectFromModel() 使用我们从时间序列创建的 968 个特征训练了逻辑回归模型,并识别了那些系数不等于 0 的特征。然后,通过 get_feature_names_out() 方法,我们在一个新变量中捕获了所选特征的名字。
为了只创建由 Lasso 正则化选择的 12 个特征,我们使用tsfresh的from_columns()函数从变量名创建了一个字典。此函数返回一个字典,其中的键是从中选择了特征的变量,值是包含用于创建特征的方法的键和(如果有的话)参数的值。为了创建新特征,我们使用此字典与extract_features函数一起。
注意
在步骤 9中,我们将整个数据集传递给了extract_features函数。结果特征只包含从五个时间序列中的三个提取的特征。另外两个时间序列被忽略了。
将特征创建嵌入到 scikit-learn 管道中
在本章中,我们讨论了如何通过利用tsfresh自动创建和选择时间序列数据中的特征。然后,我们使用这些特征来训练一个分类模型,以预测在任何给定小时办公室是否被占用。
tsfresh在其主要函数extract_features和extract_relevant_features周围包含包装器类,以便特征创建和选择与 scikit-learn 管道兼容。
在这个菜谱中,我们将设置一个 scikit-learn 管道,使用tsfresh从时间序列中提取特征,然后使用这些特征训练一个逻辑回归模型来预测办公室的占用情况。
如何做到这一点...
让我们首先导入必要的库并准备好数据集:
-
让我们导入所需的库和函数:
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) -
加载技术要求部分中描述的占用检测数据集:
X = pd.read_csv("occupancy.csv", parse_dates=["date"]) y = pd.read_csv( "occupancy_target.csv", index_col="id")["occupancy"] -
创建一个包含目标变量索引的空 DataFrame:
tmp = pd.DataFrame(index=y.index) -
现在,让我们将步骤 3中的 DataFrame 和步骤 2中的目标分割成训练集和测试集:
X_train, X_test, y_train, y_test = train_test_split( tmp, y, random_state=0)
注意
X_train和X_test将用作容器来存储tsfresh创建的特征。它们对于我们将要讨论的RelevantFeatureAugmenter()的功能是必需的。
-
让我们创建一个字典,指定从每个时间序列中提取的特征(我任意定义了以下特征):
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}, }我们在从不同时间序列提取不同特征菜谱中讨论了此字典的参数。
-
让我们设置
RelevantFeatureAugmenter(),这是一个围绕extract_relevant_features函数的包装器,以创建步骤 5中指定的特征:augmenter = RelevantFeatureAugmenter( column_id="id", column_sort="date", kind_to_fc_parameters=kind_to_fc_parameters, )
注意
要创建所有可能的特征,请在步骤 6中使用FeatureAugmenter()类。
-
让我们将步骤 6中的特征创建实例与逻辑回归模型结合到一个 scikit-learn 管道中:
pipe = Pipeline( [ ("augmenter", augmenter), («classifier», LogisticRegression( random_state=10, C=0.01)), ] ) -
现在,让我们告诉
RelevantFeatureAugmenter()它需要使用哪个数据集来创建特征:pipe.set_params(augmenter__timeseries_container=X) -
让我们拟合管道,这将触发特征创建过程,然后训练逻辑回归模型:
pipe.fit(X_train, y_train) -
现在,让我们使用测试集中的时间序列来获取预测,并通过分类报告评估模型性能:
print(classification_report( y_test, pipe.predict(X_test)))我们可以在这里看到步骤 10的输出:
precision recall f1-score support 0 1.00 0.96 0.98 28 1 0.86 1.00 0.92 6 accuracy 0.97 34 macro avg 0.93 0.98 0.95 34 weighted avg 0.97 0.97 0.97 34
分类报告的值表明,提取的特征适合预测在任何给定小时办公室是否被占用。
它是如何工作的...
在这个配方中,我们将从时间序列创建特征与使用 scikit-learn 库训练机器学习算法的管道结合起来。
tsfresh库在其主要函数周围包含两个包装类,以使特征创建过程与 scikit-learn 管道兼容。在这个配方中,我们使用了RelevantFeatureAugmenter()类,它包装了extract_relevant_features函数,用于从时间序列创建并选择特征。
RelevantFeatureAugmenter()的工作方式如下;使用fit(),它通过extract_relevant_features创建并选择特征。所选特征的名称随后存储在转换器内部。使用transform(),RelevantFeatureAugmenter()从时间序列创建所选特征。
我们通过传递一个包含我们想要创建的特征的字典到其kind_to_fc_parameters参数,覆盖了RelevantFeatureAugmenter()的默认功能。因此,使用transform(),RelevantFeatureAugmenter()从时间序列创建了指定的特征。
要从时间序列创建所有特征,tsfresh包括FeatureAugmenter()类,它具有与RelevantFeatureAugmenter()相同的功能,但没有特征选择步骤。
RelevantFeatureAugmenter()和FeatureAugmenter()都需要两个 DataFrame 来工作。第一个 DataFrame 包含时间序列数据和唯一标识符(我们在步骤 2中加载了这个 DataFrame)。第二个 DataFrame 应该是空的,并包含其索引中的唯一标识符(我们在步骤 3中创建了此 DataFrame)。特征是从包含时间序列的第一个 DataFrame 中提取的(在应用transform()时),然后添加到第二个 DataFrame 中,然后用于训练逻辑回归或获取其预测。
注意
空 DataFrame 的索引被RelevantFeatureAugmenter()和FeatureAugmenter()用于识别从中提取特征的时间序列。因此,在传递X_train时应用fit(),从id值在训练集中的时间序列中提取了特征。之后,通过观察使用测试集做出的预测来评估模型,这触发了从id值在X_test中的时间序列创建特征。
当我们在管道上使用fit()时,我们从原始时间序列创建了特征,并使用这些特征训练了一个逻辑回归模型。使用predict()方法,我们从测试集创建了特征,并基于这些特征获得了逻辑回归的预测。
参见
如需了解此配方中使用的类和程序的更多详细信息,请访问以下链接:
第十一章:从文本变量中提取特征
文本可以是我们数据集中的一个变量。例如,在保险领域,描述事故情况的详细信息可能来自表单中的自由文本字段。如果一家公司收集客户评价,这些信息将以用户提供的短文本片段的形式收集。文本数据不显示我们在这本书中一直处理的数据集的 表格 模式。相反,文本中的信息在长度、内容和写作风格上可能有所不同。我们可以从文本变量中提取大量信息,用作机器学习模型中的预测特征。本章中我们将涵盖的技术属于 自然语言处理 (NLP) 的领域。NLP 是语言学和计算机科学的一个子领域,它关注计算机与人类语言之间的交互,换句话说,就是如何编程计算机理解人类语言。NLP 包括理解文本的句法、语义和语篇的众多技术。因此,要公正地对待这个领域,就需要一本整本书。
在本章中,我们将讨论允许我们快速从短文本片段中提取特征以补充我们的预测模型的方法。具体来说,我们将讨论如何通过查看文本的一些统计参数来捕捉文本的复杂性,例如单词长度和计数、使用的单词和唯一单词的数量、句子的数量等等。我们将使用 pandas 和 scikit-learn 库,并且我们将对一个非常有用的 Python NLP 工具包进行浅入浅出的探讨,该工具包称为 自然语言 工具包 (NLTK)。
本章包括以下食谱:
-
计算字符、单词和词汇量
-
通过计算句子来估计文本复杂度
-
使用词袋和 n-gram 创建特征
-
实现词频-逆文档频率
-
清洗和词干提取文本变量
技术要求
在本章中,我们将使用 pandas、matplotlib 和 scikit-learn Python 库。我们还将使用 NLTK,这是一个全面的 Python NLP 和文本分析库。你可以在 www.nltk.org/install.html 找到安装 NLTK 的说明。
如果你使用的是 Python Anaconda 发行版,请按照 anaconda.org/anaconda/nltk 上的说明安装 NLTK。
在你安装了 NLTK 之后,打开一个 Python 控制台并执行以下命令:
import nltk
nltk.download('punkt')
nltk.download('stopwords')
这些命令将为你下载运行本章中食谱所需的数据。
注意
如果你还没有下载这些或其他必要的 NLTK 功能数据源,NLTK 将会引发错误。仔细阅读错误信息,因为它会指导你下载运行你试图执行的命令所需的数据。
计算字符、单词和词汇量
文本的一个显著特征是其复杂性。长描述比短描述更有可能包含更多信息。包含不同、独特词汇的文本比反复重复相同词汇的文本更可能细节丰富。同样,当我们说话时,我们使用许多短词,如冠词和介词来构建句子结构,而主要概念通常来自我们使用的名词和形容词,这些往往是较长的词。所以,正如你所看到的,即使不阅读文本,我们也可以通过确定单词数量、唯一单词数量(单词的非重复出现)、词汇多样性和这些单词的长度来推断文本提供的信息量。在这个菜谱中,我们将学习如何使用pandas从文本变量中提取这些特征。
准备工作
我们将使用包含大约 18,000 篇关于 20 个不同主题的新闻的scikit-learn。更多关于这个数据集的详细信息可以在以下网站上找到:
-
scikit-learn 数据集网站:
scikit-learn.org/stable/datasets/real_world.html#newsgroups-dataset -
20 个新闻组数据集的主页:
qwone.com/~jason/20Newsgroups/
在深入菜谱之前,让我们讨论一下我们将从这些文本片段中提取的特征。我们提到,较长的描述、文章中的更多单词、更多唯一词汇和较长的单词往往与文章提供的信息量相关。因此,我们可以通过提取以下关于文本的信息来捕捉文本复杂性:
-
总字符数
-
总单词数
-
唯一单词的总数
-
词汇多样性(总单词数除以唯一单词数)
-
单词平均长度(字符数除以单词数)
在这个菜谱中,我们将使用pandas提取这些数值特征,pandas具有广泛的字符串处理功能,可以通过str向量化字符串函数访问。
如何实现...
让我们先加载pandas并准备好数据集:
-
加载
pandas和来自scikit-learn的数据集:import pandas as pd from sklearn.datasets import fetch_20newsgroups -
让我们将 20 个新闻组数据集的火车集部分加载到一个
pandasDataFrame 中:data = fetch_20newsgroups(subset='train') df = pd.DataFrame(data.data, columns=['text'])
小贴士
您可以通过执行print(df['text'][1])来打印 DataFrame 中的文本示例。通过更改[和]之间的数字来显示不同的文本。注意,每个文本描述都是一个由字母、数字、标点和空格组成的单个字符串。您可以通过执行type(df["text"][1])来检查数据类型。
现在我们已经将文本变量放入一个pandas DataFrame 中,我们准备提取特征。
-
让我们在新列中捕捉每个文本片段中的字符数:
df['num_char'] = df['text'].str.len()
小贴士
在计数字符之前,您可以通过在len()方法之前添加strip()方法来移除字符串末尾的空白字符,包括新行中的空白字符,如下所示:df['num_char'] = df['text'].str.strip().str.len()。
-
让我们在新列中捕捉每个文本中的单词数量:
df['num_words'] = df['text'].str.split().str.len()要计算单词数量,我们使用
pandas库的split()方法,该方法在空白处分割文本。例如,通过执行df["text"].loc[1].split()来分离 DataFrame 中第二个文本的单词。 -
让我们在新列中捕捉每个文本中的唯一单词数量:
df['num_vocab']df[ 'text'].str.lower().str.split().apply( set).str.len()
注意
如果一个单词的首字母大写,Python 会将它解释为两个不同的单词。为了避免这种行为,我们可以在split()方法之前应用lower()方法。
-
让我们创建一个特征来捕捉词汇多样性 – 即总单词数(步骤 4)与唯一单词数(步骤 5)的对比:
df['lexical_div'] = df['num_words'] / df['num_vocab'] -
让我们通过将字符数(步骤 3)除以单词数(步骤 4)来计算平均单词长度:
df['ave_word_length'] = df[ 'num_char'] / df['num_words']如果我们执行
df.head(),我们将看到包含文本和新建特征的前五行数据:

图 11.1 – 包含文本变量和总结文本某些特征的 DataFrame
这样,我们已经提取了五个不同的特征来捕捉文本复杂性,我们可以将这些特征用作机器学习算法的输入。
注意
在这个菜谱中,我们直接从原始数据中创建了新特征,而没有进行任何数据清理、移除标点符号,甚至没有进行词干提取。请注意,这些步骤是在大多数标准 NLP 过程之前执行的。要了解更多信息,请访问本章末尾的清洗和词干提取文本变量菜谱。
它是如何工作的...
在这个菜谱中,我们通过使用pandas的str来访问内置的pandas字符串功能,创建了五个新特征来捕捉文本复杂性。我们处理了scikit-learn附带的数据集20 Newsgroup的train子集的文本列。这个数据集中的每一行都是一个包含文本的字符串。
我们使用pandas的str,然后是len(),来计算每个字符串中的字符数 – 即字母、数字、符号和空格的总数。我们还结合了str.len()和str.strip()来移除字符串开头和结尾的空白字符以及在新行中的空白字符,在计数字符之前。
要计算单词数量,我们使用pandas的str,然后是split(),将字符串分割成单词列表。split()方法通过在单词之间的空白处断开字符串来创建单词列表。接下来,我们使用str.len()来计数这些单词,得到每个字符串的单词数。
注意
我们可以通过传递一个字符串或字符来改变str.split()的行为,我们希望用它来分割字符串。例如,df['text'].str.split(';')在;的每个出现处分割字符串。
要确定唯一单词的数量,我们使用了 pandas 的str.split()函数将字符串分割成单词列表。接下来,我们在 pandas 的apply()方法中应用了内置的 Python set()方法,以返回一个单词集合。记住,集合包含列表中元素的唯一出现 – 那就是唯一单词。接下来,我们使用 pandas 的str.len()函数来计数这些单词,并返回lower()函数,在分割字符串和计数唯一单词之前将所有字符设置为小写。
要创建词汇多样性和平均词长特征,我们只是简单地执行了两个pandas序列的向量除法。就是这样;我们创建了五个关于文本复杂性的新特征。
更多内容...
我们可以通过使用可视化来检查数据集中 20 个不同新闻主题中提取的文本特征的分布。
在运行本食谱中“如何工作...”部分的全部步骤之后,要制作新创建特征的直方图,请遵循以下步骤:
-
导入
matplotlib:import matplotlib.pyplot as plt -
将新闻主题的目标添加到 20 个新闻组 DataFrame 中:
df['target'] = data.target -
创建一个函数,用于显示每个新闻主题中你选择的特征的直方图:
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() -
运行单词数量特征的函数:
plot_features(df, 'num_words')之前的命令返回以下图表,其中你可以看到 20 个新闻主题中每个主题的单词数量分布,图表标题中从 0 到 19 编号:

图 11.2 – 每个文本中单词数量的分布直方图,按每个文本中讨论的主题进行细分
单词数量在不同新闻主题中显示出不同的分布。因此,这个特征可能在分类算法中预测文本主题时很有用。
参见
要了解更多关于 pandas 内置字符串处理功能的信息,请访问pandas.pydata.org/pandas-docs/stable/user_guide/text.html#method-summary。
通过计算句子来估计文本复杂性
我们可以在特征中捕捉到的一篇文本的一个方面是其复杂性。通常,包含多个句子并分布在几个段落中的较长的描述,比包含非常少句子的描述提供的信息更多。因此,捕捉句子的数量可能有助于了解文本提供的信息量。这个过程被称为NLTK Python 库,它提供了这个功能。
准备工作
在这个菜谱中,我们将使用NLTK Python 库。有关如何安装NLTK的指南,请参阅本章的技术要求部分。
如何做到...
让我们先导入所需的库和数据集:
-
让我们加载
pandas、NLTK中的句子分词器和scikit-learn中的数据集:import pandas as pd from nltk.tokenize import sent_tokenize from sklearn.datasets import fetch_20newsgroups -
为了理解
NLTK中的句子分词器的功能,让我们创建一个包含多个句子的字符串变量: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. """ -
现在,让我们使用
NLTK库的句子分词器将步骤 2 中的字符串拆分为句子:sent_tokenize(text)
小贴士
如果你在步骤 3 中遇到错误,请仔细阅读错误消息并下载NLTK所需的数据源,如错误消息中所述。更多详情,请参阅技术 要求部分。
句子分词器返回以下输出中显示的句子列表:
['\nThe alarm rang at 7 in the morning as it usually did on Tuesdays.',
'She rolled over,\nstretched 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\ndid 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\nas he saw her climb the stairs.']
注意
后跟字母的转义字符\n表示新的一行。
-
让我们计算
text变量中的句子数:len(sent_tokenize(text))之前的命令返回
8,这是我们的text变量中的句子数。现在,让我们确定整个 DataFrame 中的句子数。 -
让我们将 20 个新闻组数据集的
train子集加载到pandasDataFrame 中:data = fetch_20newsgroups(subset='train') df = pd.DataFrame(data.data, columns=['text']) -
为了加快以下步骤,我们只处理 DataFrame 的前
10行:df = df.loc[1:10] -
让我们也移除文本的第一部分,这部分包含关于电子邮件发送者、主题和其他我们不感兴趣的信息。大部分这些信息都在
Lines这个词之后,跟着一个冒号:,所以让我们在Lines:处拆分字符串并捕获字符串的第二部分:df['text'] = df['text'].str.split('Lines:').apply( lambda x: x[1]) -
最后,让我们创建一个包含每
text中句子数的变量:df['num_sent'] = df['text'].apply( sent_tokenize).apply(len)使用
df命令,你可以通过text变量和新特性(包含每段文本的句子数)显示整个 DataFrame:

图 11.3 – 包含文本变量和每段文本句子数的 DataFrame
现在,我们可以使用这个新特性作为机器学习算法的输入。
它是如何工作的...
在这个菜谱中,我们使用NLTK库的sent_tokenizer将包含文本的字符串拆分为句子。sent_tokenizer已经预先训练以识别大写字母和不同类型的标点符号,这些标点符号标志着句子的开始和结束。
首先,我们手动创建一个字符串并应用sent_tokenizer以熟悉其功能。分词器将文本分为一个包含八个句子的列表。我们将分词器与内置的 Python len()方法结合使用,以计算字符串中的句子数。
接下来,我们加载了一个包含文本的数据集,为了加快计算速度,我们只使用 pandas 的loc[]函数保留了 DataFrame 的前 10 行。接下来,我们移除了文本的前一部分,这部分包含了关于电子邮件发送者和主题的信息。为此,我们使用 pandas 的str.split("Lines:")函数在Lines:处分割字符串,该函数返回一个包含两个元素的列表:Lines:之前的字符串和之后的字符串。利用apply()中的 lambda 函数,我们保留了文本的第二部分,即split()返回的列表中的第二个字符串。
最后,我们使用 pandas 的apply()方法将sent_tokenizer应用于 DataFrame 中的每一行,将字符串分割成句子,然后使用内置的 Python len()方法应用于句子列表,以返回每个字符串的句子数。这样,我们创建了一个包含每个文本的句子数的新特征。
更多内容...
NLTK除了其他有用功能外,还具有单词分词功能,我们可以用NLTK代替pandas来计数并返回单词数。你可以在这里了解更多关于NLTK功能的信息:
-
《使用 NLTK 3 进行 Python 3 文本处理食谱》,作者:雅各布·珀金斯,Packt 出版社
-
www.nltk.org/上的NLTK文档。
使用词袋模型和 n-gram 创建特征
词袋模型(Bag-of-Words,BoW)是对文本的一种简化表示,它捕捉了文本中出现的单词以及每个单词在文本中出现的次数。因此,对于文本字符串 Dogs like cats, but cats do not like dogs,得到的 BoW 如下:

图 11.4 – 从句子 Dogs like cats, but cats do not like dogs 导出的 BoW
在这里,每个单词成为一个变量,变量的值表示单词在字符串中出现的次数。正如你所见,BoW 捕捉了多重性,但没有保留单词顺序或语法。这就是为什么它是一种简单而有用的方式来提取特征并捕捉我们正在处理的文本的一些信息。
为了捕捉一些语法,BoW 可以与n-gram一起使用。n-gram 是在给定文本中连续的n个项的序列。继续使用句子 Dogs like cats, but cats do not like dogs,得到的 2-gram 如下:
-
狗喜欢
-
喜欢猫
-
猫和
-
但是
-
不
-
像狗一样
我们可以创建一个与 BoW 一起的 n-gram 袋,其中额外的变量由 2-gram 给出,每个 2-gram 的值是它们在每个字符串中出现的次数;对于这个例子,值是 1。因此,我们的最终包含 2-gram 的 BoW 看起来如下:

图 11.5 – 包含 2-gram 的 BoW
在这个菜谱中,我们将学习如何使用scikit-learn创建带有或不带有 n-gram 的 BoW。
准备工作
在深入这个菜谱之前,让我们熟悉一下 BoW 的一些参数,我们可以调整这些参数以使 BoW 更全面。在创建多个文本片段的 BoW 时,对于我们在分析的文本片段中至少出现一次的每个唯一单词,都会创建一个新特征。如果单词只在一个文本片段中出现,它将为该特定文本显示 1 的值,而为其他所有文本显示 0。因此,BoWs 往往是稀疏矩阵,其中大部分值都是零。
如果我们处理大量的文本语料库,BoW 中的列数(即单词数)可以相当大,如果我们还包括 n-gram,则更大。为了限制列数和返回矩阵的稀疏性,我们可以保留在多个文本中出现的单词;或者换句话说,我们可以保留至少在某个百分比文本中出现的单词。
为了减少列数和 BoW 的稀疏性,我们还应该处理与 Python 识别单词时相同大小写的单词——例如,小写。我们还可以通过移除停用词来减少列数和稀疏性。停用词是非常常用的单词,使句子流畅,但本身并不携带任何有用的信息。停用词的例子包括代词,如我、你和他,以及介词和冠词。
在这个菜谱中,我们将学习如何将单词转换为小写,移除停用词,保留具有最低可接受频率的单词,并使用scikit-learn的单个转换器CountVectorizer()一起捕获 n-gram:
如何操作...
让我们先加载必要的库并准备好数据集:
-
加载
pandas、CountVectorizer以及从scikit-learn的 dataset:import pandas as pd from sklearn.datasets import fetch_20newsgroups from sklearn.feature_extraction.text import ( CountVectorizer ) -
让我们将 20 个新闻组数据集的训练集部分加载到 pandas DataFrame 中:
data = fetch_20newsgroups(subset='train') df = pd.DataFrame(data.data, columns=['text']) -
为了使结果更容易解释,让我们从文本变量中移除标点符号和数字:
df['text'] = df['text'].str.replace( ‹[^\w\s]›,››, regex=True).str.replace( ‹\d+›,››, regex=True)
注意
要了解更多关于 Python 中的正则表达式,请点击此链接:docs.python.org/3/howto/regex.html
-
现在,让我们设置
CountVectorizer(),使其在创建 BoW 之前将文本转换为小写,移除停用词,并保留至少在 5%的文本片段中出现的单词:vectorizer = CountVectorizer( lowercase=True, stop_words='english', ngram_range=(1, 1), min_df=0.05)
注意
要将 n-gram 作为返回列的一部分引入,我们可以将ngrams_range的值更改为,例如(1,2)。这个元组提供了不同 n-gram 的 n 值范围的上下边界。在(1,2)的情况下,CountVectorizer()将返回单个单词和两个连续单词的数组。
-
让我们调整
CountVectorizer(),使其学习在 BoW 中应使用哪些单词:vectorizer.fit(df['text']) -
现在,让我们创建 BoW:
X = vectorizer.transform(df['text']) -
最后,让我们将相应的特征名称与 BoW 一起捕获到 DataFrame 中:
bagofwords = pd.DataFrame( X.toarray(), columns = vectorizer.get_feature_names_out() )通过这样,我们创建了一个包含单词作为列和每个文本中它们出现的次数作为值的
pandasDataFrame。你可以通过执行bagofwords.head()来检查结果:

图 11.6 – 由 20 个新闻组数据集生成的 BoW DataFrame
我们可以将这个 BoW 作为机器学习模型的输入。
它是如何工作的...
scikit-learn 的CountVectorizer()将一组文本文档转换为标记计数的矩阵。这些标记可以是单个单词或两个或更多连续单词的数组——即 n-gram。在这个菜谱中,我们从 DataFrame 中的文本变量创建了一个 BoW。
我们从scikit-learn加载了 20 个新闻组文本数据集,并使用 pandas 的replace()函数从文本行中移除了标点符号和数字,该函数可以通过 pandas 的str模块访问,用空字符串''替换数字'\d+'或符号'[^\w\s]'。然后,我们使用CountVectorizer()创建 BoW。我们将lowercase参数设置为True,在提取 BoW 之前将单词转换为小写。我们将stop_words参数设置为english以忽略停用词——也就是说,避免 BoW 中的停用词。我们将ngram_range设置为(1,1)元组,以仅返回作为列的单个单词。最后,我们将min_df设置为0.05,以返回至少出现在 5%的文本中的单词,换句话说,在 DataFrame 的 5%的行中。
在设置好转换器之后,我们使用了fit()方法让转换器找到满足先前标准的单词。最后,使用transform()方法,转换器返回一个包含 BoW 及其特征名称的对象,我们将它捕获在一个pandas DataFrame 中。
参见
更多关于CountVectorizer()的详细信息,请访问scikit-learn库的文档scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html。
实现词频-逆文档频率
词频-逆文档频率(TF-IDF)是一个数值统计量,它捕捉了单词在考虑整个文档集合的情况下在文档中的相关性。这意味着什么?一些单词在文本文档中以及跨文档中都会出现很多次,例如,英语单词 the、a 和 is 等。这些单词通常对文档的实际内容传达很少信息,并且不会使文本脱颖而出。TF-IDF 通过考虑单词在文档中出现的次数与在文档中出现的频率之间的关系来提供衡量单词重要性的方法。因此,常见的单词如 the、a 或 is 将具有较低的权重,而更具体于某个主题的单词,如 leopard,将具有更高的权重。
TF-IDF 是两个统计量的乘积:词频(tf)和逆文档频率(idf),表示如下:tf-idf = tf × idf。tf 在其最简单形式中,是单个文本中单词的计数。因此,对于术语 t,tf 的计算为 tf(t) = count(t),并且基于文本进行确定。idf 是衡量单词在 所有 文档中普遍程度的一个指标,通常在对数尺度上计算。一个常见的实现如下:

在这里,n 是文档的总数,df(t) 是包含术语 t 的文档数。df(t) 的值越大,该术语的权重越低。如果一个单词在文本中出现的次数很多(高 tf)或在文本中出现的次数很少(高 idf),则该单词的重要性会很高。
注意
TF-IDF 可以与 n-gram 结合使用。同样,为了权衡 n-gram,我们将某个文档中 n-gram 的频率与跨文档中 n-gram 的频率相乘。
在这个菜谱中,我们将学习如何使用 scikit-learn 通过 n-gram 或不使用 n-gram 提取 TF-IDF 特征。
准备工作
scikit-learn 使用一种稍微不同的方式来计算 IDF 统计量:

这种公式确保了出现在所有文本中的单词获得最低权重 1。此外,在计算每个单词的 TF-IDF 之后,scikit-learn将特征向量(包含所有单词的向量)归一化到其欧几里得范数。有关确切公式的更多详细信息,请访问scikit-learn.org/stable/modules/feature_extraction.html#tfidf-term-weighting的scikit-learn文档。
TF-IDF 在创建术语矩阵时具有与 BoW 相同的特征——即高特征空间和稀疏性。为了减少特征数量和稀疏性,我们可以移除停用词,将字符转换为小写,并保留在最小百分比观察中出现过的单词。如果您不熟悉这些术语,请访问本章中的使用词袋和 n-gram 创建特征菜谱进行复习。
在这个菜谱中,我们将学习如何将单词转换为小写,移除停用词,保留具有最低可接受频率的单词,捕获 n-gram,然后使用 scikit-learn 的单个转换器TfidfVectorizer()返回单词的 TF-IDF 统计量。
如何做到这一点...
让我们先加载必要的库并准备数据集:
-
加载
pandas、TfidfVectorizer()以及从scikit-learn中的数据集:import pandas as pd from sklearn.datasets import fetch_20newsgroups from sklearn.feature_extraction.text import ( TfidfVectorizer ) -
让我们将 20 个新闻组数据集的火车集部分加载到一个 pandas DataFrame 中:
data = fetch_20newsgroups(subset='train') df = pd.DataFrame(data.data, columns=['text']) -
为了使结果更容易解释,让我们从文本变量中移除标点符号和数字:
df['text'] = df['text'].str.replace( ‹[^\w\s]›,››, regex=True).str.replace( '\d+','', regex=True) -
现在,让我们设置
scikit-learn中的TfidfVectorizer(),以便在创建 TF-IDF 度量之前,将所有文本转换为小写,移除停用词,并保留至少在 5%的文本片段中出现的单词:vectorizer = TfidfVectorizer( lowercase=True, stop_words='english', ngram_range=(1, 1), min_df=0.05)
注意
为了将 n-gram 作为返回列的一部分引入,我们可以将ngrams_range的值更改为,例如(1,2)。这个元组提供了不同 n-gram 的 n 值范围的上下界。在(1,2)的情况下,TfidfVectorizer()将返回单个单词和两个连续单词的数组作为列。
-
让我们拟合
TfidfVectorizer(),以便它学习哪些单词应该作为 TF-IDF 矩阵的列引入,并确定单词的idf:vectorizer.fit(df['text']) -
现在,让我们创建 TF-IDF 矩阵:
X = vectorizer.transform(df['text']) -
最后,让我们将 TF-IDF 矩阵捕获到一个具有相应特征名称的 DataFrame 中:
tfidf = pd.DataFrame( X.toarray(), columns = vectorizer.get_feature_names_out() )通过这样,我们创建了一个包含单词作为列和 TF-IDF 作为值的
pandasDataFrame。您可以通过执行tfidf.head()来检查结果:

图 11.7 – 由 TF-IDF 产生的特征 DataFrame
现在,我们可以使用这个词频 DataFrame 来训练机器学习模型。
它是如何工作的...
在这个菜谱中,我们通过使用 scikit-learn 的TfidfVectorizer()提取了至少存在于 5%的文档中的单词的 TF-IDF 值。
我们从scikit-learn加载了 20 个新闻组文本数据集,然后使用 pandas 的replace()从文本行中删除了标点符号和数字,这可以通过 pandas 的str访问,用空字符串''替换数字'\d+'或符号'[^\w\s]'。然后,我们使用TfidfVectorizer()为单词创建 TF-IDF 统计信息。我们将lowercase参数设置为True,在计算之前将单词转换为小写。我们将stop_words参数设置为english,以避免返回矩阵中的停用词。我们将ngram_range设置为(1,1)元组,以返回单个单词作为特征。最后,我们将min_df参数设置为0.05,以返回至少出现在 5%的文本或换句话说,在 5%的行中的单词。
在设置完转换器后,我们应用了fit()方法,让转换器找到最终项矩阵中需要保留的单词。使用transform()方法,转换器返回了一个包含单词及其 TF-IDF 值的对象,然后我们将其捕获到一个具有适当特征名称的 pandas DataFrame 中。现在,我们可以将这些特征用于机器学习算法。
另请参阅
有关TfidfVectorizer()的更多详细信息,请访问 scikit-learn 的文档:scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html
清洗和词干化文本变量
我们数据集中的某些变量来自自由文本字段,这些字段由用户手动填写。人们有不同的写作风格,我们使用各种标点符号、大写模式、动词变位来传达内容以及与之相关的情感。我们可以通过创建总结文本复杂度、关键词和文档中单词相关性的统计参数来从文本中提取(一些)信息,而无需费心阅读它。我们已在本章前面的菜谱中讨论了这些方法。然而,为了推导这些统计信息和聚合特征,我们首先应该清理文本变量。
文本清理或预处理包括标点符号删除、停用词消除、字符大小写设置和词干提取。标点符号删除包括删除不是字母、数字或空格的字符;在某些情况下,我们也会删除数字。停用词消除是指删除在我们语言中用于允许句子结构和流畅性的常见单词,但它们各自传达的信息很少或没有。例如,英语中的停用词包括诸如the和a之类的冠词,以及诸如I、you和they之类的代词,以及各种变形中常用的动词,如to be和to have,以及诸如would和do之类的助动词。
为了让计算机正确识别单词,还需要将所有单词设置为相同的格式,因为由于第一个单词中的大写T,计算机会将Toy和toy识别为不同的单词。
最后,为了专注于文本的信息,我们不希望计算机在单词不同变形的情况下将单词视为不同。因此,我们将使用词干提取作为预处理流程的一部分。词干提取是指将每个单词还原为其词根或基础,使得单词playing、plays和played变为play,本质上传达的是相同或非常相似的意义。
在本食谱中,我们将学习如何使用 pandas 和NLTK删除标点符号、停用词,将单词转换为小写,并执行词干提取。
准备工作
我们将使用NLTK的词干提取包来执行词干提取,该包包含不同的算法,可以从英语和其他语言中提取单词。每种方法在寻找单词的根时使用的算法不同,因此它们可能会输出略微不同的结果。我建议您了解更多相关信息,尝试不同的方法,并选择适合您正在工作的项目的那个。
更多关于 NLTK 词干提取器的信息可以在www.nltk.org/api/nltk.stem.html找到。
如何操作...
让我们先加载必要的库并准备数据集:
-
从
NLTK加载pandas、stopwords和SnowballStemmer,以及从scikit-learn加载数据集:import pandas as pd from nltk.corpus import stopwords from nltk.stem.snowball import SnowballStemmer from sklearn.datasets import fetch_20newsgroups -
让我们先将 20 个新闻组数据集的训练集部分加载到 pandas DataFrame 中:
data = fetch_20newsgroups(subset='train') df = pd.DataFrame(data.data, columns=['text'])现在,让我们开始文本清理。
注意
在执行本食谱中的每个命令后,通过执行例如print(df['text'][10])之类的命令来打印一些示例文本,以便您可以可视化对文本引入的变化。现在就做吧,然后在每个步骤之后重复该命令。
-
让我们先删除标点符号:
df["text"] = df['text'].str.replace('[^\w\s]','')
小贴士
您还可以使用 Python 内置的string模块来删除标点符号。首先,通过执行import string来导入模块,然后执行df['text'] = df['text'].str.replace('[{}]'.format(string.punctuation), '')。
-
我们也可以移除数字字符,只留下字母,如下所示:
df['text'] = df['text'].str.replace( '\d+', '', regex=True) -
现在,让我们将所有单词转换为小写:
df['text'] = df['text'].str.lower()现在,让我们开始移除停用词的过程。
注意
如果你没有下载NLTK库的stopwords,步骤 6可能会失败。请访问本章的技术要求部分以获取更多详细信息。
-
让我们创建一个函数,该函数可以将字符串分割成单词列表,移除停用词,并将剩余的单词重新连接成字符串:
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
注意
为了能够使用scikit-learn库的CountVectorizer()或TfidfVectorizer()处理数据,我们需要文本以字符串格式存在。因此,在移除停用词后,我们需要将单词作为单个字符串返回。我们已经将 NLTK 库的停用词列表转换为一个集合,因为集合比列表扫描更快,这提高了计算时间。
-
现在,让我们使用第6 步中的函数从
text变量中移除停用词:df['text'] = df['text'].apply(remove_stopwords)如果你想知道哪些单词是停用词,请执行
stopwords.words('english')。最后,让我们使用
NLTK的SnowballStemer来提取我们数据中的词干。 -
让我们为英语语言创建一个
SnowballStemer实例:stemmer = SnowballStemmer("english")
小贴士
尝试在单个单词上使用词干提取器以查看其工作方式;例如,运行stemmer.stem('running')。你应该看到run作为该命令的结果。尝试不同的单词!
-
让我们创建一个函数,该函数可以将字符串分割成单词列表,对每个单词应用
stemmer,并将词干列表重新连接成字符串:def stemm_words(text): text = [ stemmer.stem(word) for word in text.split() ] text = ‹ ‹.join(x for x in text) return text -
让我们使用第9 步中的函数来对数据中的单词进行词干提取:
df['text'] = df['text'].apply(stemm_words)现在,我们的文本已经准备好根据字符和单词计数创建特征,以及创建 BoWs 或 TF-IDF 矩阵,正如本章前面的食谱所描述的。
如果我们执行
print(df['text'][10]),我们将在清理后看到一个文本示例: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
注意
如果你正在计算句子数,你需要在移除标点符号之前这样做,因为标点和大小写是定义每个句子边界的必要条件。
它是如何工作的...
在这个食谱中,我们从文本变量中移除了标点符号、数字和停用词,将单词转换为小写,并最终将单词还原到词根。我们使用 pandas 的replace()函数从文本变量中移除标点符号和数字,该函数可以通过 pandas 的str访问,用空字符串''替换数字'\d+'或符号'[^\w\s]'。或者,我们可以使用内置string包中的punctuation模块。
小贴士
在导入string后,在 Python 控制台中运行string.punctuation以查看将被替换为空字符串的符号。
接下来,通过 pandas 的字符串处理功能str,我们使用lower()方法将所有单词转换为小写。为了从文本中移除停用词,我们使用了NLTK中的stopwords模块,该模块包含了一组频繁出现的单词列表——即停用词。我们创建了一个函数,该函数接受一个字符串并使用 pandas 的str.split()将其拆分为单词列表,然后使用列表推导,我们遍历列表中的单词并保留非停用词。最后,使用join()方法,我们将保留的单词重新连接成字符串。我们使用 Python 内置的set()方法在NLTK停用词列表上提高计算效率,因为遍历集合比遍历列表要快。最后,通过 pandas 的apply(),我们将该函数应用于文本数据的每一行。
小贴士
在从NLTK导入stopwords后,在 Python 控制台中运行stopwords.words('english')以可视化将要被移除的停用词列表。
最后,我们使用NLTK中的SnowballStemmer对单词进行了词干提取。SnowballStemmer一次处理一个单词。因此,我们创建了一个函数,该函数接受一个字符串并使用 pandas 的str.split()将其拆分为单词列表。在列表推导中,我们逐个单词应用SnowballStemmer,然后使用join()方法将提取的词干单词列表重新连接成字符串。通过 pandas 的apply(),我们将该函数应用于 DataFrame 的每一行以提取词干。
在本食谱中我们执行的清洗步骤产生了包含原始文本的字符串,没有标点符号或数字,全部小写,没有常用词汇,并且使用词根而不是其变形。返回的数据可以用来推导特征,如计数字符、单词和词汇食谱中所述,或者创建 BoWs 和 TI-IDF 矩阵,如使用词袋和 n-gram 创建特征和实现词频-逆文档频率食谱中所述。
如本食谱所示,清洗文本可能会根据文本的特征导致数据丢失,如果我们希望在创建 BoW 或 TF-IDF 矩阵后解释模型,理解词干的重要性可能并不那么直接。


; 如果 λ ≠ 0 且 X >= 0
; 如果 λ ≠ 2 且 X < 0
浙公网安备 33010602011771号