Press "Enter" to skip to content

使用Scikit-Learn中的自定义转换器进行高级数据准备

超越“初学者模式”,充分利用scikit-learn的更强大功能

Image by Daniel K Cheung on Unsplash

Scikit-Learn提供了许多有用的数据准备工具,但有时预先构建的选项不足。

在本文中,我将向您展示如何使用自定义转换器创建高级数据准备工作流程。如果您已经使用scikit-learn一段时间并想提高技能水平,学习有关转换器的知识是超越“初学者模式”并了解现代数据科学团队所需的一些更高级功能的绝佳方式。

如果这个主题听起来有点高级,不用担心——本文充满了例子,这将帮助您对代码和概念感到自信。

我将从简要概述scikit-learn的Transformer类开始,然后介绍构建自定义转换器的两种方法:

  1. 使用FunctionTransformer
  2. 从头开始编写自定义Transformer

转换器:在scikit-learn中预处理数据的最佳方法

Transformer是scikit-learn的中心构建块之一。事实上,它非常基础,以至于您可能已经在使用它而没有意识到。

在scikit-learn中,Transformer是具有fit()transform()方法的任何对象。简单来说,这意味着转换器是一个类(即可重用的代码块),它以原始数据集作为输入并返回该数据集的转换版本。

Image by author

重要的是,scikit-learn的Transformer与BERT和GPT-4等大型语言模型(LLM)中使用的“transformers”不同,或通过HuggingFace的transformers库提供的模型。在LLM的背景下,“transformer”(小写‘t’)是深度学习模型;scikit-learn的Transformer(大写‘T’)是完全不同(而且更简单)的实体。您可以简单地将其视为典型ML工作流程中预处理数据的工具。

当您导入scikit-learn时,您将自动访问一堆预构建的转换器,这些转换器设计用于常见的ML数据预处理任务,例如替换缺失值,重新缩放特征和独热编码。一些最受欢迎的转换器包括:

  1. sklearn.impute.SimpleImputer – 一个转换器,将替换数据集中的缺失值
  2. sklearn.preprocessing.MinMaxScaler – 一个可以重新缩放数据集中数值特征的转换器
  3. sklearn.preprocessing.OneHotEncoder – 用于独热编码分类特征的转换器

使用scikit-learn的sklearn.pipeline.Pipeline,您甚至可以将多个转换器链接在一起,以构建多步数据准备工作流程,以备后续的ML建模:

Image by author

如果您不熟悉管道或列转换器,它们是简化ML代码的好方法,您可以在我的先前文章中了解更多内容:

使用这4个鲜为人知的scikit-learn类简化您的数据准备

别忘了train_test_split:Pipeline、ColumnTransformer、FeatureUnion和FunctionTransformer即使在……时也是必不可少的

towardsdatascience.com

预先构建的scikit-learn转换器有什么问题?

没有问题!

如果您使用简单的数据集并执行标准的数据准备步骤,则scikit-learn的预构建转换器可能完全足够。没有必要通过从头编写自定义转换器来重新发明轮子。

但是 – 让我们诚实 – 在现实生活中,数据集何时真正简单了呢?

(剧透:永远不会。)

如果您正在使用实际数据或需要实现一些有趣的预处理方法,则scikit-learn内置的转换器并不总是足够。迟早,您将需要实现自定义数据转换。

幸运的是,scikit-learn提供了一些扩展其基本转换器功能并构建更自定义转换器的方法。为了展示这些方法的工作原理,我将使用经典的泰坦尼克号生存预测数据集。即使在这个所谓的“简单”数据集上,您也会发现有很多机会进行数据准备创意。并且,正如我将展示的那样,自定义转换器是完成任务的理想工具。

数据

首先,让我们加载数据集并将其分成训练和测试子集:

import pandas as pdfrom sklearn.datasets import fetch_openmlfrom sklearn.model_selection import train_test_split# Load data and split into training and testing setsX, y = fetch_openml("titanic", version=1, as_frame=True, return_X_y=True)X.drop(['boat', 'body', 'home.dest'], axis=1, inplace=True)X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, test_size=0.2)X_train.head()
Image by author. Titanic dataset available a CC0 public domain license.

因为本文重点介绍如何构建自定义转换器,所以不会详细介绍使用scikit-learn内置转换器可以轻松应用于此数据集的标准预处理步骤(例如使用OneHotEncoder对类别变量进行独热编码,或使用SimpleImputer替换缺失值)。

相反,我将重点介绍如何将更复杂的预处理步骤纳入其中,这些步骤不能使用“现成”的转换器实现。

其中之一涉及从name字段中提取每个乘客的头衔(例如先生、夫人、主人)。为什么我们要这样做呢?嗯,如果我们知道每个乘客的头衔都包含其班级/年龄/性别的指示,并且我们假设这些因素影响了乘客上救生艇的能力,那么认为头衔可能对生存机会有所启示是合理的。例如,一个带有“主人”头衔(表示他们是孩子)的乘客可能比一个带有“先生”头衔(表示他们是成年人)的乘客更有可能生存。

