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)。任何其他稀疏输入将被 **转换为压缩稀疏行表示**。为了避免不必要的内存复制,建议在 upstream 选择 CSR 或 CSC 表示。

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

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

如果您的数据包含许多异常值,则使用数据的均值和方差进行缩放可能无法很好地工作。在这些情况下,您可以使用 RobustScaler 作为直接替换。它使用更稳健的估计来估计数据的中心和范围。

参考资料#

有关对数据进行中心化和缩放的重要性,请参阅此常见问题解答:我应该对数据进行归一化/标准化/重新缩放吗?

缩放与白化#

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

为了解决这个问题,您可以使用 PCA 并设置 whiten=True 来进一步去除特征之间的线性相关性。

6.3.1.4. 对核矩阵进行中心化#

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

数学公式#

现在我们有了直觉,可以看看数学公式。设 \(K\) 是在 fit 阶段从形状为 (n_samples, n_features) 的数据矩阵 \(X\) 计算得到的形状为 (n_samples, n_samples) 的核矩阵。 \(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

还可以使用 QuantileTransformer 将数据映射到正态分布,方法是将 output_distribution='normal' 设置为。

>>> 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)。为了避免不必要的内存复制,建议在 upstream 选择 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 估计器的特征的另一种可能性是使用 one-of-K,也称为 one-hot 或虚拟编码。这种类型的编码可以通过 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' 并且在转换期间遇到未知类别时,不会引发错误,但此特征的结果 one-hot 编码列将全部为零或被视为不常见类别(如果启用)。(handle_unknown='infrequent_if_exist' 仅支持 one-hot 编码)

>>> 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 中,第一列是具有类别“male”/”female”的特征的编码,而其余 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. 如果未配置不常见类别支持或在训练期间没有不常见类别,则此特征的生成的独热编码列将全部为零。在逆变换中,未知类别将表示为 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]。这种编码方案对于具有高基数的分类特征非常有用,在这些特征中,独热编码会扩展特征空间,使其更难以被下游模型处理。高基数类别的经典示例是基于位置的类别,例如邮政编码或区域。

二元分类目标#

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

\[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 且 bin 边缘位于值 threshold 时, BinarizerKBinsDiscretizer 类似。

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 并调用 fit 然后再调用 transform 来确保 funcinverse_func 互为逆函数。请注意,会发出警告,可以通过 filterwarnings 将其转换为错误。

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

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