Scikit-learn 的 Python 机器学习库提供了一系列数据转换功能,用于改变输入数据的尺度和分布,以及移除输入特征(列)。
有许多简单的数据清理操作,例如移除异常值和移除观测值较少的列,这些操作通常需要手动进行,需要自定义代码。
Scikit-learn 库提供了一种标准化的方式来封装这些自定义数据转换,这样它们就可以像任何其他转换一样使用,可以直接用于数据,也可以作为建模管道的一部分。
在本教程中,您将了解如何为 scikit-learn 定义和使用自定义数据转换。
完成本教程后,您将了解:
- 可以使用 FunctionTransformer 类为 scikit-learn 创建自定义数据转换。
- 如何开发和应用自定义转换来移除唯一值较少的列。
- 如何开发和应用自定义转换来替换每列的异常值。
通过我的新书《机器学习数据准备》开始您的项目,其中包含分步教程以及所有示例的Python源代码文件。
让我们开始吧。

如何为 Scikit-Learn 创建自定义数据转换
照片来源:Berit Watkin,部分权利保留。
教程概述
本教程分为四个部分;它们是
- Scikit-Learn 中的自定义数据转换
- 溢油数据集
- 自定义转换以移除列
- 自定义转换以替换异常值
Scikit-Learn 中的自定义数据转换
数据准备是指以某种方式改变原始数据,使其更适合机器学习算法进行预测性建模。
Scikit-learn Python 机器学习库提供了许多内置的数据准备技术,例如用于缩放数值输入变量和改变变量概率分布的技术。
这些转换可以进行拟合,然后应用于数据集,或者用作预测建模管道的一部分,从而可以正确地应用一系列转换,而在使用数据抽样技术(如k 折交叉验证)评估模型性能时避免数据泄露。
尽管 scikit-learn 中可用 的数据准备技术非常广泛,但可能需要额外的 数据准备步骤。
通常,这些额外的步骤在建模之前是手动执行的,并且需要编写自定义代码。风险在于这些数据准备步骤可能会不一致地执行。
解决方案是使用 FunctionTransformer 类在 scikit-learn 中创建自定义数据转换。
此类允许您指定一个用于转换数据的函数。您可以定义函数并执行任何有效的更改,例如更改值或移除数据列(不移除行)。
然后,此类可以像 scikit-learn 中的任何其他数据转换一样使用,例如直接转换数据,或在建模管道中使用。
关键在于转换是无状态的,这意味着无法保留任何状态。
这意味着无法使用该转换来计算训练数据集上的统计数据,然后将这些统计数据用于转换训练集和测试集。
除了自定义缩放操作之外,这对于标准数据清理操作也很有帮助,例如识别和移除具有少量唯一值的列以及识别和移除相对异常值。
我们将探讨这两种情况,但首先,让我们定义一个可以用于探索基础的数据集。
想开始学习数据准备吗?
立即参加我为期7天的免费电子邮件速成课程(附示例代码)。
点击注册,同时获得该课程的免费PDF电子书版本。
溢油数据集
所谓的“漏油”数据集是一个标准机器学习数据集。
该任务涉及根据描述卫星图像某个区域内容的向量,预测某个区域是否包含漏油,例如由于非法或意外倾倒石油到海洋中。
共有 937 个案例。每个案例由 48 个数值计算机视觉特征、一个区域编号和一个类别标签组成。
正常情况是没有溢油,分配的类别标签为 0,而溢油则用类别标签 1 表示。没有溢油的案例有 896 个,溢油的案例有 41 个。
您可以在此处访问整个数据集
查看文件内容。
文件的前几行应如下所示
1 2 3 4 5 6 |
1,2558,1506.09,456.63,90,6395000,40.88,7.89,29780,0.19,214.7,0.21,0.26,0.49,0.1,0.4,99.59,32.19,1.84,0.16,0.2,87.65,0,0.47,132.78,-0.01,3.78,0.22,3.2,-3.71,-0.18,2.19,0,2.19,310,16110,0,138.68,89,69,2850,1000,763.16,135.46,3.73,0,33243.19,65.74,7.95,1 2,22325,79.11,841.03,180,55812500,51.11,1.21,61900,0.02,901.7,0.02,0.03,0.11,0.01,0.11,6058.23,4061.15,2.3,0.02,0.02,87.65,0,0.58,132.78,-0.01,3.78,0.84,7.09,-2.21,0,0,0,0,704,40140,0,68.65,89,69,5750,11500,9593.48,1648.8,0.6,0,51572.04,65.73,6.26,0 3,115,1449.85,608.43,88,287500,40.42,7.34,3340,0.18,86.1,0.21,0.32,0.5,0.17,0.34,71.2,16.73,1.82,0.19,0.29,87.65,0,0.46,132.78,-0.01,3.78,0.7,4.79,-3.36,-0.23,1.95,0,1.95,29,1530,0.01,38.8,89,69,1400,250,150,45.13,9.33,1,31692.84,65.81,7.84,1 4,1201,1562.53,295.65,66,3002500,42.4,7.97,18030,0.19,166.5,0.21,0.26,0.48,0.1,0.38,120.22,33.47,1.91,0.16,0.21,87.65,0,0.48,132.78,-0.01,3.78,0.84,6.78,-3.54,-0.33,2.2,0,2.2,183,10080,0,108.27,89,69,6041.52,761.58,453.21,144.97,13.33,1,37696.21,65.67,8.07,1 5,312,950.27,440.86,37,780000,41.43,7.03,3350,0.17,232.8,0.15,0.19,0.35,0.09,0.26,289.19,48.68,1.86,0.13,0.16,87.65,0,0.47,132.78,-0.01,3.78,0.02,2.28,-3.44,-0.44,2.19,0,2.19,45,2340,0,14.39,89,69,1320.04,710.63,512.54,109.16,2.58,0,29038.17,65.66,7.35,0 ... |
我们可以看到第一列包含区域编号的整数。我们还可以看到计算机视觉派生的特征是实值,并且尺度不同,例如第二列中的数千,而其他列中的是分数。
此数据集包含具有非常少唯一值的列和具有异常值的列,这些为数据清理提供了良好的基础。
下面的示例下载数据集并将其加载为 numPy 数组,并汇总行数和列数。
1 2 3 4 5 6 7 8 9 10 11 |
# 加载漏油数据集 from pandas import read_csv # 定义数据集位置 path = 'https://raw.githubusercontent.com/jbrownlee/Datasets/master/oil-spill.csv' # 加载数据集 df = read_csv(path, header=None) # 将数据拆分为输入和输出 data = df.values X = data[:, :-1] y = data[:, -1] print(X.shape, y.shape) |
运行该示例加载数据集并确认预期的行数和列数。
1 |
(937, 49) (937,) |
现在我们有了一个可以作为数据转换基础的数据集,让我们看看如何使用 FunctionTransformer 类定义一些自定义数据清理转换。
自定义转换以移除列
具有较少唯一值的列可能无法为预测目标值提供任何有用的信息。
这并非绝对正确,但足够准确,您应该测试在移除此类列的数据集上拟合的模型性能。
这是一种数据清理,scikit-learn 中提供了一个名为 VarianceThreshold 的数据转换器,它试图使用每列的方差来解决这个问题。
另一种方法是移除具有少于指定数量的唯一值(例如 1)的列。
我们可以开发一个函数来应用此转换,并将最小唯一值数量作为可配置的默认参数。我们还将添加一些调试信息以确认其按预期工作。
首先,可以计算每列的唯一值数量。可以识别出具有等于或少于最小唯一值数量的十列。最后,可以从数据集中移除已识别的列。
下面的 cust_transform() 函数实现了这一点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# 移除具有较少唯一值的列 def cust_transform(X, min_values=1, verbose=True): # 获取每列的唯一值数量 counts = [len(unique(X[:, i])) for i in range(X.shape[1])] if verbose: print('唯一值:%s' % counts) # 选择要删除的列 to_del = [i for i,v in enumerate(counts) if v <= min_values] if verbose: print('删除:%s' % to_del) if len(to_del) is 0: return X # 选择除被删除列之外的所有列 ix = [i for i in range(X.shape[1]) if i not in to_del] result = X[:, ix] return result |
然后,我们可以将此函数用于 FunctionTransformer。
此转换的一个限制是它根据提供的数据选择要删除的列。这意味着如果训练集和测试集差异很大,则可能从每个集中删除不同的列,从而使模型评估变得困难(不稳定!?)。因此,最好将最小唯一值数量保持较小,例如 1。
我们可以将此转换应用于漏油数据集。完整的示例列在下面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# 用于移除具有较少唯一值的列的自定义数据转换 from numpy import unique from pandas import read_csv from sklearn.preprocessing import FunctionTransformer 从 sklearn.preprocessing 导入 LabelEncoder # 加载数据集 def load_dataset(path): # 加载数据集 df = read_csv(path, header=None) data = df.values # 将数据分割为输入和输出 X, y = data[:, :-1], data[:, -1] # 最小化准备数据集 X = X.astype('float') y = LabelEncoder().fit_transform(y.astype('str')) 返回 X, y # 移除具有较少唯一值的列 def cust_transform(X, min_values=1, verbose=True): # 获取每列的唯一值数量 counts = [len(unique(X[:, i])) for i in range(X.shape[1])] if verbose: print('唯一值:%s' % counts) # 选择要删除的列 to_del = [i for i,v in enumerate(counts) if v <= min_values] if verbose: print('删除:%s' % to_del) if len(to_del) is 0: return X # 选择除被删除列之外的所有列 ix = [i for i in range(X.shape[1]) if i not in to_del] result = X[:, ix] return result # 定义数据集位置 url = 'https://raw.githubusercontent.com/jbrownlee/Datasets/master/oil-spill.csv' # 加载数据集 X, y = load_dataset(url) print(X.shape, y.shape) # 定义转换器 trans = FunctionTransformer(cust_transform) # 应用转换 X = trans.fit_transform(X) # 总结新形状 print(X.shape) |
运行该示例,首先报告原始数据集的行数和列数。
接下来,打印一个列表,显示数据集中每列的唯一值数量。我们可以看到许多列具有非常少的唯一值。
然后识别并报告具有一个(或更少)唯一值的列。在这种情况下,是第 22 列。该列已从数据集中删除。
最后,报告转换后数据集的形状,显示为 48 列而不是 49 列,这证实了具有单个唯一值的列已被删除。
1 2 3 4 |
(937, 49) (937,) 唯一值:[238, 297, 927, 933, 179, 375, 820, 618, 561, 57, 577, 59, 73, 107, 53, 91, 893, 810, 170, 53, 68, 9, 1, 92, 9, 8, 9, 308, 447, 392, 107, 42, 4, 45, 141, 110, 3, 758, 9, 9, 388, 220, 644, 649, 499, 2, 937, 169, 286] 删除:[22] (937, 48) |
此转换有许多可以探索的扩展,例如
- 确保它仅应用于数值输入变量。
- 尝试不同的最小唯一值数量。
- 使用百分比而不是唯一值的绝对数量。
如果您探索了任何这些扩展,请在下面的评论中告诉我。
接下来,让我们看看一个替换数据集中值的转换。
自定义转换以替换异常值
异常值是与其他观测值不同或不像的观测值。
如果我们一次考虑一个变量,异常值将是远离质心(其余值)的值,这意味着它很少见或观察到的概率很低。
对于常见概率分布,有识别异常值的标准方法。对于高斯数据,我们可以将异常值识别为距离均值三倍或更多标准差的观测值。
对于具有许多输入变量的数据,这可能是一种可取或不可取的方式,但在某些情况下可能有效。
我们可以以这种方式识别异常值,并用修正值(例如均值)替换它们的值。
一次考虑每一列,计算均值和标准差统计量。使用这些统计量,定义“正常”值的上限和下限,然后可以识别落在这些边界之外的所有值。如果识别出一个或多个异常值,则用已计算出的均值替换它们的值。
下面的 cust_transform() 函数实现了这一点,作为应用于数据集的函数,我们将标准差数和是否显示调试信息作为参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# 替换异常值 def cust_transform(X, n_stdev=3, verbose=True): # 复制数组 result = X.copy() # 枚举每一列 for i in range(result.shape[1]): # 获取列的值 col = X[:, i] # 计算统计量 mu, sigma = mean(col), std(col) # 定义边界 lower, upper = mu-(sigma*n_stdev), mu+(sigma*n_stdev) # 选择超出边界的索引 ix = where(logical_or(col < lower, col > upper))[0] if verbose and len(ix) > 0: print('>列=%d, 异常值=%d' % (i, len(ix))) # 替换值 result[ix, i] = mu return result |
然后,我们可以将此函数用于 FunctionTransformer。
异常检测方法假设高斯概率分布,并独立应用于每个变量,这两者都是强假设。
此实现的另一个限制是均值和标准差统计量是在提供的数据集上计算的,这意味着异常值的定义和替换值都相对于数据集。这意味着如果转换用于训练集和测试集,则可以使用不同的异常值定义和不同的替换值。
我们可以将此转换应用于漏油数据集。完整的示例列在下面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# 用于替换异常值的自定义数据转换 from numpy import mean from numpy import std from numpy import where from numpy import logical_or from pandas import read_csv from sklearn.preprocessing import FunctionTransformer 从 sklearn.preprocessing 导入 LabelEncoder # 加载数据集 def load_dataset(path): # 加载数据集 df = read_csv(path, header=None) data = df.values # 将数据分割为输入和输出 X, y = data[:, :-1], data[:, -1] # 最小化准备数据集 X = X.astype('float') y = LabelEncoder().fit_transform(y.astype('str')) 返回 X, y # 替换异常值 def cust_transform(X, n_stdev=3, verbose=True): # 复制数组 result = X.copy() # 枚举每一列 for i in range(result.shape[1]): # 获取列的值 col = X[:, i] # 计算统计量 mu, sigma = mean(col), std(col) # 定义边界 lower, upper = mu-(sigma*n_stdev), mu+(sigma*n_stdev) # 选择超出边界的索引 ix = where(logical_or(col < lower, col > upper))[0] if verbose and len(ix) > 0: print('>列=%d, 异常值=%d' % (i, len(ix))) # 替换值 result[ix, i] = mu return result # 定义数据集位置 url = 'https://raw.githubusercontent.com/jbrownlee/Datasets/master/oil-spill.csv' # 加载数据集 X, y = load_dataset(url) print(X.shape, y.shape) # 定义转换器 trans = FunctionTransformer(cust_transform) # 应用转换 X = trans.fit_transform(X) # 总结新形状 print(X.shape) |
运行示例,首先报告数据集在任何更改之前的形状。
接下来,计算每列的异常值数量,并且只在输出中报告那些有一个或多个异常值的列。我们可以看到数据集中共有 32 列有一个或多个异常值。
然后移除异常值并报告结果数据集的形状,确认行数或列数没有变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
(937, 49) (937,) >列=0, 异常值=10 >列=1, 异常值=8 >列=3, 异常值=8 >列=5, 异常值=7 >列=6, 异常值=1 >列=7, 异常值=12 >列=8, 异常值=15 >列=9, 异常值=14 >列=10, 异常值=19 >列=11, 异常值=17 >列=12, 异常值=22 >列=13, 异常值=2 >列=14, 异常值=16 >列=15, 异常值=8 >列=16, 异常值=8 >列=17, 异常值=6 >列=19, 异常值=12 >列=20, 异常值=20 >列=27, 异常值=14 >列=28, 异常值=18 >列=29, 异常值=2 >列=30, 异常值=13 >列=32, 异常值=3 >列=34, 异常值=14 >列=35, 异常值=15 >列=37, 异常值=13 >列=40, 异常值=18 >列=41, 异常值=13 >列=42, 异常值=12 >列=43, 异常值=12 >列=44, 异常值=19 >列=46, 异常值=21 (937, 49) |
此转换有许多可以探索的扩展,例如
- 确保它仅应用于数值输入变量。
- 尝试不同的标准差数,例如 2 或 4。
- 使用不同的异常值定义,例如 IQR 或模型。
如果您探索了任何这些扩展,请在下面的评论中告诉我。
进一步阅读
如果您想深入了解,本节提供了更多关于该主题的资源。
相关教程
API
总结
在本教程中,您了解了如何为 scikit-learn 定义和使用自定义数据转换。
具体来说,你学到了:
- 可以使用 FunctionTransformer 类为 scikit-learn 创建自定义数据转换。
- 如何开发和应用自定义转换来移除唯一值较少的列。
- 如何开发和应用自定义转换来替换每列的异常值。
你有什么问题吗?
在下面的评论中提出你的问题,我会尽力回答。
尊敬的Jason博士,
在“自定义转换以移除列”部分,我理解了函数
该函数接受三个参数,其中 min_values 和 verbose 的默认值为 1 和 True。
需要澄清为什么我不能将默认值更改为其他值。
例如,我可以这样做
我不能这样做
即使我拟合了两个变量,FunctionTransformer(X,36) 也会忽略 36 并使用默认值 1。
请问:
如何让 FunctionTransformer 绕过 cust_transform 的两个默认值?
为什么不直接使用 cust_transform?我可以成功地设置所有三个参数而没有任何问题?
我可以在不出现任何问题的情况下直接调用函数,为何还要使用FunctionTransformer,这看起来“没有用”。我是否遗漏了什么?
再次提前感谢,
悉尼的Anthony
它们是带有默认值的命名参数——您必须通过名称来指定它们。
…
cust_transform(X, min_values=36, verbose=True)
也许可以阅读一下python函数参数。
我们可以直接使用该函数,但本教程的目的是展示如何使用一个自定义转换器对象,该对象可以以任何您喜欢的方式使用——例如直接使用或在管道中使用。
尊敬的Jason博士,
感谢您的回复。
我明白在使用自定义函数时函数参数是如何工作的。
但是,我无法通过FunctionTransformer传递参数。
作为演示
为什么我不能像使用cust_transformer那样,使用FunctionTransformer来传递参数?
谢谢你,
悉尼的Anthony
好问题,您必须将参数作为字典传递给“kw_args”参数,请参阅此处的API以获取更多信息
https://scikit-learn.cn/stable/modules/generated/sklearn.preprocessing.FunctionTransformer.html
例如,我认为这会起作用
尊敬的Jason博士,
谢谢!它奏效了!kw_args正如您所说是一个字典,所以kw_args必须用字典括号(如{})来赋值。
虽然scikit-learn.org网站上的文档有时显得模糊,但该网站在FunctionTransformer的上下文中阐明了许多示例,包括反向转换器函数,参见https://www.programcreek.com/python/example/93354/sklearn.preprocessing.FunctionTransformer
再次感谢您,
悉尼的Anthony
尊敬的Jason博士,
抱歉。我打印错了X。我稍后会解释。
上面的X应该大得多
这才是应该打印出来的
错误解释:我正在试验一个反向函数,并使用了一个简单的矩阵,然后打印了错误的东西。灵感来源:https://scikit-learn.cn/stable/modules/generated/sklearn.preprocessing.FunctionTransformer.html
如何使用简单的矩阵获得反向函数。
再次感谢您,
悉尼的Anthony
尊敬的Jason博士,
我想做一个反向函数,但我必须让反向函数接受一个输入参数。
transform 和 inverse_transform 可以工作。我知道。
但是,我不能有一个不接受参数的inverse_transform。
因为如果我创建一个不带参数的反向函数,就会发生这种情况
不知道为什么我的InverseFunction需要一个参数,而我定义它时不接受参数。
但是,如果我的InverseFunction定义为接受一个参数doNothingVariable,它就会起作用——请参考本回复顶部的成功示例。
再次感谢您,
悉尼的Anthony
反向函数必须接收要进行反向转换的数据。
根据文档,您必须指定另一个函数来计算反向,以及传递给该函数的参数。
对了,我太急了。干得不错!
尊敬的Jason博士,
感谢您的时间。
在“Scikit-Learn中的自定义数据转换”标题下的第二段中,FunctionTransformer可以集成到管道中。
FunctionTransformers与管道的正确实现是什么?这里有一个包含其他函数在管道中实现的示例。
考虑到管道,trans1、trans2、rfe和model都有fit函数,当调用
在管道中的顺序是否执行了trans1、trans2、rfe和model中的所有fit函数?
谢谢你,
悉尼的Anthony
在我看来,看起来差不多。
尊敬的Jason博士,
它似乎有效。
我创建了两个管道——一个包含自定义转换器以排除异常值,另一个不包含自定义转换器。有趣的是,通过排除异常值,您的得分反而降低了!
结论:保留异常值比排除异常值能获得更高的得分。
太棒了!
现在您可能会问,如果我执行pipeline.fit_transform()会发生什么?
您可能会注意到,管道中的某些函数在使用fit_transform时将无法工作,因为其中一些函数不包含fit_transform函数:让我们看看
DecisionTreeClassifier 没有 transform 函数。但是
但是 trans1 有一个 inverse
回顾 trans1 在 inverse_func 中指定了 inverse 函数
这是我的InverseFunction
所以教训是:如果你想调用 pipeline 的 inverse_transform 函数,确保 pipeline 中的函数都有一个 inverse。
回顾其他教训
从数据集中移除异常值并不一定会提高分数。在 pima-indians-diabetes 的案例中,分数下降了。
谢谢你,
悉尼的Anthony
你只能对数据转换器进行 fit_transform,而不能对 pipeline 进行。你可以拟合一个 pipeline。
尊敬的Jason博士,
我在第一个教程中犯了一个错误。
在第一个教程中,我提到如果你想调用 pipeline 的 inverse_transform,我本来是想说调用 pipeline 的 transform 函数,确保 pipeline 中的其他函数有 transform 函数。在这种情况下,DecisionTreeClassifier 没有 transform 函数。
谢谢你,
悉尼的Anthony
尊敬的Jason博士,
总的来说,关于 pipelines。
如果你调用 pipeline 的 transform 函数或任何其他方法,那么步骤中的所有函数都必须有一个相应的函数。在这种情况下是 transform 方法。
换句话说,如果你调用 pipeline.particular_method(),那么所有列出的步骤都必须有一个相应的 particular_method()。
谢谢你,
悉尼的Anthony
是的。
这看起来很简单。
我认为你的意思是“下面的例子下载了数据集并将其加载为 Pandas DataFrame,并总结了行数和列数。”而不是“加载为 NumPy 数组”。如果我错了,请纠正我。
谢谢。功能上没有太大区别。
尊敬的先生,
我有一个问题,假设我有一个数据集,我需要通过对两个现有列进行逐元素除法来生成一个新列。在训练测试分离和缺失值插补之后,我能否通过函数转换器在训练集和之后在测试集中生成此列?
也许可以试试。
先生,
我正在写一个关于如何使用函数转换器向现有 numpy 数组添加新列的示例
>>> import numpy as np
>>> from sklearn.preprocessing import FunctionTransformer
>>> def col_add(x)
x1 = x[:, 0] + x[:, 1]
x2 = x[:, 0] * x[:, 1]
x3 = x[:, 0] / x[:, 1]
return np.c_[x, x1, x2, x3]
>>> col_adder = FunctionTransformer(col_add)
>>> arr = np.array([[2, 7], [4, 9], [3, 5]])
>>> arr
array([[2, 7],
[4, 9],
[3, 5]])
>>> col_adder.transform(arr) # 将添加 3 列
array([[ 2. , 7. , 9. , 14. , 0.28571429],
[ 4. , 9. , 13. , 36. , 0.44444444],
[ 3. , 5. , 8. , 15. , 0.6 ]])
>>>
我制作了这样一个转换器,上面给出的例子是生成 3 个新列,来自现有的 2 个列的 numpy 数组,第一个列是用于逐元素相加,第二个列是用于逐元素相乘,第三个列是用于逐元素相除。
所以这样一来,函数转换器就可以用来添加由现有列生成的新特征了吗?
你可以使用 hstack() 将一个新列添加到现有的矩阵中。
sklearn 转换器不能添加列。