当然,问题在于没有内置的scikit-learn类可以执行从name字段中提取标题这样具体的操作。要提取标题,我们需要构建一个自定义转换器。

1. FunctionTransformer

构建自定义转换器的最快方法是使用FunctionTransformer类,它允许您直接从普通Python函数创建转换器。

要使用FunctionTransformer,您首先需要定义一个函数,该函数接受输入数据集X,执行所需的转换,并返回X的转换版本。然后,将您的函数包装在FunctionTransformer中,scikit-learn将创建一个实现您函数的定制转换器。

例如,这里有一个函数可以从我们的泰坦尼克号数据集的name字段中提取乘客的头衔:

from sklearn.preprocessing import FunctionTransformerdef extract_title(X):    """从每个乘客的`name`中提取头衔。"""    X['title'] = X['name'].str.split(', ', expand=True)[1].str.split('.', expand=True)[0]    return Xextract_title_transformer = FunctionTransformer(extract_title)print(type(extract_title_transformer))# <class 'sklearn.preprocessing._function_transformer.FunctionTransformer'>

如您所见,将函数包装在FunctionTransformer中,将其转换为scikit-learn Transformer,使其具有.fit().transform()方法。

我们可以将此Transformer与我们想要包括的任何其他预处理步骤/transformers一起并入数据准备管道中:

from sklearn.pipeline import Pipelinepreprocessor = Pipeline(steps=[  ('extract_title', extract_title_transformer),  # ... 我们想要包括的任何其他transformers,例如SimpleImputer或MinMaxScaler])X_train_transformed = preprocessor.fit_transform(X_train)X_train_transformed
Image by author

如果您想定义一个更复杂的函数/Transformer,该函数/Transformer需要其他参数,则可以通过将这些参数纳入FunctionTransformerkw_args参数中将其传递给函数。例如,让我们定义另一个函数,该函数基于头衔确定每个乘客是否来自上层阶级/专业背景:

def extract_title(X):    """从每个乘客的`name`中提取头衔。"""    X['title'] = X['name'].str.split(', ', expand=True)[1].str.split('.', expand=True)[0]def is_upper_class(X, upper_class_titles):    """如果乘客的头衔在`upper_class_titles`列表中,则返回1,否则返回0。"""    X['upper_class'] = X['title'].apply(lambda x: 1 if x in upper_class_titles else 0)    return Xpreprocessor = Pipeline(steps=[  ('extract_title', FunctionTransformer(extract_title)),  ('is_upper_class', FunctionTransformer(is_upper_class,                      kw_args={'upper_class_titles':['Dr', 'Col', 'Major', 'Lady', 'Rev', 'Sir', 'Capt']})),    # ... 我们想要包括的任何其他transformers,例如SimpleImputer或MinMaxScaler])X_train_transformed = preprocessor.fit_transform(X_train)X_train_transformed
Image by author

如您所见,使用FunctionTransformer是一种非常简单的方法,可以在不从根本上改变代码结构的情况下将这些复杂的预处理步骤合并到Pipeline中。

1.1 FunctionTransformer的限制:有状态的转换

FunctionTransformer是一种强大而优雅的解决方案,但只适用于应用无状态转换(即基于规则的转换,这些转换不依赖于从训练数据计算得出的先前值)。如果您想定义一个自定义Transformer,可以根据在训练数据集中观察到的值来转换测试数据集,则不能使用FunctionTransformer,您需要采取不同的方法。

如果这听起来有点困惑,请花一分钟时间重新考虑我们刚刚编写用于提取乘客头衔的函数:

def extract_title(X):    """从每个乘客的`name`中提取头衔。"""    X['title'] = X['name'].str.split(', ', expand=True)[1].str.split('.', expand=True)[0]

该函数是无状态的,因为它没有记忆过去的能力;在操作期间它不使用任何预计算值。每次我们调用此函数时,它都会像第一次执行一样从头开始应用。

与之相反,状态函数保留来自先前操作的信息,并在执行当前操作时使用此信息。为了说明这种区别,下面有两个函数,用平均值替换数据集中的缺失值:

# 无状态 - 转换中不使用先前信息def impute_mean_stateless(X):    X['column1'] = X['column1'].fillna(X['column1'].mean())# 有状态 - 使用训练集的先前信息进行转换column1_mean_train = np.mean(X_train['column1'])def impute_mean(X):    X['column1'] = X['column1'].fillna(column1_mean_train)    return X

第一个函数是无状态函数,因为转换中不使用先前信息;平均值仅使用传递给函数的数据集X进行计算。

第二个是有状态函数,它使用column1_mean_train(即来自训练集X_traincolumn1的平均值)替换X中的缺失值。

无状态和有状态转换之间的区别可能似乎有点抽象,但在具有单独的训练和测试数据集的 ML 任务中,这是一个非常重要的概念。每当我们想要在测试数据集上替换缺失值、缩放特征或执行独热编码时,我们希望这些转换基于训练数据集中观察到的值。换句话说,我们希望我们的 Transformer 适合于训练集。使用使用平均值填充缺失值的示例,我们希望“平均值”是训练集的平均值。

使用FunctionTransformer的问题在于它无法用于实现有状态转换。尽管使用FunctionTransformer创建的Transformer技术上具有.fit()方法,但调用它不会做任何事情,因此我们无法真正将此 Transformer “拟合”到训练数据。为什么?因为FunctionTransformer创建的 Transformer 中的转换始终依赖于函数的输入值X。我们的 Transformer 将始终使用传递的数据集重新计算值;它无法使用预先计算的值来填充/转换。

1.2 FunctionTransformer 限制示例

为了说明这一点,这里有一个示例,我尝试将基于 FunctionTransformer 的 Transformer “拟合”到训练集,然后使用此所谓“已拟合”的 Transformer 来转换测试集。正如您所看到的,测试集中的缺失值没有用训练集的平均值替换;它们是基于测试集重新计算的。换句话说,Transformer 无法应用有状态转换。

# 显示测试集,在转换之前X_test.head(3)
作者的图片,显示测试集中‘Age’列中第三行中的缺失值
print("X_train 平均值: ", X_train['age'].mean())# X_train 平均值:  29.857414148681055print("X_test 平均值: ", X_test['age'].mean())# X_test 平均值:  29.97444952830189

def impute_mean(X):    X['age'] = X['age'].fillna(X['age'].mean())    return Ximpute_mean_FT = FunctionTransformer(impute_mean) # 将函数转换为 Transformerprepro = impute_mean_FT.fit(X_train) # 将 Transformer“拟合”到训练集prepro.transform(X_test) # 使用已拟合的 Transformer 转换测试集
作者的图片。第三行中的缺失值被替换为测试集的平均值,而不是训练集的平均值,说明 FuntionTransformer 无法生成能够进行有状态转换的 Transformer。

如果这听起来有点令人困惑,不要担心。关键的信息是:如果您想定义一个自定义的Transformer,它可以根据训练数据集中观察到的值预处理测试数据集,您不能使用FunctionTransformer,您需要采用不同的方法。

2. 从头开始创建自定义Transformer

另一种替代方法是定义一个新的Transformer类,该类继承自sklearn.base模块中的一个类:TransformerMixin。然后,这个新类将作为一个Transformer,并适用于应用无状态和有状态的转换。

以下是我们如何使用这种方法将我们的extract_title代码片段转换为Transformer:

from sklearn.base import TransformerMixinclass ExtractTitle(TransformerMixin):    def fit(self, X, y=None):        return self    def transform(self, X, y=None):        X['title'] = X['name'].str.split(', ', expand=True)[1].str.split('.', expand=True)[0]        return Xpreprocessor = Pipeline(steps=[    ('extract_title', ExtractTitle()),])X_train_transformed = preprocessor.fit_transform(X_train)X_train_transformed.head()
Image by author

正如您所看到的,我们实现了与使用FunctionTransformer构建Transformer时完全相同的转换。

2.1 传递参数到自定义Transformer

如果您需要向自定义Transformer传递数据,只需在定义fit()transform()方法之前定义一个__init__()方法即可:

class IsUpperClass(TransformerMixin):    def __init__(self, upper_class_titles):        self.upper_class_titles = upper_class_titles                    def fit(self, X, y=None):        return self        def transform(self, X, y=None):        X['upper_class'] = X['title'].apply(lambda x: 1 if x in self.upper_class_titles else 0)        return Xpreprocessor = Pipeline(steps=[    ('IsUpperClass', IsUpperClass(upper_class_titles=['Dr', 'Col', 'Major', 'Lady', 'Rev', 'Sir', 'Capt'])),])X_train_transformed = preprocessor.fit_transform(X_train)X_train_transformed.head()
Image by author

这就是构建自定义Transformer的两种方法。使用这些方法的能力是非常有价值的,我作为一名数据科学家在日常工作中经常使用它。希望您会发现它有用。

如果您喜欢本文,并想获取更多关于数据科学工作的技巧和见解,请考虑在小猪AI或LinkedIn上关注我。如果您想无限制地访问我所有的故事(以及小猪AI.com的其余部分),您可以通过我的推荐链接每月支付5美元进行注册。与通过一般注册页面注册相比,它不会增加额外的费用,而且可以帮助支持我的写作,因为我会得到一小笔佣金。

感谢您的阅读!

Leave a Reply

Your email address will not be published. Required fields are marked *