【问题标题】:Create a custom sklearn TransformerMixin that transforms categorical variables consistently创建一个自定义的 sklearn TransformerMixin,它可以一致地转换分类变量
【发布时间】:2018-06-27 11:20:27
【问题描述】:

这个问题不是有人建议的duplicate。为什么?因为在那个例子中,所有可能的值都是已知的。在这个例子中,它们不是。此外,除了对未知值使用自定义转换器之外,这个问题还专门询问如何以与初始转换相同的方式执行转换。我可以再一次告诉我我最终必须回答我自己的问题。


在创建自定义 scikit-learn 转换器时,如何保证或“强制”转换方法仅输出它最初匹配的列?

下图说明。这是我的示例转换器。

import numpy as np
import pandas as pd
from sklearn.base import TransformerMixin
from sklearn.linear_model import LogisticRegression

class DFTransformer(TransformerMixin):

    def fit(self, df, y=None, **fit_params):
        return self

    def transform(self, df, **trans_params):
        self.df = df
        self.STACKER = pd.DataFrame()

        for col in self.df:
            dtype = self.df[col].dtype.name
            if dtype == 'object':
                self.STACKER = pd.concat([self.STACKER, self.get_dummies(col)], axis=1)
            elif dtype == 'int64':
                self.STACKER = pd.concat([self.STACKER, self.cut_it(col)], axis=1)

        return self.STACKER

    def get_dummies(self, name):
        return pd.get_dummies(self.df[name], prefix=name)

    def cut_it(self, name, bins=5):
        s = self.df[name].copy()
        return pd.get_dummies(pd.cut(s, bins), prefix=name)

这里有一些虚拟数据。我的一种方法使用pd.cut in-efforts 来合并大范围的整数或浮点数。另一种方法使用pd.get_dummies,它将唯一值转换为列。

df = pd.DataFrame({'integers': np.random.randint(2000, 20000, 30, dtype='int64'),
                   'categorical': np.random.choice(list('ABCDEFGHIJKLMNOP'), 30)},
                  columns=['integers', 'categorical'])

trans = DFTransformer()
X = trans.fit_transform(df)
y = np.random.binomial(1, 0.5, 30)
lr = LogisticRegression()
lr.fit(X, y)

X_test = pd.DataFrame({'integers': np.random.randint(2000, 60000, 30, dtype='int64'),
                   'categorical': np.random.choice(list('ABGIOPXYZ'), 30)},
                  columns=['integers', 'categorical'])
lr.predict(trans.transform(X_test))

我遇到的问题是,当我去转换“测试”数据(我想对其进行预测的数据)时,转换很可能不会输出完全相同的列,因为不同的分类值(例如:可能出现一次然后再也看不到或听到的模糊值)。

例如,上面的代码会产生这个错误:

Traceback (most recent call last):
  File "C:/Users/myname/Downloads/SO009949884.py", line 44, in <module>
    lr.predict(trans.transform(X_test))
  File "C:\python36\lib\site-packages\sklearn\linear_model\base.py", line 324, in predict
    scores = self.decision_function(X)
  File "C:\python36\lib\site-packages\sklearn\linear_model\base.py", line 305, in decision_function
    % (X.shape[1], n_features))
ValueError: X has 14 features per sample; expecting 20

问题:如何确保我的转换方法以相同的方式转换我的测试数据?

我能想到的一个糟糕的解决方案是:转换训练数据、转换测试数据、查看列相交的位置、修改我的转换函数以将输出限制为这些列。或者,为缺少的那些填写空白列。这是不可扩展的。肯定有更好的方法吗?我不想事先知道输出列必须是什么。

我的总体目标是在训练和测试数据集中以一致的方式转换分类变量。我有 150 多列要转换!

【问题讨论】:

  • 如果可以,请使用当前开发分支中的CategoricalEncoder instead。其他见我的other answer
  • pd.get_dummies() 仅在对整个数据使用然后拆分时才推荐使用。反之亦然。
  • @VivekKumar 如果使用 get_dummies 与 CategoricalEncoder 有什么区别?当应用于测试数据时,是否存在一些有助于转换过程的有利差异?我看不出您的建议如何解决这里的问题,即测试数据中的某些值不存在和/或测试数据具有训练数据转换没有的值。
  • pd.get_dummies 在不同的数据上使用时会返回不同的列,因此在拆分后使用它显然会返回不同的列数和您现在面临的相同问题。
  • 我写了一篇博文来解决这个问题:blog.pursuitofzen.com/…

