数据预处理

dask_ml.preprocessing 包含一些 scikit-learn 风格的转换器(transformer),可用于 Pipelines 中,作为模型拟合过程的一部分执行各种数据转换。这些转换器可以很好地处理 dask 集合(dask.array, dask.dataframe)、NumPy 数组或 pandas 数据框。它们将并行进行拟合(fit)和转换(transform)。

Scikit-Learn 克隆

一些转换器(transformer)是其 scikit-learn 对应项的(大部分)直接替代品。

MinMaxScaler([feature_range, copy, clip])

通过缩放每个特征到给定范围来转换特征。

QuantileTransformer(*[, n_quantiles, ...])

使用分位数信息转换特征。

RobustScaler(*[, with_centering, ...])

使用对异常值鲁棒的统计量来缩放特征。

StandardScaler(*[, copy, with_mean, with_std])

通过移除均值并缩放到单位方差来标准化特征。

LabelEncoder([use_categorical])

将标签编码为 0 到 n_classes-1 之间的值。

OneHotEncoder(n_values, ...)

将类别整数特征编码为独热(one-hot)数值数组。

PolynomialFeatures([degree, ...])

生成多项式和交互特征。

它们可以像 scikit-learn 版本一样使用,不同之处在于

  1. 它们并行处理 dask 集合

  2. .transform 在输入是 dask 集合时将返回一个 dask.arraydask.dataframe

有关任何特定转换器的更多信息,请参阅 sklearn.preprocessing。Scikit-learn 确实有一些转换器可以作为 Dask 处理大内存任务的替代方案。这些包括 FeatureHasher(是 DictVectorizerCountVectorizer 的良好替代品)和 HashingVectorizer(最适合在文本处理中使用,优于 CountVectorizer)。它们是无状态的,这使得它们可以轻松地与 Dask 的 map_partitions 一起使用。

In [1]: import dask.bag as db

In [2]: from sklearn.feature_extraction import FeatureHasher

In [3]: D = [{'dog': 1, 'cat':2, 'elephant':4}, {'dog': 2, 'run': 5}]

In [4]: b = db.from_sequence(D)

In [5]: h = FeatureHasher()

In [6]: b.map_partitions(h.transform).compute()
Out[6]: 
[<Compressed Sparse Row sparse matrix of dtype 'float64'
 	with 3 stored elements and shape (1, 1048576)>,
 <Compressed Sparse Row sparse matrix of dtype 'float64'
 	with 2 stored elements and shape (1, 1048576)>]

注意

dask_ml.preprocessing.LabelEncoderdask_ml.preprocessing.OneHotEncoder 将使用 pandas.api.types.CategoricalDtype 的 dask 或 pandas Series 的类别 dtype 信息。这提高了性能,但可能导致不同的编码,具体取决于类别。更多信息请参阅类文档字符串。

类别特征编码

dask_ml.preprocessing.OneHotEncoder 可用于对特征进行“独热”(或“虚拟变量”)编码。

有关完整讨论,请参阅 scikit-learn 文档。本节仅关注与 scikit-learn 的差异。

Dask-ML 支持 pandas 的 Categorical dtype

Dask-ML 支持并使用 pandas Categorical dtype 的类型信息。有关介绍,请参阅 https://pandas.ac.cn/pandas-docs/stable/categorical.html。对于大型数据集,使用类别 dtype 对于提高性能至关重要。

这将对学习到的属性和转换后的值产生一些影响。

  1. 学习到的 categories_ 可能有所不同。Scikit-Learn 要求类别是排序的。使用 CategoricalDtype 时,类别不需要排序。

  2. OneHotEncoder.transform() 的输出类型将与输入类型相同。传入 pandas DataFrame 将返回 pandas DataFrame,而不是 NumPy 数组。同样,Dask DataFrame 返回 Dask DataFrame。

Dask-ML 的稀疏数据支持

OneHotEncoder 的默认行为是返回稀疏数组。Scikit-Learn 对于传递给 transform 的 ndarrays 返回 SciPy 稀疏矩阵。

当传入 Dask Array 时,OneHotEncoder.transform() 返回一个 Dask Array,*其中每个块都是一个 scipy 稀疏矩阵*。SciPy 稀疏矩阵不支持与 NumPy ndarray 相同的 API,因此大多数方法对结果无效。即使是像 compute 这样的基本操作也会失败。为了解决这个问题,我们目前建议将稀疏矩阵转换为密集矩阵。

