6.3. 数据预处理#

sklearn.preprocessing 包提供了几个常用的实用函数和转换器类,用于将原始特征向量转换为更适合下游估计器的表示。

一般来说,许多学习算法(如线性模型)都受益于数据集的标准化(参见 特征缩放的重要性)。如果数据集中存在一些异常值,则鲁棒缩放器或其他转换器可能更合适。不同缩放器、转换器和归一化器在包含边缘异常值的数据集上的行为在 比较不同缩放器对包含异常值的数据的影响 中进行了重点介绍。

6.3.1. 标准化,或均值移除和方差缩放#

数据集的标准化是 scikit-learn 中许多机器学习估计器的常见要求;如果各个特征看起来不太像标准正态分布数据:均值为且方差为的高斯分布,则它们的行为可能会很糟糕。

在实践中,我们通常忽略分布的形状,只需转换数据以通过移除每个特征的平均值来使其居中,然后通过将其非恒定特征除以它们的标准差来缩放它。

例如,学习算法目标函数中使用的许多元素(例如支持向量机的 RBF 核或线性模型的 l1 和 l2 正则化器)可能假设所有特征都以零为中心或具有相同数量级的方差。如果一个特征的方差比其他特征大几个数量级,它可能会支配目标函数,并使估计器无法像预期的那样正确地从其他特征中学习。

preprocessing 模块提供了 StandardScaler 实用程序类,这是一种快速简便的方法,可以在类似数组的数据集上执行以下操作

>>> from sklearn import preprocessing
>>> import numpy as np
>>> X_train = np.array([[ 1., -1.,  2.],
...                     [ 2.,  0.,  0.],
...                     [ 0.,  1., -1.]])
>>> scaler = preprocessing.StandardScaler().fit(X_train)
>>> scaler
StandardScaler()

>>> scaler.mean_
array([1. ..., 0. ..., 0.33...])

>>> scaler.scale_
array([0.81..., 0.81..., 1.24...])

>>> X_scaled = scaler.transform(X_train)
>>> X_scaled
array([[ 0.  ..., -1.22...,  1.33...],
       [ 1.22...,  0.  ..., -0.26...],
       [-1.22...,  1.22..., -1.06...]])

缩放后的数据均值为零,方差为一

>>> X_scaled.mean(axis=0)
array([0., 0., 0.])

>>> X_scaled.std(axis=0)
array([1., 1., 1.])

此类实现 Transformer API 以计算训练集上的均值和标准差,以便稍后能够在测试集上重新应用相同的转换。因此,此类适合用在 Pipeline 的早期步骤中。

>>> from sklearn.datasets import make_classification
>>> from sklearn.linear_model import LogisticRegression
>>> from sklearn.model_selection import train_test_split
>>> from sklearn.pipeline import make_pipeline
>>> from sklearn.preprocessing import StandardScaler

>>> X, y = make_classification(random_state=42)
>>> X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
>>> pipe = make_pipeline(StandardScaler(), LogisticRegression())
>>> pipe.fit(X_train, y_train)  # apply scaling on training data
Pipeline(steps=[('standardscaler', StandardScaler()),
                ('logisticregression', LogisticRegression())])

>>> pipe.score(X_test, y_test)  # apply scaling on testing data, without leaking training data.
0.96

可以通过将 with_mean=Falsewith_std=False 传递到 StandardScaler 的构造函数来禁用居中或缩放。

6.3.1.1. 将特征缩放至特定范围#

另一种标准化方法是将特征缩放至给定的最小值和最大值之间,通常在零到一之间,或者使每个特征的最大绝对值缩放到单位大小。这可以使用MinMaxScalerMaxAbsScaler分别实现。

使用这种缩放的动机包括对特征非常小的标准差的鲁棒性以及保留稀疏数据中的零条目。

以下是一个将玩具数据矩阵缩放至[0, 1]范围的示例

>>> X_train = np.array([[ 1., -1.,  2.],
...                     [ 2.,  0.,  0.],
...                     [ 0.,  1., -1.]])
...
>>> min_max_scaler = preprocessing.MinMaxScaler()
>>> X_train_minmax = min_max_scaler.fit_transform(X_train)
>>> X_train_minmax
array([[0.5       , 0.        , 1.        ],
       [1.        , 0.5       , 0.33333333],
       [0.        , 1.        , 0.        ]])

然后可以将相同的转换器实例应用于在拟合调用期间未见到的某些新的测试数据:将应用相同的缩放和移位操作,以与对训练数据执行的转换保持一致。

>>> X_test = np.array([[-3., -1.,  4.]])
>>> X_test_minmax = min_max_scaler.transform(X_test)
>>> X_test_minmax
array([[-1.5       ,  0.        ,  1.66666667]])

可以检查缩放器的属性以了解在训练数据上学到的转换的确切性质。

>>> min_max_scaler.scale_
array([0.5       , 0.5       , 0.33...])

>>> min_max_scaler.min_
array([0.        , 0.5       , 0.33...])

如果MinMaxScaler给定显式的feature_range=(min, max),则完整公式为

X_std = (X - X.min(axis=0)) / (X.max(axis=0) - X.min(axis=0))

X_scaled = X_std * (max - min) + min

MaxAbsScaler的工作方式非常相似,但缩放方式使训练数据位于[-1, 1]范围内,方法是除以每个特征中的最大最大值。它适用于已经以零为中心或稀疏的数据。

以下是如何使用前一个示例中的玩具数据以及此缩放器。

>>> X_train = np.array([[ 1., -1.,  2.],
...                     [ 2.,  0.,  0.],
...                     [ 0.,  1., -1.]])
...
>>> max_abs_scaler = preprocessing.MaxAbsScaler()
>>> X_train_maxabs = max_abs_scaler.fit_transform(X_train)
>>> X_train_maxabs
array([[ 0.5, -1. ,  1. ],
       [ 1. ,  0. ,  0. ],
       [ 0. ,  1. , -0.5]])
