sklearn.LabelEncoder,值之前从未出现


74

如果将asklearn.LabelEncoder拟合到训练集上,则在测试集上使用时如果遇到新的值,它可能会损坏。

为此我唯一能想到的解决方案是将测试集中的所有新内容(即不属于任何现有类)映射到"<unknown>",然后在其后显式添加一个相应的类LabelEncoder

# train and test are pandas.DataFrame's and c is whatever column
le = LabelEncoder()
le.fit(train[c])
test[c] = test[c].map(lambda s: '<unknown>' if s not in le.classes_ else s)
le.classes_ = np.append(le.classes_, '<unknown>')
train[c] = le.transform(train[c])
test[c] = le.transform(test[c])

这可行,但是有更好的解决方案吗?

更新资料

正如@sapo_cosmico在评论中指出的那样,鉴于我假设是中的实现更改LabelEncoder.transform,现在上述似乎不再起作用了,该更改现在似乎正在使用np.searchsorted(我不知道以前是否是这种情况)。因此,无需将<unknown>类追加到LabelEncoder已经提取的类的列表中,而需要按排序顺序将其插入:

import bisect
le_classes = le.classes_.tolist()
bisect.insort_left(le_classes, '<unknown>')
le.classes_ = le_classes

但是,由于总体而言这感觉很笨拙,因此我敢肯定有更好的方法。


Answers:


42

由于看不见数据的问题,我最终切换到Pandas的get_dummies

  • 在训练数据上创建假人
    dummy_train = pd.get_dummies(train)
  • 在新的假人中创建假人(看不见的数据)
    dummy_new = pd.get_dummies(new_data)
  • 将新数据重新索引到训练数据的列中,用0填充缺失值
    dummy_new.reindex(columns = dummy_train.columns, fill_value=0)

实际上,任何分类的新功能都不会进入分类器,但是我认为这不会引起问题,因为它不知道如何处理它们。


2
而是dummies.columns您的意思dummy_train.columns
凯文·马克汉姆

1
@KevinMarkham向您致敬,先生,发现了已经存在了将近一年的错误:)
sapo_cosmico

保存(修补)模型时,是否将dummy_train.columns其保存到其自己的文件中?
matthiash

2
@matthiash通常,我将在管道对象中使用它。我不能说我对酸洗了解得足够多,我通常会避免使用酸洗,但是可以冒险猜测管道中的状态应该保留并保留这些列
sapo_cosmico

在我的情况下,@ matthiash将列与模型保存在同一文件中。只要确保您以相同的顺序进行读写即可!
shikhanshu

38

LabelEncoder基本上是一个字典。您可以提取并将其用于将来的编码:

from sklearn.preprocessing import LabelEncoder

le = preprocessing.LabelEncoder()
le.fit(X)

le_dict = dict(zip(le.classes_, le.transform(le.classes_)))

检索单个新项目的标签,如果缺少项目,则将值设置为unknown

le_dict.get(new_item, '<Unknown>')

检索数据框列的标签:

df[your_col] = df[your_col].apply(lambda x: le_dict.get(x, <unknown_value>))

这是一个相当简洁有效的答案;)(赞)
弃用

25

我创建了一个类来支持这一点。如果您有一个新标签,它将被分配为未知类。

from sklearn.preprocessing import LabelEncoder
import numpy as np


class LabelEncoderExt(object):
    def __init__(self):
        """
        It differs from LabelEncoder by handling new classes and providing a value for it [Unknown]
        Unknown will be added in fit and transform will take care of new item. It gives unknown class id
        """
        self.label_encoder = LabelEncoder()
        # self.classes_ = self.label_encoder.classes_

    def fit(self, data_list):
        """
        This will fit the encoder for all the unique values and introduce unknown value
        :param data_list: A list of string
        :return: self
        """
        self.label_encoder = self.label_encoder.fit(list(data_list) + ['Unknown'])
        self.classes_ = self.label_encoder.classes_

        return self

    def transform(self, data_list):
        """
        This will transform the data_list to id list where the new values get assigned to Unknown class
        :param data_list:
        :return:
        """
        new_data_list = list(data_list)
        for unique_item in np.unique(data_list):
            if unique_item not in self.label_encoder.classes_:
                new_data_list = ['Unknown' if x==unique_item else x for x in new_data_list]

        return self.label_encoder.transform(new_data_list)

样本用法:

country_list = ['Argentina', 'Australia', 'Canada', 'France', 'Italy', 'Spain', 'US', 'Canada', 'Argentina, ''US']

label_encoder = LabelEncoderExt()

label_encoder.fit(country_list)
print(label_encoder.classes_) # you can see new class called Unknown
print(label_encoder.transform(country_list))


new_country_list = ['Canada', 'France', 'Italy', 'Spain', 'US', 'India', 'Pakistan', 'South Africa']
print(label_encoder.transform(new_country_list))

您的解决方案是迄今为止最简单的解决方案。谢谢!
AleksandarMakragić,

8

我的印象是,您所做的工作与面对这种情况时其他人所做的非常相似。