from dask_ml.preprocessing import OneHotEncoder
import dask.array as da
import numpy as np

enc = OneHotEncoder(sparse=True)
X = da.from_array(np.array([['A'], ['B'], ['A'], ['C']]), chunks=2)
enc = enc.fit(X)
result = enc.transform(X)
result

result 的每个块都是一个 scipy 稀疏矩阵

result.blocks[0].compute()
# This would fail!
# result.compute()
# Convert to, say, pydata/sparse COO matrices instead
from sparse import COO

result.map_blocks(COO.from_scipy_sparse, dtype=result.dtype).compute()

Dask-ML 对稀疏数据的支持目前正在变化中。如有任何问题,请联系我们。

附加的转换器

其他转换器是 dask-ml 特有的。

Categorizer([categories, columns])

将 DataFrame 的列转换为 categorical dtype。

DummyEncoder([columns, drop_first])

对类别列进行虚拟变量(独热)编码。

OrdinalEncoder([columns])

对类别列进行序号(整数)编码。

dask_ml.preprocessing.Categorizerdask_ml.preprocessing.DummyEncoder 都处理将非数值数据转换为数值数据。它们在流水线中作为预处理步骤非常有用,特别是当你从异构数据(数值和非数值混合)开始,但估计器需要所有数值数据时。

在这个玩具示例中,我们使用一个包含两列的数据集。'A' 是数值类型,'B' 包含文本数据。我们构建一个小型流水线来:

  1. 对文本数据进行类别化

  2. 对类别数据进行虚拟变量编码

  3. 拟合线性回归

In [7]: from dask_ml.preprocessing import Categorizer, DummyEncoder

In [8]: from sklearn.linear_model import LogisticRegression

In [9]: from sklearn.pipeline import make_pipeline

In [10]: import pandas as pd

In [11]: import dask.dataframe as dd

In [12]: df = pd.DataFrame({"A": [1, 2, 1, 2], "B": ["a", "b", "c", "c"]})

In [13]: X = dd.from_pandas(df, npartitions=2)

In [14]: y = dd.from_pandas(pd.Series([0, 1, 1, 0]), npartitions=2)

In [15]: pipe = make_pipeline(
   ....:    Categorizer(),
   ....:    DummyEncoder(),
   ....:    LogisticRegression(solver='lbfgs')
   ....: )
   ....: 

In [16]: pipe.fit(X, y)
Out[16]: 
Pipeline(steps=[('categorizer', Categorizer()),
                ('dummyencoder', DummyEncoder()),
                ('logisticregression', LogisticRegression())])

Categorizer 将把 X 中的一部分列转换为 categorical dtype(有关 pandas 如何处理类别数据,请参阅此处)。默认情况下,它会转换所有 object dtype 的列。

DummyEncoder 将对数据集进行虚拟变量(或独热)编码。这将一个类别列替换为多个列,其中值为 0 或 1,取决于原始值。

In [17]: df['B']
Out[17]: 
0    a
1    b
2    c
3    c
Name: B, dtype: object

In [18]: pd.get_dummies(df['B'])
Out[18]: 
       a      b      c
0   True  False  False
1  False   True  False
2  False  False   True
3  False  False   True

原始值为 'a' 的地方,转换后在 a 列中为 1,在其他地方为 0

为什么 Categorizer 步骤是必要的?为什么我们不能直接操作 object (字符串) dtype 列?这样做会很脆弱,尤其是在使用 dask.dataframe 时,因为*输出的形状将取决于存在的值*。例如,假设我们在训练数据集中只看到前两行,在测试数据集中只看到后两行。那么,在训练时,我们转换后的列将是

In [19]: pd.get_dummies(df.loc[[0, 1], 'B'])
Out[19]: 
       a      b
0   True  False
1  False   True

而在测试数据集上,它们将是

In [20]: pd.get_dummies(df.loc[[2, 3], 'B'])
Out[20]: 
      c
2  True
3  True

这是不正确的!列不匹配。

当我们将数据类别化时,我们可以确信所有可能的值都已指定,这样输出的形状就不再取决于我们当前看到的任何数据子集中的值。相反,它取决于 categories,这些类别在所有子集中都是相同的。