>>> X_test = np.array([[ -3., -1.,  4.]])
>>> X_test_maxabs = max_abs_scaler.transform(X_test)
>>> X_test_maxabs
array([[-1.5, -1. ,  2. ]])
>>> max_abs_scaler.scale_
array([2.,  1.,  2.])

6.3.1.2. 稀疏数据缩放#

对稀疏数据进行中心化会破坏数据中的稀疏性结构,因此很少是明智的做法。但是,对稀疏输入进行缩放可能是有意义的,尤其是在特征处于不同比例时。

MaxAbsScaler专门设计用于缩放稀疏数据,是推荐的方法。但是,StandardScaler可以接受scipy.sparse矩阵作为输入,只要with_mean=False显式地传递给构造函数。否则,将引发ValueError,因为静默中心化会破坏稀疏性,并且通常会通过无意中分配过多的内存来导致执行崩溃。RobustScaler不能拟合稀疏输入,但是你可以在稀疏输入上使用transform方法。

请注意,缩放器接受压缩稀疏行和压缩稀疏列格式(参见scipy.sparse.csr_matrixscipy.sparse.csc_matrix)。任何其他稀疏输入都将**转换为压缩稀疏行表示**。为了避免不必要的内存复制,建议在上游选择CSR或CSC表示。

最后,如果预期居中数据足够小,则使用稀疏矩阵的toarray方法将输入显式转换为数组是另一种选择。

6.3.1.3. 缩放包含异常值的数据#

如果你的数据包含许多异常值,则使用数据的均值和方差进行缩放可能效果不佳。在这些情况下,你可以使用RobustScaler作为替代品。它使用更鲁棒的估计来计算数据的中心和范围。

参考文献#

有关对数据进行居中和缩放的重要性,可在以下常见问题解答中找到进一步的讨论:我应该对数据进行归一化/标准化/重新缩放吗?

缩放与白化#

有时仅仅独立地对特征进行居中和缩放是不够的,因为下游模型可以进一步对特征的线性独立性做出一些假设。

为了解决这个问题,可以使用PCA,其中whiten=True,以进一步去除特征间的线性相关性。

6.3.1.4. 居中核矩阵#

如果你有一个核矩阵\(K\),它计算由函数\(\phi(\cdot)\)定义的(可能隐式)特征空间中的点积,则KernelCenterer可以转换核矩阵,使其包含由\(\phi\)定义的特征空间中的内积,然后去除该空间中的均值。换句话说,KernelCenterer计算与半正定核\(K\)相关的居中格拉姆矩阵。

数学公式#

在了解直觉之后,我们可以看看数学公式。令\(K\)为形状为(n_samples, n_samples)的核矩阵,它是在fit步骤中根据形状为(n_samples, n_features)的数据矩阵\(X\)计算得到的。\(K\)定义为

\[K(X, X) = \phi(X) . \phi(X)^{T}\]

\(\phi(X)\)是将\(X\)映射到希尔伯特空间的函数。中心化核\(\tilde{K}\)定义为

\[\tilde{K}(X, X) = \tilde{\phi}(X) . \tilde{\phi}(X)^{T}\]

其中\(\tilde{\phi}(X)\)是由希尔伯特空间中\(\phi(X)\)的中心化得到的。

因此,可以通过使用函数\(\phi(\cdot)\)映射\(X\)并在新的空间中对数据进行中心化来计算\(\tilde{K}\)。然而,经常使用核是因为它们允许一些代数计算,从而避免了使用\(\phi(\cdot)\)显式地进行这种映射。事实上,可以像[Scholkopf1998]的附录B中所示那样隐式地进行中心化

\[\tilde{K} = K - 1_{\text{n}_{samples}} K - K 1_{\text{n}_{samples}} + 1_{\text{n}_{samples}} K 1_{\text{n}_{samples}}\]

\(1_{\text{n}_{samples}}\)是一个(n_samples, n_samples)的矩阵,其中所有元素都等于\(\frac{1}{\text{n}_{samples}}\)。在transform步骤中,核变为\(K_{test}(X, Y)\),定义为

\[K_{test}(X, Y) = \phi(Y) . \phi(X)^{T}\]

\(Y\)是形状为(n_samples_test, n_features)的测试数据集,因此\(K_{test}\)的形状为(n_samples_test, n_samples)。在这种情况下,\(K_{test}\)的中心化方式如下

\[\tilde{K}_{test}(X, Y) = K_{test} - 1'_{\text{n}_{samples}} K - K_{test} 1_{\text{n}_{samples}} + 1'_{\text{n}_{samples}} K 1_{\text{n}_{samples}}\]