已经进行了一些努力以将对看不见的标签进行编码的功能添加到LabelEncoder中(尤其参阅https://github.com/scikit-learn/scikit-learn/pull/3483https://github.com/scikit-learn/ scikit-learn / pull / 3599),但是更改现有行为实际上比乍一看要困难得多。

目前看来,处理“词汇外”标签留给了scikit-learn的各个用户。


7

我最近遇到了这个问题,并且能够快速解决该问题。我的答案不仅仅解决了这个问题,还可以轻松解决您的问题。(我认为它很酷)

我正在使用熊猫数据帧,并且最初使用sklearns labelencoder()编码我的数据,然后将其腌制以在程序的其他模块中使用。

但是,sklearn预处理中的标签编码器无法将新值添加到编码算法中。我解决了对多个值进行编码并将映射值保存为WELL的问题,因为可以通过以下方式向编码器添加新值(这是我所做工作的粗略概述):

encoding_dict = dict()
for col in cols_to_encode:
    #get unique values in the column to encode
    values = df[col].value_counts().index.tolist()

    # create a dictionary of values and corresponding number {value, number}
    dict_values = {value: count for value, count in zip(values, range(1,len(values)+1))}

    # save the values to encode in the dictionary
    encoding_dict[col] = dict_values

    # replace the values with the corresponding number from the dictionary
    df[col] = df[col].map(lambda x: dict_values.get(x))

然后,您可以简单地将字典保存到JSON文件中,并能够通过添加新值和相应的整数值来拉出字典并添加所需的任何值。

我将解释使用map()而不是replace()的背后原因。我发现使用pandas replace()函数花了一分钟的时间来遍历大约117,000行代码。使用map可使该时间超过100毫秒。

TLDR:无需使用sklearns预处理,而是通过制作映射字典并自己映射值来使用数据框。


太好了
约翰·梭哈

3

我知道有两个开发人员正在围绕变压器和Sklearn管道构建包装器。它们具有2个坚固的编码器变压器(一个虚拟和一个标签编码器),可以处理看不见的值。这是他们的skutil库的文档。搜索skutil.preprocessing.OneHotCategoricalEncoderskutil.preprocessing.SafeLabelEncoder。在它们中SafeLabelEncoder(),看不见的值会自动编码为999999。


2
他们有没有尝试屈服于sklearn自己?这是一个普遍的问题。显然,我们对default_label_value进行了参数化。
smci

只是好奇,将默认值设置为-1而不是999999会完全有益吗?假设我的类别有56个类别,我想我希望标签在-1和56之间,而不是0到56之间,并在其末尾加上999999。另外,如果您在缩放之前进行了分类转换,那么您可以将数字以0到1的比例压缩或适当地缩放/居中,是吗?如果要使用999999,这似乎会消除进一步处理的可能性,并可能为要素的比例增加一个极为不同的幅度。我在想什么吗?
TaylorV

通常,在我的大多数工作流程中,看不见的值会在推断/预测时间内从管道中过滤掉。所以对我来说,将其编码为-1还是999999都没关系。–
Jason

2

我试图解决此问题,发现了两种方便的方法,可以在使用和不使用LabelEncoder的情况下,对来自训练和测试集的分类数据进行编码。新类别中填充了一些已知的分类法“ c”(例如“ other”或“ missing”)。第一种方法似乎工作更快。希望对您有所帮助。

import pandas as pd
import time
df=pd.DataFrame()

df["a"]=['a','b', 'c', 'd']
df["b"]=['a','b', 'e', 'd']


#LabelEncoder + map
t=time.clock()
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
suf="_le"
col="a"
df[col+suf] = le.fit_transform(df[col])
dic = dict(zip(le.classes_, le.transform(le.classes_)))
col='b'
df[col+suf]=df[col].map(dic).fillna(dic["c"]).astype(int)
print(time.clock()-t)

#---
#pandas category

t=time.clock()
df["d"] = df["a"].astype('category').cat.codes
dic =df["a"].astype('category').cat.categories.tolist()
df['f']=df['b'].astype('category',categories=dic).fillna("c").cat.codes
df.dtypes
print(time.clock()-t)

在此#pandas category方法中,生产线df['f']=df['b'].astype('category',categories=dic)........出现此错误:TypeError: astype() got an unexpected keyword argument 'categories'
edesz

2

这是使用来自熊猫的相对较新的功能。其主要动机是像“ lightgbm”这样的机器学习包可以接受pandas类作为特征列,并且在某些情况下比使用onehotencoding更好。在此示例中,转换器返回一个整数,但也可以更改日期类型,并用-1替换为看不见的分类值。

from collections import defaultdict
from sklearn.base import BaseEstimator,TransformerMixin
from pandas.api.types import CategoricalDtype
import pandas as pd
import numpy as np

class PandasLabelEncoder(BaseEstimator,TransformerMixin):
    def __init__(self):
        self.label_dict = defaultdict(list)

    def fit(self, X):
        X = X.astype('category')
        cols = X.columns
        values = list(map(lambda col: X[col].cat.categories, cols))
        self.label_dict = dict(zip(cols,values))
        # return as category for xgboost or lightgbm 
        return self

    def transform(self,X):
        # check missing columns
        missing_col=set(X.columns)-set(self.label_dict.keys())
        if missing_col:
            raise ValueError('the column named {} is not in the label dictionary. Check your fitting data.'.format(missing_col)) 
        return X.apply(lambda x: x.astype('category').cat.set_categories(self.label_dict[x.name]).cat.codes.astype('category').cat.set_categories(np.arange(len(self.label_dict[x.name]))))


    def inverse_transform(self,X):
        return X.apply(lambda x: pd.Categorical.from_codes(codes=x.values,
                                                           categories=self.label_dict[x.name]))

dff1 = pd.DataFrame({'One': list('ABCC'), 'Two': list('bccd')})
dff2 = pd.DataFrame({'One': list('ABCDE'), 'Two': list('debca')})


enc=PandasLabelEncoder()
enc.fit_transform(dff1)
One Two
0   0   0
1   1   1
2   2   1
3   2   2
dff3=enc.transform(dff2)
dff3
    One Two
0   0   2
1   1   -1
2   2   0
3   -1  1
4   -1  -1
enc.inverse_transform(dff3)
One Two
0   A   d
1   B   NaN
2   C   b
3   NaN c
4   NaN NaN

1

LabelEncoder()仅应用于目标标签编码。要对分类特征进行编码,请使用OneHotEncoder(),该类可以处理看不见的值:https ://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html#sklearn.preprocessing.OneHotEncoder


如果功能的基数大于10000+怎么办?
jeevs

视情况而定。多种解决方案是可能的。也许您应该考虑进行分类或嵌入。不了解实际情况很难。
亚历克斯

1

scikit-learn0.24.0版本开始,您不必使用LabelEncoder功能(也应该使用OrdinalEncoder),因此它的名称为LabelEncoder

由于模型永远不会预测在训练数据中看不到的标签,LabelEncoder因此永远都不应支持未知标签。

但是对于功能,它是不同的,因为很显然您可能会遇到训练集中从未见过的不同类别。在0.24.0版中,为scikit-learn提出了两个新参数OrdinalEncoder,使它可以对未知类别进行编码。

OrdinalEncoder编码要素并将未知类别转换为值的示例用法-1

from sklearn.preprocessing import OrdinalEncoder

# Create encoder
ordinal_encoder = OrdinalEncoder(handle_unknown='use_encoded_value',
                                 unknown_value=-1)

# Fit on training data
ordinal_encoder.fit(np.array([1,2,3,4,5]).reshape(-1, 1))

# Transform, notice that 0 and 6 are values that were never seen before
ordinal_encoder.transform(np.array([0,1,2,3,4,5,6]).reshape(-1, 1))

输出:

array([[-1.],
       [ 0.],
       [ 1.],
       [ 2.],
       [ 3.],
       [ 4.],
       [-1.]])

0

我遇到了同样的问题,并意识到我的编码器以某种方式在列数据帧内混合了值。假设您将编码器运行了几列,并且在为标签分配数字时,编码器会自动向其中写入数字,有时还会发现您有两个具有相似值的不同列。我要解决的问题是为我的pandas DataFrame中的每一列创建一个LabelEncoder()实例,结果很不错。

encoder1 = LabelEncoder()
encoder2 = LabelEncoder()
encoder3 = LabelEncoder()

df['col1'] = encoder1.fit_transform(list(df['col1'].values))
df['col2'] = encoder2.fit_transform(list(df['col2'].values))
df['col3'] = encoder3.fit_transform(list(df['col3'].values))

问候!!


0

如果有人仍在寻找,这是我的解决方法。

假设您有
enc_list:已编码的变量名称列表
enc_map:包含来自变量的字典enc_list以及相应的编码映射
df:包含不存在的变量值的数据框enc_map

假设您在编码值中已经具有类别“ NA”或“未知”,则此方法有效

for l in enc_list:  

    old_list = enc_map[l].classes_
    new_list = df[l].unique()
    na = [j for j in new_list if j not in old_list]
    df[l] = df[l].replace(na,'NA')

-1

如果仅用于训练和测试模型,为什么不对整个数据集进行标签编码。然后使用从编码器对象生成的类。

encoder = LabelEncoder()
encoder.fit_transform(df["label"])
train_y = encoder.transform(train_y)
test_y = encoder.transform(test_y)

10
我相信这样做将是数据泄漏(基本ML罪恶)的一个实例。
cjauvin

2
这似乎是一个很好的解决方案。正如我所看到的,当我们正在做的是对变量进行编码时,没有泄漏的问题。
Regi Mathew

对于新的数据,看到我的解决办法:stackoverflow.com/questions/45495308/...
Namrata托拉尼

1
如果我们事先有固定的测试数据,则此解决方案有效。但是,在大多数情况下我们都不知道测试数据的现实应用中这是不可能的。
Sufyan Khot
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.