标签: python scikit-learn


【解决方案1】:

我写了一篇博文来解决这个问题。下面是我搭建的变压器。

class CategoryGrouper(BaseEstimator, TransformerMixin):  
    """A tranformer for combining low count observations for categorical features.

    This transformer will preserve category values that are above a certain
    threshold, while bucketing together all the other values. This will fix issues
    where new data may have an unobserved category value that the training data
    did not have.
    """

    def __init__(self, threshold=0.05):
        """Initialize method.

        Args:
            threshold (float): The threshold to apply the bucketing when
                categorical values drop below that threshold.
        """
        self.d = defaultdict(list)
        self.threshold = threshold

    def transform(self, X, **transform_params):
        """Transforms X with new buckets.

        Args:
            X (obj): The dataset to pass to the transformer.

        Returns:
            The transformed X with grouped buckets.
        """
        X_copy = X.copy()
        for col in X_copy.columns:
            X_copy[col] = X_copy[col].apply(lambda x: x if x in self.d[col] else 'CategoryGrouperOther')
        return X_copy

    def fit(self, X, y=None, **fit_params):
        """Fits transformer over X.

        Builds a dictionary of lists where the lists are category values of the
        column key for preserving, since they meet the threshold.
        """
        df_rows = len(X.index)
        for col in X.columns:
            calc_col = X.groupby(col)[col].agg(lambda x: (len(x) * 1.0) / df_rows)
            self.d[col] = calc_col[calc_col >= self.threshold].index.tolist()
        return self

基本上,动机最初来自我必须处理稀疏类别值,但后来我意识到这可以应用于未知值。转换器本质上是在给定阈值的情况下将稀疏类别值组合在一起,因此由于未知值将继承 0% 的值空间,因此它们将被分桶到 CategoryGrouperOther 组中。

这里只是一个变压器的演示:

# dfs with 100 elements in cat1 and cat2
# note how df_test has elements 'g' and 't' in the respective categories (unknown values)
df_train = pd.DataFrame({'cat1': ['a'] * 20 + ['b'] * 30 + ['c'] * 40 + ['d'] * 3 + ['e'] * 4 + ['f'] * 3,
                         'cat2': ['z'] * 25 + ['y'] * 25 + ['x'] * 25 + ['w'] * 20 +['v'] * 5})
df_test = pd.DataFrame({'cat1': ['a'] * 10 + ['b'] * 20 + ['c'] * 5 + ['d'] * 50 + ['e'] * 10 + ['g'] * 5,
                        'cat2': ['z'] * 25 + ['y'] * 55 + ['x'] * 5 + ['w'] * 5 + ['t'] * 10})

catgrouper = CategoryGrouper()
catgrouper.fit(df_train)
df_test_transformed = catgrouper.transform(df_test)

df_test_transformed

    cat1    cat2
0   a   z
1   a   z
2   a   z
3   a   z
4   a   z
5   a   z
6   a   z
7   a   z
8   a   z
9   a   z
10  b   z
11  b   z
12  b   z
13  b   z
14  b   z
15  b   z
16  b   z
17  b   z
18  b   z
19  b   z
20  b   z
21  b   z
22  b   z
23  b   z
24  b   z
25  b   y
26  b   y
27  b   y
28  b   y
29  b   y
... ... ...
70  CategoryGrouperOther    y
71  CategoryGrouperOther    y
72  CategoryGrouperOther    y
73  CategoryGrouperOther    y
74  CategoryGrouperOther    y
75  CategoryGrouperOther    y
76  CategoryGrouperOther    y
77  CategoryGrouperOther    y
78  CategoryGrouperOther    y
79  CategoryGrouperOther    y
80  CategoryGrouperOther    x
81  CategoryGrouperOther    x
82  CategoryGrouperOther    x
83  CategoryGrouperOther    x
84  CategoryGrouperOther    x
85  CategoryGrouperOther    w
86  CategoryGrouperOther    w
87  CategoryGrouperOther    w
88  CategoryGrouperOther    w
89  CategoryGrouperOther    w
90  CategoryGrouperOther    CategoryGrouperOther
91  CategoryGrouperOther    CategoryGrouperOther
92  CategoryGrouperOther    CategoryGrouperOther
93  CategoryGrouperOther    CategoryGrouperOther
94  CategoryGrouperOther    CategoryGrouperOther
95  CategoryGrouperOther    CategoryGrouperOther
96  CategoryGrouperOther    CategoryGrouperOther
97  CategoryGrouperOther    CategoryGrouperOther
98  CategoryGrouperOther    CategoryGrouperOther
99  CategoryGrouperOther    CategoryGrouperOther