\(1'_{\text{n}_{samples}}\)是一个形状为(n_samples_test, n_samples)的矩阵,其中所有元素都等于\(\frac{1}{\text{n}_{samples}}\)

参考文献

[Scholkopf1998]

B. Schölkopf, A. Smola和K.R. Müller,“非线性成分分析作为核特征值问题”。神经计算 10.5 (1998): 1299-1319。

6.3.2. 非线性变换#

有两种类型的变换可用:分位数变换和幂变换。分位数变换和幂变换都是基于特征的单调变换,因此保留了沿每个特征的值的秩。

分位数变换根据公式\(G^{-1}(F(X))\)将所有特征转换为相同的期望分布,其中\(F\)是特征的累积分布函数,\(G^{-1}\)是期望输出分布\(G\)分位数函数。这个公式使用了以下两个事实:(i)如果\(X\)是一个具有连续累积分布函数\(F\)的随机变量,则\(F(X)\)\([0,1]\)上服从均匀分布;(ii)如果\(U\)是一个在\([0,1]\)上服从均匀分布的随机变量,则\(G^{-1}(U)\)服从分布\(G\)。通过执行秩变换,分位数变换可以平滑异常分布,并且比缩放方法受异常值的影响更小。但是,它会扭曲特征内部和跨特征的相关性和距离。

幂变换是一族参数变换,旨在将来自任何分布的数据映射到尽可能接近高斯分布的数据。

6.3.2.1. 映射到均匀分布#

QuantileTransformer提供了一种非参数变换,用于将数据映射到值介于0和1之间的均匀分布

>>> from sklearn.datasets import load_iris
>>> from sklearn.model_selection import train_test_split
>>> X, y = load_iris(return_X_y=True)
>>> X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
>>> quantile_transformer = preprocessing.QuantileTransformer(random_state=0)
>>> X_train_trans = quantile_transformer.fit_transform(X_train)
>>> X_test_trans = quantile_transformer.transform(X_test)
>>> np.percentile(X_train[:, 0], [0, 25, 50, 75, 100]) 
array([ 4.3,  5.1,  5.8,  6.5,  7.9])

此特征对应于以厘米为单位的花萼长度。应用分位数变换后,这些地标接近先前定义的百分位数。

>>> np.percentile(X_train_trans[:, 0], [0, 25, 50, 75, 100])
... 
array([ 0.00... ,  0.24...,  0.49...,  0.73...,  0.99... ])

这可以在具有类似结果的独立测试集上得到证实。

>>> np.percentile(X_test[:, 0], [0, 25, 50, 75, 100])
... 
array([ 4.4  ,  5.125,  5.75 ,  6.175,  7.3  ])
>>> np.percentile(X_test_trans[:, 0], [0, 25, 50, 75, 100])
... 
array([ 0.01...,  0.25...,  0.46...,  0.60... ,  0.94...])

6.3.2.2. 映射到高斯分布#

在许多建模场景中,数据集特征的正态性是理想的。幂变换是一族参数化的单调变换,旨在将来自任何分布的数据尽可能映射到高斯分布,以稳定方差并最小化偏度。

PowerTransformer目前提供了两种幂变换,即Yeo-Johnson变换和Box-Cox变换。

Yeo-Johnson变换#
\[\begin{split}x_i^{(\lambda)} = \begin{cases} [(x_i + 1)^\lambda - 1] / \lambda & \text{if } \lambda \neq 0, x_i \geq 0, \\[8pt] \ln{(x_i + 1)} & \text{if } \lambda = 0, x_i \geq 0 \\[8pt] -[(-x_i + 1)^{2 - \lambda} - 1] / (2 - \lambda) & \text{if } \lambda \neq 2, x_i < 0, \\[8pt] - \ln (- x_i + 1) & \text{if } \lambda = 2, x_i < 0 \end{cases}\end{split}\]
Box-Cox变换#
\[\begin{split}x_i^{(\lambda)} = \begin{cases} \dfrac{x_i^\lambda - 1}{\lambda} & \text{if } \lambda \neq 0, \\[8pt] \ln{(x_i)} & \text{if } \lambda = 0, \end{cases}\end{split}\]

Box-Cox变换只能应用于严格为正的数据。在这两种方法中,变换都由\(\lambda\)参数化,该参数通过最大似然估计确定。这是一个使用Box-Cox变换将从对数正态分布中抽取的样本映射到正态分布的示例。

>>> pt = preprocessing.PowerTransformer(method='box-cox', standardize=False)
>>> X_lognormal = np.random.RandomState(616).lognormal(size=(3, 3))
>>> X_lognormal
array([[1.28..., 1.18..., 0.84...],
      [0.94..., 1.60..., 0.38...],
      [1.35..., 0.21..., 1.09...]])
>>> pt.fit_transform(X_lognormal)
array([[ 0.49...,  0.17..., -0.15...],
      [-0.05...,  0.58..., -0.57...],
      [ 0.69..., -0.84...,  0.10...]])

虽然上述示例将standardize选项设置为False,但PowerTransformer默认情况下会对转换后的输出应用零均值、单位方差归一化。

以下是 Box-Cox 和 Yeo-Johnson 变换应用于各种概率分布的示例。请注意,当应用于某些分布时,幂变换可以实现非常接近高斯的的结果,但在其他情况下则无效。这突出了在变换前后可视化数据的重要性。

../_images/sphx_glr_plot_map_data_to_normal_001.png

也可以通过设置output_distribution='normal',使用QuantileTransformer将数据映射到正态分布。使用前面鸢尾花数据集的示例

>>> quantile_transformer = preprocessing.QuantileTransformer(
...     output_distribution='normal', random_state=0)
>>> X_trans = quantile_transformer.fit_transform(X)
>>> quantile_transformer.quantiles_
array([[4.3, 2. , 1. , 0.1],
       [4.4, 2.2, 1.1, 0.1],
       [4.4, 2.2, 1.2, 0.1],
       ...,
       [7.7, 4.1, 6.7, 2.5],
       [7.7, 4.2, 6.7, 2.5],
       [7.9, 4.4, 6.9, 2.5]])

因此,输入的中位数成为输出的均值,中心位于 0。正态输出被裁剪,以便输入的最小值和最大值——分别对应于 1e-7 和 1 - 1e-7 分位数——在变换下不会变成无穷大。

6.3.3. 标准化#

标准化是将单个样本缩放为单位范数的过程。如果您计划使用二次形式(例如点积或任何其他核)来量化任何样本对的相似性,则此过程可能很有用。

这个假设是文本分类和聚类环境中经常使用的向量空间模型的基础。

函数normalize提供了一种快速简便的方法来对单个类数组数据集执行此操作,可以使用l1l2max范数。

>>> X = [[ 1., -1.,  2.],
...      [ 2.,  0.,  0.],
...      [ 0.,  1., -1.]]
>>> X_normalized = preprocessing.normalize(X, norm='l2')

>>> X_normalized
array([[ 0.40..., -0.40...,  0.81...],
       [ 1.  ...,  0.  ...,  0.  ...],
       [ 0.  ...,  0.70..., -0.70...]])

preprocessing模块还提供了一个实用程序类Normalizer,它使用Transformer API实现相同的操作(即使在这种情况下fit方法没有用:因为此操作独立处理样本,所以该类是无状态的)。

因此,此类适用于Pipeline的早期步骤。

>>> normalizer = preprocessing.Normalizer().fit(X)  # fit does nothing
>>> normalizer
Normalizer()

然后,可以像任何转换器一样在样本向量上使用标准化器实例。

>>> normalizer.transform(X)
array([[ 0.40..., -0.40...,  0.81...],
       [ 1.  ...,  0.  ...,  0.  ...],
       [ 0.  ...,  0.70..., -0.70...]])

>>> normalizer.transform([[-1.,  1., 0.]])
array([[-0.70...,  0.70...,  0.  ...]])

注意:L2 标准化也称为空间符号预处理。

稀疏输入#

normalizeNormalizer接受scipy.sparse 的密集类数组和稀疏矩阵作为输入

对于稀疏输入,数据在被馈送到高效的 Cython 例程之前会被转换为压缩稀疏行表示(参见scipy.sparse.csr_matrix)。为了避免不必要的内存复制,建议在上游选择 CSR 表示。

6.3.4. 编码分类特征#

特征通常不是作为连续值给出,而是分类的。例如,一个人可能有特征["male", "female"]["from Europe", "from US", "from Asia"]["uses Firefox", "uses Chrome", "uses Safari", "uses Internet Explorer"]。此类特征可以有效地编码为整数,例如["male", "from US", "uses Internet Explorer"]可以表示为[0, 1, 3],而["female", "from Asia", "uses Chrome"]将为[1, 2, 1]

为了将分类特征转换为这样的整数代码,我们可以使用OrdinalEncoder。这个估计器将每个分类特征转换为一个新的整数特征(0 到 n_categories - 1)。

>>> enc = preprocessing.OrdinalEncoder()
>>> X = [['male', 'from US', 'uses Safari'], ['female', 'from Europe', 'uses Firefox']]
>>> enc.fit(X)
OrdinalEncoder()
>>> enc.transform([['female', 'from US', 'uses Safari']])
array([[0., 1., 1.]])

然而,这种整数表示不能直接与所有 scikit-learn 估计器一起使用,因为这些估计器期望连续输入,并且会将类别解释为有序的,这通常是不希望的(即浏览器的集合是任意排序的)。

OrdinalEncoder默认情况下还会传递由np.nan指示的缺失值。

>>> enc = preprocessing.OrdinalEncoder()
>>> X = [['male'], ['female'], [np.nan], ['female']]
>>> enc.fit_transform(X)
array([[ 1.],
       [ 0.],
       [nan],
       [ 0.]])

OrdinalEncoder提供一个参数encoded_missing_value来编码缺失值,而无需创建管道并使用SimpleImputer

>>> enc = preprocessing.OrdinalEncoder(encoded_missing_value=-1)
>>> X = [['male'], ['female'], [np.nan], ['female']]
>>> enc.fit_transform(X)
array([[ 1.],
       [ 0.],
       [-1.],
       [ 0.]])

上述处理等效于以下管道

>>> from sklearn.pipeline import Pipeline
>>> from sklearn.impute import SimpleImputer
>>> enc = Pipeline(steps=[
...     ("encoder", preprocessing.OrdinalEncoder()),
...     ("imputer", SimpleImputer(strategy="constant", fill_value=-1)),
... ])
>>> enc.fit_transform(X)
array([[ 1.],
       [ 0.],
       [-1.],
       [ 0.]])

将分类特征转换为可用于 scikit-learn 估计器的另一种方法是使用一种K编码,也称为独热编码或虚拟编码。这种编码可以使用OneHotEncoder 获得,它将每个具有 n_categories 个可能值的分类特征转换为 n_categories 个二元特征,其中一个为 1,其余都为 0。

继续上面的例子

>>> enc = preprocessing.OneHotEncoder()
>>> X = [['male', 'from US', 'uses Safari'], ['female', 'from Europe', 'uses Firefox']]
>>> enc.fit(X)
OneHotEncoder()
>>> enc.transform([['female', 'from US', 'uses Safari'],
...                ['male', 'from Europe', 'uses Safari']]).toarray()
array([[1., 0., 0., 1., 0., 1.],
       [0., 1., 1., 0., 0., 1.]])

默认情况下,每个特征可以取的值会自动从数据集中推断出来,并且可以在 categories_ 属性中找到。

>>> enc.categories_
[array(['female', 'male'], dtype=object), array(['from Europe', 'from US'], dtype=object), array(['uses Firefox', 'uses Safari'], dtype=object)]

可以使用参数 categories 显式地指定这些值。我们的数据集中有两个性别、四个可能的洲和四个网络浏览器。

>>> genders = ['female', 'male']
>>> locations = ['from Africa', 'from Asia', 'from Europe', 'from US']
>>> browsers = ['uses Chrome', 'uses Firefox', 'uses IE', 'uses Safari']
>>> enc = preprocessing.OneHotEncoder(categories=[genders, locations, browsers])
>>> # Note that for there are missing categorical values for the 2nd and 3rd
>>> # feature
>>> X = [['male', 'from US', 'uses Safari'], ['female', 'from Europe', 'uses Firefox']]
>>> enc.fit(X)
OneHotEncoder(categories=[['female', 'male'],
                          ['from Africa', 'from Asia', 'from Europe',
                           'from US'],
                          ['uses Chrome', 'uses Firefox', 'uses IE',
                           'uses Safari']])
>>> enc.transform([['female', 'from Asia', 'uses Chrome']]).toarray()
array([[1., 0., 0., 1., 0., 0., 1., 0., 0., 0.]])

如果训练数据可能存在缺失的分类特征,则通常最好指定 handle_unknown='infrequent_if_exist',而不是像上面那样手动设置 categories。当指定 handle_unknown='infrequent_if_exist' 并且在转换期间遇到未知类别时,不会引发错误,但此特征的结果独热编码列将全部为零,或者如果启用则被视为不常见类别。(handle_unknown='infrequent_if_exist' 只支持独热编码)

>>> enc = preprocessing.OneHotEncoder(handle_unknown='infrequent_if_exist')
>>> X = [['male', 'from US', 'uses Safari'], ['female', 'from Europe', 'uses Firefox']]
>>> enc.fit(X)
OneHotEncoder(handle_unknown='infrequent_if_exist')
>>> enc.transform([['female', 'from Asia', 'uses Chrome']]).toarray()
array([[1., 0., 0., 0., 0., 0.]])

也可以使用 drop 参数将每一列编码为 n_categories - 1 列而不是 n_categories 列。此参数允许用户指定要删除的每个特征的类别。这对于避免某些分类器中输入矩阵的共线性很有用。例如,在使用非正则化回归(LinearRegression)时,此功能非常有用,因为共线性会导致协方差矩阵不可逆。

>>> X = [['male', 'from US', 'uses Safari'],
...      ['female', 'from Europe', 'uses Firefox']]
>>> drop_enc = preprocessing.OneHotEncoder(drop='first').fit(X)
>>> drop_enc.categories_
[array(['female', 'male'], dtype=object), array(['from Europe', 'from US'], dtype=object),
 array(['uses Firefox', 'uses Safari'], dtype=object)]
>>> drop_enc.transform(X).toarray()
array([[1., 1., 1.],
       [0., 0., 0.]])

可能只想为只有 2 个类别的特征删除两列中的一列。在这种情况下,可以设置参数 drop='if_binary'

>>> X = [['male', 'US', 'Safari'],
...      ['female', 'Europe', 'Firefox'],
...      ['female', 'Asia', 'Chrome']]
>>> drop_enc = preprocessing.OneHotEncoder(drop='if_binary').fit(X)
>>> drop_enc.categories_
[array(['female', 'male'], dtype=object), array(['Asia', 'Europe', 'US'], dtype=object),
 array(['Chrome', 'Firefox', 'Safari'], dtype=object)]
>>> drop_enc.transform(X).toarray()
array([[1., 0., 0., 1., 0., 0., 1.],
       [0., 0., 1., 0., 0., 1., 0.],
       [0., 1., 0., 0., 1., 0., 0.]])

在转换后的 X 中,第一列是具有类别“男性”/“女性”的特征的编码,而其余 6 列分别是具有 3 个类别的 2 个特征的编码。

handle_unknown='ignore'drop 不为 None 时,未知类别将被编码为全零。

>>> drop_enc = preprocessing.OneHotEncoder(drop='first',
...                                        handle_unknown='ignore').fit(X)
>>> X_test = [['unknown', 'America', 'IE']]
>>> drop_enc.transform(X_test).toarray()
array([[0., 0., 0., 0., 0.]])

在转换期间,X_test 中的所有类别都是未知的,并将映射到全零。这意味着未知类别将具有与已删除类别相同的映射。OneHotEncoder.inverse_transform 将将所有零映射到已删除的类别(如果已删除类别)或 None(如果未删除类别)。

>>> drop_enc = preprocessing.OneHotEncoder(drop='if_binary', sparse_output=False,
...                                        handle_unknown='ignore').fit(X)
>>> X_test = [['unknown', 'America', 'IE']]
>>> X_trans = drop_enc.transform(X_test)
>>> X_trans
array([[0., 0., 0., 0., 0., 0., 0.]])
>>> drop_enc.inverse_transform(X_trans)
array([['female', None, None]], dtype=object)
支持具有缺失值的分类特征#

OneHotEncoder 通过将缺失值视为附加类别来支持具有缺失值的分类特征。

>>> X = [['male', 'Safari'],
...      ['female', None],
...      [np.nan, 'Firefox']]
>>> enc = preprocessing.OneHotEncoder(handle_unknown='error').fit(X)
>>> enc.categories_
[array(['female', 'male', nan], dtype=object),
array(['Firefox', 'Safari', None], dtype=object)]
>>> enc.transform(X).toarray()
array([[0., 1., 0., 0., 1., 0.],
      [1., 0., 0., 0., 0., 1.],
      [0., 0., 1., 1., 0., 0.]])

如果一个特征同时包含 np.nanNone,它们将被视为单独的类别。

>>> X = [['Safari'], [None], [np.nan], ['Firefox']]
>>> enc = preprocessing.OneHotEncoder(handle_unknown='error').fit(X)
>>> enc.categories_
[array(['Firefox', 'Safari', None, nan], dtype=object)]
>>> enc.transform(X).toarray()
array([[0., 1., 0., 0.],
      [0., 0., 1., 0.],
      [0., 0., 0., 1.],
      [1., 0., 0., 0.]])

有关表示为字典而不是标量的分类特征,请参见从字典加载特征

6.3.4.1. 不常见类别#

OneHotEncoderOrdinalEncoder 支持将不常见类别聚合到每个特征的单个输出中。启用收集不常见类别的参数是 min_frequencymax_categories

  1. min_frequency 可以是大于或等于 1 的整数,也可以是区间 (0.0, 1.0) 中的浮点数。如果 min_frequency 是整数,则基数小于 min_frequency 的类别将被视为不常见。如果 min_frequency 是浮点数,则基数小于样本总数的此分数的类别将被视为不常见。默认值为 1,这意味着每个类别都单独编码。

  2. max_categories 可以是 None 或任何大于 1 的整数。此参数为每个输入特征的输出特征数量设置上限。max_categories 包括组合不常见类别的特征。

在以下使用 OrdinalEncoder 的示例中,类别 'dog' and 'snake' 被视为不常见。

>>> X = np.array([['dog'] * 5 + ['cat'] * 20 + ['rabbit'] * 10 +
...               ['snake'] * 3], dtype=object).T
>>> enc = preprocessing.OrdinalEncoder(min_frequency=6).fit(X)
>>> enc.infrequent_categories_
[array(['dog', 'snake'], dtype=object)]
>>> enc.transform(np.array([['dog'], ['cat'], ['rabbit'], ['snake']]))
array([[2.],
       [0.],
       [1.],
       [2.]])

OrdinalEncodermax_categories **不**考虑缺失或未知类别。将 unknown_valueencoded_missing_value 设置为整数,每个都会使唯一整数代码的数量增加一。这可能导致最多 max_categories + 2 个整数代码。在下面的示例中,“a”和“d”被认为是不常见的,并被组合成一个类别,“b”和“c”是它们自己的类别,未知值编码为 3,缺失值编码为 4。

>>> X_train = np.array(
...     [["a"] * 5 + ["b"] * 20 + ["c"] * 10 + ["d"] * 3 + [np.nan]],
...     dtype=object).T
>>> enc = preprocessing.OrdinalEncoder(
...     handle_unknown="use_encoded_value", unknown_value=3,
...     max_categories=3, encoded_missing_value=4)
>>> _ = enc.fit(X_train)
>>> X_test = np.array([["a"], ["b"], ["c"], ["d"], ["e"], [np.nan]], dtype=object)
>>> enc.transform(X_test)
array([[2.],
       [0.],
       [1.],
       [2.],
       [3.],
       [4.]])

类似地,OneHotEncoder 可以配置为将不常见的类别组合在一起。

>>> enc = preprocessing.OneHotEncoder(min_frequency=6, sparse_output=False).fit(X)
>>> enc.infrequent_categories_
[array(['dog', 'snake'], dtype=object)]
>>> enc.transform(np.array([['dog'], ['cat'], ['rabbit'], ['snake']]))
array([[0., 0., 1.],
       [1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])

通过将 handle_unknown 设置为 'infrequent_if_exist',未知类别将被视为不常见。

>>> enc = preprocessing.OneHotEncoder(
...    handle_unknown='infrequent_if_exist', sparse_output=False, min_frequency=6)
>>> enc = enc.fit(X)
>>> enc.transform(np.array([['dragon']]))
array([[0., 0., 1.]])

OneHotEncoder.get_feature_names_out 使用“infrequent”作为不常见特征的名称。

>>> enc.get_feature_names_out()
array(['x0_cat', 'x0_rabbit', 'x0_infrequent_sklearn'], dtype=object)

'handle_unknown' 设置为 'infrequent_if_exist' 并在转换中遇到未知类别时。

  1. 如果没有配置不常见类别支持,或者在训练期间没有不常见类别,则此特征的最终 one-hot 编码列将全为零。在逆变换中,未知类别将表示为 None

  2. 如果在训练期间存在不常见类别,则未知类别将被视为不常见。在逆变换中,将使用“infrequent_sklearn”来表示不常见类别。

也可以使用 max_categories 配置不常见类别。在下面的示例中,我们将 max_categories=2 用于限制输出中的特征数量。这将导致除“cat”类别之外的所有类别都被视为不常见,从而导致两个特征,一个用于“cat”,另一个用于不常见类别——也就是所有其他类别。

>>> enc = preprocessing.OneHotEncoder(max_categories=2, sparse_output=False)
>>> enc = enc.fit(X)
>>> enc.transform([['dog'], ['cat'], ['rabbit'], ['snake']])
array([[0., 1.],
       [1., 0.],
       [0., 1.],
       [0., 1.]])

如果 max_categoriesmin_frequency 都不是默认值,则首先根据 min_frequency 选择类别,然后保留 max_categories 个类别。在下面的示例中,min_frequency=4 只认为 snake 不常见,但 max_categories=3 强制 dog 也被视为不常见。

>>> enc = preprocessing.OneHotEncoder(min_frequency=4, max_categories=3, sparse_output=False)
>>> enc = enc.fit(X)
>>> enc.transform([['dog'], ['cat'], ['rabbit'], ['snake']])
array([[0., 0., 1.],
       [1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])

如果在 max_categories 的截止点处有不常见类别具有相同的基数,则根据词典顺序取前 max_categories 个。在下面的示例中,“b”、“c”和“d”具有相同的基数,并且 max_categories=2,“b”和“c”是不常见的,因为它们的词典顺序更高。

>>> X = np.asarray([["a"] * 20 + ["b"] * 10 + ["c"] * 10 + ["d"] * 10], dtype=object).T
>>> enc = preprocessing.OneHotEncoder(max_categories=3).fit(X)
>>> enc.infrequent_categories_
[array(['b', 'c'], dtype=object)]

6.3.4.2. 目标编码器#

TargetEncoder 使用以类别特征为条件的目标均值来编码无序类别,即名义类别 [PAR] [MIC]。这种编码方案对于基数较高的类别特征非常有用,在这些特征中,one-hot 编码会膨胀特征空间,从而使下游模型的处理成本更高。基于位置的高基数类别的经典示例包括邮政编码或区域。

二元分类目标#

对于二元分类目标,目标编码由下式给出:

\[S_i = \lambda_i\frac{n_{iY}}{n_i} + (1 - \lambda_i)\frac{n_Y}{n}\]

其中 \(S_i\) 是类别 \(i\) 的编码,\(n_{iY}\)\(Y=1\) 和类别 \(i\) 的观测数,\(n_i\) 是类别 \(i\) 的观测数,\(n_Y\)\(Y=1\) 的观测数,\(n\) 是观测数,\(\lambda_i\) 是类别 \(i\) 的收缩因子。收缩因子由下式给出:

\[\lambda_i = \frac{n_i}{m + n_i}\]

其中 \(m\) 是平滑因子,由 TargetEncoder 中的 smooth 参数控制。较大的平滑因子会赋予全局均值更大的权重。当 smooth="auto" 时,平滑因子计算为经验贝叶斯估计:\(m=\sigma_i^2/\tau^2\),其中 \(\sigma_i^2\) 是类别 \(i\)y 方差,\(\tau^2\)y 的全局方差。

多类别分类目标#

对于多类别分类目标,公式与二元分类类似。

\[S_{ij} = \lambda_i\frac{n_{iY_j}}{n_i} + (1 - \lambda_i)\frac{n_{Y_j}}{n}\]

其中,\(S_{ij}\) 是类别 \(i\) 和分类 \(j\) 的编码,\(n_{iY_j}\) 是具有 \(Y=j\) 和类别 \(i\) 的观测值数量,\(n_i\) 是具有类别 \(i\) 的观测值数量,\(n_{Y_j}\) 是具有 \(Y=j\) 的观测值数量,\(n\) 是观测值总数,\(\lambda_i\) 是类别 \(i\) 的收缩因子。

连续目标#

对于连续目标,其公式与二元分类类似。

\[S_i = \lambda_i\frac{\sum_{k\in L_i}Y_k}{n_i} + (1 - \lambda_i)\frac{\sum_{k=1}^{n}Y_k}{n}\]

其中,\(L_i\) 是具有类别 \(i\) 的观测值集合,\(n_i\) 是具有类别 \(i\) 的观测值数量。

fit_transform 内部依赖于一种 交叉拟合 方案,以防止目标信息泄漏到训练时间的表示中,特别是对于非信息性的高基数分类变量,并有助于防止下游模型过度拟合虚假相关性。因此,fit(X, y).transform(X) 不等于 fit_transform(X, y)。在 fit_transform 中,训练数据被分成 *k* 折(由 cv 参数确定),并且使用其他 *k-1* 折学习到的编码对每一折进行编码。下图显示了 交叉拟合 方案在 fit_transform 中的应用,默认 cv=5

../_images/target_encoder_cross_validation.svg

fit_transform 还使用整个训练集学习“完整数据”编码。这在 fit_transform 中从未使用过,但会保存到属性 encodings_ 中,以便在调用 transform 时使用。请注意,在 交叉拟合 方案期间为每一折学习到的编码不会保存到属性中。

fit 方法**不**使用任何 交叉拟合 方案,而是在整个训练集上学习一种编码,用于在 transform 中对类别进行编码。此编码与在 fit_transform 中学习到的“完整数据”编码相同。

注意

TargetEncoder 将缺失值,例如 np.nanNone,视为另一类别,并像其他类别一样对其进行编码。在 fit 期间未见到的类别将使用目标均值进行编码,即 target_mean_

示例

参考文献

6.3.5. 离散化#

离散化(也称为量化或分箱)提供了一种将连续特征划分为离散值的方法。某些具有连续特征的数据集可能会受益于离散化,因为离散化可以将具有连续属性的数据集转换为仅具有名义属性的数据集。

独热编码的离散化特征可以使模型更具表现力,同时保持可解释性。例如,使用离散化器进行预处理可以为线性模型引入非线性。有关更高级的可能性,特别是平滑的可能性,请参见下面进一步的生成多项式特征

6.3.5.1. K-bins 离散化#

KBinsDiscretizer 将特征离散化为 k 个箱。

>>> X = np.array([[ -3., 5., 15 ],
...               [  0., 6., 14 ],
...               [  6., 3., 11 ]])
>>> est = preprocessing.KBinsDiscretizer(n_bins=[3, 2, 2], encode='ordinal').fit(X)

默认情况下,输出被独热编码为稀疏矩阵(参见 编码分类特征),这可以使用 encode 参数进行配置。对于每个特征,箱边缘在 fit 期间计算,并且与箱数一起,它们将定义区间。因此,对于当前示例,这些区间定义为:

  • 特征 1:\({[-\infty, -1), [-1, 2), [2, \infty)}\)

  • 特征 2:\({[-\infty, 5), [5, \infty)}\)

  • 特征 3:\({[-\infty, 14), [14, \infty)}\)

基于这些区间,X 的转换方式如下:

>>> est.transform(X)                      
array([[ 0., 1., 1.],
       [ 1., 1., 1.],
       [ 2., 0., 0.]])

结果数据集包含有序属性,可进一步用于Pipeline

离散化类似于为连续数据构建直方图。但是,直方图侧重于统计落入特定区间内的特征数量,而离散化侧重于将特征值分配到这些区间。

KBinsDiscretizer 实现不同的分箱策略,可以通过 strategy 参数进行选择。“uniform”策略使用等宽区间。“quantile”策略使用分位数值来使每个特征中的区间包含相等数量的样本。“kmeans”策略基于对每个特征独立执行的 k 均值聚类过程来定义区间。

请注意,可以通过传递一个定义离散化策略的可调用对象到 FunctionTransformer 来指定自定义区间。例如,我们可以使用 Pandas 函数 pandas.cut

>>> import pandas as pd
>>> import numpy as np
>>> from sklearn import preprocessing
>>>
>>> bins = [0, 1, 13, 20, 60, np.inf]
>>> labels = ['infant', 'kid', 'teen', 'adult', 'senior citizen']
>>> transformer = preprocessing.FunctionTransformer(
...     pd.cut, kw_args={'bins': bins, 'labels': labels, 'retbins': False}
... )
>>> X = np.array([0.2, 2, 15, 25, 97])
>>> transformer.fit_transform(X)
['infant', 'kid', 'teen', 'adult', 'senior citizen']
Categories (5, object): ['infant' < 'kid' < 'teen' < 'adult' < 'senior citizen']

示例

6.3.5.2. 特征二值化#

特征二值化对数值特征进行阈值处理以获得布尔值的过程。这对于假设输入数据服从多变量伯努利分布的下游概率估计器很有用。例如,BernoulliRBM 就是这种情况。

在文本处理领域,即使标准化计数(又称词频)或 TF-IDF 值特征在实践中通常表现略好,使用二元特征值(可能是为了简化概率推理)也很常见。

Normalizer 类似,实用程序类 Binarizer 旨在用于 Pipeline 的早期阶段。fit 方法什么也不做,因为每个样本都是独立于其他样本处理的。

>>> X = [[ 1., -1.,  2.],
...      [ 2.,  0.,  0.],
...      [ 0.,  1., -1.]]

>>> binarizer = preprocessing.Binarizer().fit(X)  # fit does nothing
>>> binarizer
Binarizer()

>>> binarizer.transform(X)
array([[1., 0., 1.],
       [1., 0., 0.],
       [0., 1., 0.]])

可以调整二值化器的阈值。

>>> binarizer = preprocessing.Binarizer(threshold=1.1)
>>> binarizer.transform(X)
array([[0., 0., 1.],
       [1., 0., 0.],
       [0., 0., 0.]])

Normalizer 类类似,预处理模块提供了一个配套函数 binarize,可在不需要转换器 API 时使用。

请注意,当 k = 2 且区间边缘位于 threshold 值时,Binarizer 类似于 KBinsDiscretizer

6.3.6. 缺失值的插补#

用于插补缺失值的工具在缺失值的插补中讨论。

6.3.7. 生成多项式特征#

通常,通过考虑输入数据的非线性特征来增加模型的复杂度非常有用。我们展示了两种可能性,它们都基于多项式:第一种使用纯多项式,第二种使用样条,即分段多项式。

6.3.7.1. 多项式特征#

一种简单且常用的方法是多项式特征,它可以获得特征的高阶项和交互项。它在 PolynomialFeatures 中实现。

>>> import numpy as np
>>> from sklearn.preprocessing import PolynomialFeatures
>>> X = np.arange(6).reshape(3, 2)
>>> X
array([[0, 1],
       [2, 3],
       [4, 5]])
>>> poly = PolynomialFeatures(2)
>>> poly.fit_transform(X)
array([[ 1.,  0.,  1.,  0.,  0.,  1.],
       [ 1.,  2.,  3.,  4.,  6.,  9.],
       [ 1.,  4.,  5., 16., 20., 25.]])

X 的特征已从 \((X_1, X_2)\) 转换为 \((1, X_1, X_2, X_1^2, X_1X_2, X_2^2)\)

在某些情况下,只需要特征之间的交互项,可以通过设置 interaction_only=True 来获得。

>>> X = np.arange(9).reshape(3, 3)
>>> X
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
>>> poly = PolynomialFeatures(degree=3, interaction_only=True)
>>> poly.fit_transform(X)
array([[  1.,   0.,   1.,   2.,   0.,   0.,   2.,   0.],
       [  1.,   3.,   4.,   5.,  12.,  15.,  20.,  60.],
       [  1.,   6.,   7.,   8.,  42.,  48.,  56., 336.]])

X 的特征已从 \((X_1, X_2, X_3)\) 转换为 \((1, X_1, X_2, X_3, X_1X_2, X_1X_3, X_2X_3, X_1X_2X_3)\)

请注意,在使用多项式核函数时,核方法(例如,SVCKernelPCA)中隐式地使用了多项式特征。

请参见多项式和样条插值,了解如何使用创建的多项式特征进行岭回归。

6.3.7.2. 样条变换器#

除了使用特征的纯多项式之外,另一种添加非线性项的方法是使用SplineTransformer为每个特征生成样条基函数。样条是分段多项式,由其多项式次数和节点位置参数化。SplineTransformer实现B样条基,参见下面的参考文献。

注意

SplineTransformer分别处理每个特征,也就是说,它不会给你交互项。

与多项式相比,样条的一些优点是:

  • 如果您保持固定的低阶(通常为3)并谨慎地调整节点数量,则B样条非常灵活和稳健。多项式需要更高的阶数,这导致了下一个要点。

  • B样条在边界处没有像多项式那样出现振荡行为(阶数越高,情况越糟)。这被称为龙格现象

  • B样条为超出边界(即超出拟合值范围)的外推提供了良好的选择。请查看选项extrapolation

  • B样条生成具有带状结构的特征矩阵。对于单个特征,每一行仅包含degree + 1个非零元素,这些元素连续出现并且都是正数。这导致一个具有良好数值特性的矩阵,例如条件数低,这与多项式矩阵(称为范德蒙矩阵)形成鲜明对比。低条件数对于线性模型的稳定算法非常重要。

以下代码片段显示了样条的实际应用

>>> import numpy as np
>>> from sklearn.preprocessing import SplineTransformer
>>> X = np.arange(5).reshape(5, 1)
>>> X
array([[0],
       [1],
       [2],
       [3],
       [4]])
>>> spline = SplineTransformer(degree=2, n_knots=3)
>>> spline.fit_transform(X)
array([[0.5  , 0.5  , 0.   , 0.   ],
       [0.125, 0.75 , 0.125, 0.   ],
       [0.   , 0.5  , 0.5  , 0.   ],
       [0.   , 0.125, 0.75 , 0.125],
       [0.   , 0.   , 0.5  , 0.5  ]])

由于X已排序,因此可以很容易地看到带状矩阵输出。对于degree=2,只有三个中间对角线是非零的。阶数越高,样条的重叠越多。

有趣的是,如果knots = strategy,则degree=0SplineTransformerencode='onehot-dense'n_bins = n_knots - 1KBinsDiscretizer相同。

示例

参考文献#

6.3.8. 自定义转换器#

通常,您可能希望将现有的Python函数转换为转换器以辅助数据清理或处理。您可以使用FunctionTransformer从任意函数实现转换器。例如,要构建一个在管道中应用对数转换的转换器,请执行以下操作:

>>> import numpy as np
>>> from sklearn.preprocessing import FunctionTransformer
>>> transformer = FunctionTransformer(np.log1p, validate=True)
>>> X = np.array([[0, 1], [2, 3]])
>>> # Since FunctionTransformer is no-op during fit, we can call transform directly
>>> transformer.transform(X)
array([[0.        , 0.69314718],
       [1.09861229, 1.38629436]])

您可以通过设置check_inverse=True并在transform之前调用fit来确保funcinverse_func互为逆函数。请注意,会发出警告,并可以使用filterwarnings将其转换为错误。

>>> import warnings
>>> warnings.filterwarnings("error", message=".*check_inverse*.",
...                         category=UserWarning, append=False)

有关演示如何使用FunctionTransformer从文本数据中提取特征的完整代码示例,请参见具有异构数据源的列转换器时间相关的特征工程