Python 特征工程秘籍第三版(二)
原文:
annas-archive.org/md5/86f7f4009ce12f06cb3f3594f063591b译者:飞龙
第三章:转换数值变量
在数据分析中使用的数据统计方法对数据做出某些假设。例如,在一般线性模型中,假设因变量(目标)的值是独立的,目标变量与自变量(预测变量)之间存在线性关系,以及残差——即预测值与目标真实值之间的差异——是正态分布且中心在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的信息,请访问以下文档:
www.scikit-yb.org/en/latest/i…
进行等宽离散化
等宽离散化包括将变量的观测值范围划分为用户提供的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()
首先,让我们导入必要的 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 机器学习仓库(archive.ics.uci.edu/ml/datasets… 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. dx.doi.org/10.1016/j.j…
-
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