甚至在我将阈值设置为 0 时也可以工作(这将专门为“其他”组设置未知值,同时保留所有其他类别值)。不过,我会提醒不要将阈值设置为 0,因为您的训练数据集没有“其他”类别,因此请调整阈值以将至少一个值标记为“其他”组:

catgrouper = CategoryGrouper(threshold=0)
catgrouper.fit(df_train)
df_test_transformed = catgrouper.transform(df_test)

df_test_transformed

    cat1    cat2
0   a   z
1   a   z
2   a   z
3   a   z
4   a   z
5   a   z
6   a   z
7   a   z
8   a   z
9   a   z
10  b   z
11  b   z
12  b   z
13  b   z
14  b   z
15  b   z
16  b   z
17  b   z
18  b   z
19  b   z
20  b   z
21  b   z
22  b   z
23  b   z
24  b   z
25  b   y
26  b   y
27  b   y
28  b   y
29  b   y
... ... ...
70  d   y
71  d   y
72  d   y
73  d   y
74  d   y
75  d   y
76  d   y
77  d   y
78  d   y
79  d   y
80  d   x
81  d   x
82  d   x
83  d   x
84  d   x
85  e   w
86  e   w
87  e   w
88  e   w
89  e   w
90  e   CategoryGrouperOther
91  e   CategoryGrouperOther
92  e   CategoryGrouperOther
93  e   CategoryGrouperOther
94  e   CategoryGrouperOther
95  CategoryGrouperOther    CategoryGrouperOther
96  CategoryGrouperOther    CategoryGrouperOther
97  CategoryGrouperOther    CategoryGrouperOther
98  CategoryGrouperOther    CategoryGrouperOther
99  CategoryGrouperOther    CategoryGrouperOther

【讨论】:

  • 博文链接已失效。 (说 503,但实际上可能不是临时的,除非有什么问题是固定的。)
  • @jpmc26 感谢您的通知。是的,由于托管服务进行了更改,该链接与整个博客一起失效。我必须启动另一个博客实例并将数据从旧博客迁移到新博客。
【解决方案2】:

就像我说的,回答我自己的问题。这是我现在要使用的解决方案。

def get_datasets(df):
    trans1= DFTransformer()
    trans2= DFTransformer()
    train = trans1.fit_transform(df.iloc[:, :-1])
    test = trans2.fit_transform(pd.read_pickle(TEST_PICKLE_PATH))
    columns = train.columns.intersection(test.columns).tolist()
    X_train = train[columns]
    y_train = df.iloc[:, -1]
    X_test = test[columns]
    return X_train, y_train, X_test

【讨论】:

    【解决方案3】:

    如果您担心 pd.get_dummies() 输出错误的尺寸,您可以简单地为您的列指定分类编码。

    例如:

    fit_df = pd.DataFrame({'COUNTRY': ['UK', 'FR', 'IT']}, dtype='category')
    fit_categories = fit_df.COUNTRY.cat.categories
    
    predict_df = pd.DataFrame({'COUNTRY': ['UK']}, dtype='category')
    predict_df.COUNTRY = predict_df.COUNTRY.cat.set_categories(fit_categories)
    pd.get_dummies(predict_df)
    

    将返回下表:

       COUNTRY_FR    COUNTRY_IT    COUNTRY_UK
                0             0             1
    

    因此,在您的情况下,您可以简单地在配置文件中定义分类编码,或者让转换器类跟踪初始编码。

    这种方法也可以通过使用pd.Series.cat.add_categories扩展来处理看不见的分类值

    希望这会有所帮助。

    更多信息请参见documentation

    【讨论】:

      猜你喜欢
      • 2018-11-30
      • 2020-07-31
      • 2018-09-21
      • 1970-01-01
      • 1970-01-01
      • 2011-10-15
      • 2016-03-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多