在Scikit-learn中跨多列进行标签编码

323

我正在尝试使用scikit-learn的LabelEncoder对pandas中的字符串标签DataFrame进行编码。由于数据帧有许多(50+)列,我想避免为每个列创建一个LabelEncoder对象;我宁愿只有一个可以在所有数据列上工作的大型LabelEncoder对象。

将整个DataFrame放入LabelEncoder会产生以下错误。请注意,这里我使用的是虚拟数据;实际上,我正在处理大约50个字符串标记数据列,因此需要一种不引用任何列名称的解决方案。

import pandas
from sklearn import preprocessing 

df = pandas.DataFrame({
    'pets': ['cat', 'dog', 'cat', 'monkey', 'dog', 'dog'], 
    'owner': ['Champ', 'Ron', 'Brick', 'Champ', 'Veronica', 'Ron'], 
    'location': ['San_Diego', 'New_York', 'New_York', 'San_Diego', 'San_Diego', 
                 'New_York']
})

le = preprocessing.LabelEncoder()

le.fit(df)

Traceback (most recent call last): File "", line 1, in File "/Users/bbalin/anaconda/lib/python2.7/site-packages/sklearn/preprocessing/label.py", line 103, in fit y = column_or_1d(y, warn=True) File "/Users/bbalin/anaconda/lib/python2.7/site-packages/sklearn/utils/validation.py", line 306, in column_or_1d raise ValueError("bad input shape {0}".format(shape)) ValueError: bad input shape (6, 3)

您有什么想法可以解决这个问题吗?


为了简化对包含字符串数据的多列dataframe进行编码,我正在选择编码对象,因此希望避免对50个独立对象进行pickle/unpickle操作。另外,我想知道是否有一种方法可以让编码器简化数据,例如只返回每列中变量的唯一组合的一个标识符所在的行。 - Bryan
可以通过将一个字典的字典传递给 replace 方法,在 pandas 中以简单的方式完成所有操作。请参见下面的答案 - Ted Petrou
1
scikit-learn 0.20 开始,不需要实现自定义类来对多列进行标签编码。您可以直接使用OrdinalEncoder - Ric S
25个回答

606

不过,你可以很容易地做到这一点。

df.apply(LabelEncoder().fit_transform)

编辑2:

在scikit-learn 0.20中,推荐的方法是:

OneHotEncoder().fit_transform(df)

现在OneHotEncoder支持字符串输入。使用ColumnTransformer可以仅将OneHotEncoder应用于特定列。

编辑:由于这个原始回答已经超过一年了,并且获得了许多赞(包括悬赏),我应该进一步扩展一下。

对于inverse_transform和transform,你需要做一点小技巧。

from collections import defaultdict
d = defaultdict(LabelEncoder)

有了这个,你现在将所有的列 LabelEncoder 保留为字典。

# Encoding the variable
fit = df.apply(lambda x: d[x.name].fit_transform(x))

# Inverse the encoded
fit.apply(lambda x: d[x.name].inverse_transform(x))

# Using the dictionary to label future data
df.apply(lambda x: d[x.name].transform(x))

更多编辑:

使用 Neuraxle 的 FlattenForEach 步骤,同样可以一次对所有展平的数据使用相同的 LabelEncoder 进行编码:

FlattenForEach(LabelEncoder(), then_unflatten=True).fit_transform(df)

如果您需要为数据的不同列使用单独的LabelEncoder,或者仅需要对部分列进行标签编码而不是全部列,则使用ColumnTransformer可以更好地控制您的列选择和LabelEncoder实例。


5
这很棒,但在这种情况下我们如何应用逆变换? - Supreeth Meka
14
如果我想在管道中使用这个解决方案,例如分离拟合和转换(在训练集上进行拟合,然后在测试集上使用 --> 重复使用已学习的字典),那么 df.apply(LabelEncoder().fit_transform) 支持吗? - Georg Heiler
2
如何使用LabelBinarizer使其正常工作,并重复使用字典进行测试集?我尝试了d = defaultdict(LabelBinarizer),然后fit = df.apply(lambda x: d[x.name].fit_transform(x)),但是会引发异常:Exception: Data must be 1-dimensional。我不确定预期的DataFrame应该是什么样子...也许每列都应该保存二进制向量。 - Qululu
8
好的解决方案。如何仅在特定列中进行转换? - stenlytw
1
如果我只想反转一个列的编码,我该怎么做? - Ib D
显示剩余12条评论

129

正如larsmans所提到的,LabelEncoder()只接受一个一维数组作为参数。话虽如此,很容易编写自己的标签编码器,以操作您选择的多个列,并返回转换后的数据帧。我的代码在某种程度上基于Zac Stewart在这里发表的博客文章。

创建自定义编码器只需要创建一个响应fit()transform()fit_transform()方法的类。在您的情况下,一个好的开始可能是这样的:

import pandas as pd
from sklearn.preprocessing import LabelEncoder
from sklearn.pipeline import Pipeline

# Create some toy data in a Pandas dataframe
fruit_data = pd.DataFrame({
    'fruit':  ['apple','orange','pear','orange'],
    'color':  ['red','orange','green','green'],
    'weight': [5,6,3,4]
})

class MultiColumnLabelEncoder:
    def __init__(self,columns = None):
        self.columns = columns # array of column names to encode

    def fit(self,X,y=None):
        return self # not relevant here

    def transform(self,X):
        '''
        Transforms columns of X specified in self.columns using
        LabelEncoder(). If no columns specified, transforms all
        columns in X.
        '''
        output = X.copy()
        if self.columns is not None:
            for col in self.columns:
                output[col] = LabelEncoder().fit_transform(output[col])
        else:
            for colname,col in output.iteritems():
                output[colname] = LabelEncoder().fit_transform(col)
        return output

    def fit_transform(self,X,y=None):
        return self.fit(X,y).transform(X)

假设我们想要编码两个分类属性(fruitcolor),同时保留数值属性weight不变。我们可以按照以下步骤进行:

MultiColumnLabelEncoder(columns = ['fruit','color']).fit_transform(fruit_data)

这会将我们的fruit_data数据集从

enter image description here转换为

enter image description here

如果传递一个完全由分类变量组成并省略columns参数的数据帧,将对每列进行编码(我认为这可能是您最初想要的):

MultiColumnLabelEncoder().fit_transform(fruit_data.drop('weight',axis=1))

这将把

enter image description here 转换为

enter image description here

请注意,当尝试编码已经是数字的属性时,它可能会出现错误(如果需要,请添加一些代码来处理此问题)。

另一个很好的特性是,我们可以在流水线中使用这个自定义转换器:

encoding_pipeline = Pipeline([
    ('encoding',MultiColumnLabelEncoder(columns=['fruit','color']))
    # add more pipeline steps as needed
])
encoding_pipeline.fit_transform(fruit_data)

2
刚刚意识到数据表明橙色是绿色的。哎呀。 ;) - PriceHardman
8
这是一种好的数据转换方法,但是如果我想将此转换应用于验证集,该怎么办?你需要再次进行拟合和转换,但可能会出现问题,例如我的新数据集中某些变量的类别不包括所有类别,比如说绿色在新数据集中没有出现就会影响编码。 - Ben
3
同意 @Ben 的观点。除了方法名称之外,这实际上并没有模仿 sklearn。如果你尝试将它放入 Pipeline 中,它不会起作用。 - TayTay
4
为确保标签编码在训练集和测试集中保持一致,您需要对整个数据集(训练集+测试集)进行编码。这可以在将它们分成训练集和测试集之前完成,或者您可以将它们合并,执行编码,然后再次分开。 - PriceHardman
3
反过来怎么样?解码回原始状态? - Areza
显示剩余8条评论

33
自scikit-learn 0.20版本以来,您可以使用sklearn.compose.ColumnTransformersklearn.preprocessing.OneHotEncoder
如果只有分类变量,直接使用OneHotEncoder
from sklearn.preprocessing import OneHotEncoder

OneHotEncoder(handle_unknown='ignore').fit_transform(df)

如果您具有异构类型的特征:

from sklearn.compose import make_column_transformer
from sklearn.preprocessing import RobustScaler
from sklearn.preprocessing import OneHotEncoder

categorical_columns = ['pets', 'owner', 'location']
numerical_columns = ['age', 'weigth', 'height']
column_trans = make_column_transformer(
    (categorical_columns, OneHotEncoder(handle_unknown='ignore'),
    (numerical_columns, RobustScaler())
column_trans.fit_transform(df)

文档中有更多选择: http://scikit-learn.org/stable/modules/compose.html#columntransformer-for-heterogeneous-data


inverse_transform()虽然在ColumnTransformer上不受支持,但目前还不支持:https://github.com/scikit-learn/scikit-learn/issues/11463。这对我的应用程序来说是一个很大的劣势,也可能对其他人有影响。 - Sander Vanden Hautte
1
虽然ColumnTransformer是一个很好的建议,但是这段代码无法运行(括号不平衡,column_transformer不再以那种方式工作)。 - Christabella Irwanto
我已经提出了一个编辑原始答案的建议,以修复代码。 - Christabella Irwanto

20

我们不需要使用LabelEncoder。

您可以将列转换为类别,然后获取它们的代码。我在下面使用了字典推导式来将此过程应用于每个列,并将结果包装回形状相同、索引和列名相同的数据框中。

>>> pd.DataFrame({col: df[col].astype('category').cat.codes for col in df}, index=df.index)
   location  owner  pets
0         1      1     0
1         0      2     1
2         0      0     0
3         1      1     2
4         1      3     1
5         0      2     1

为创建映射字典,您可以使用字典推导式列举类别:

>>> {col: {n: cat for n, cat in enumerate(df[col].astype('category').cat.categories)} 
     for col in df}

{'location': {0: 'New_York', 1: 'San_Diego'},
 'owner': {0: 'Brick', 1: 'Champ', 2: 'Ron', 3: 'Veronica'},
 'pets': {0: 'cat', 1: 'dog', 2: 'monkey'}}

3
如果我想要对一列进行反转(例如目标变量:Y),应该如何操作? - Ib D
不错,亚历山大!我非常喜欢这个!! - ASH

13

这并不直接回答你的问题(对此,Naputipulu Jon和PriceHardman有很棒的回答)

然而,针对一些分类任务等目的,你可以使用

pandas.get_dummies(input_df) 

此函数可用于处理包含类别数据的数据框,并将其转换为具有二元值的数据框。在结果数据框中,变量值会被编码成列名。 了解更多


12

虽然已经过去了一年半,但我也需要一次性对多个pandas数据框列进行.transform()操作(并且还能够进行.inverse_transform()操作)。这进一步扩展了@PriceHardman的出色建议:

class MultiColumnLabelEncoder(LabelEncoder):
    """
    Wraps sklearn LabelEncoder functionality for use on multiple columns of a
    pandas dataframe.

    """
    def __init__(self, columns=None):
        self.columns = columns

    def fit(self, dframe):
        """
        Fit label encoder to pandas columns.

        Access individual column classes via indexig `self.all_classes_`

        Access individual column encoders via indexing
        `self.all_encoders_`
        """
        # if columns are provided, iterate through and get `classes_`
        if self.columns is not None:
            # ndarray to hold LabelEncoder().classes_ for each
            # column; should match the shape of specified `columns`
            self.all_classes_ = np.ndarray(shape=self.columns.shape,
                                           dtype=object)
            self.all_encoders_ = np.ndarray(shape=self.columns.shape,
                                            dtype=object)
            for idx, column in enumerate(self.columns):
                # fit LabelEncoder to get `classes_` for the column
                le = LabelEncoder()
                le.fit(dframe.loc[:, column].values)
                # append the `classes_` to our ndarray container
                self.all_classes_[idx] = (column,
                                          np.array(le.classes_.tolist(),
                                                  dtype=object))
                # append this column's encoder
                self.all_encoders_[idx] = le
        else:
            # no columns specified; assume all are to be encoded
            self.columns = dframe.iloc[:, :].columns
            self.all_classes_ = np.ndarray(shape=self.columns.shape,
                                           dtype=object)
            for idx, column in enumerate(self.columns):
                le = LabelEncoder()
                le.fit(dframe.loc[:, column].values)
                self.all_classes_[idx] = (column,
                                          np.array(le.classes_.tolist(),
                                                  dtype=object))
                self.all_encoders_[idx] = le
        return self

    def fit_transform(self, dframe):
        """
        Fit label encoder and return encoded labels.

        Access individual column classes via indexing
        `self.all_classes_`

        Access individual column encoders via indexing
        `self.all_encoders_`

        Access individual column encoded labels via indexing
        `self.all_labels_`
        """
        # if columns are provided, iterate through and get `classes_`
        if self.columns is not None:
            # ndarray to hold LabelEncoder().classes_ for each
            # column; should match the shape of specified `columns`
            self.all_classes_ = np.ndarray(shape=self.columns.shape,
                                           dtype=object)
            self.all_encoders_ = np.ndarray(shape=self.columns.shape,
                                            dtype=object)
            self.all_labels_ = np.ndarray(shape=self.columns.shape,
                                          dtype=object)
            for idx, column in enumerate(self.columns):
                # instantiate LabelEncoder
                le = LabelEncoder()
                # fit and transform labels in the column
                dframe.loc[:, column] =\
                    le.fit_transform(dframe.loc[:, column].values)
                # append the `classes_` to our ndarray container
                self.all_classes_[idx] = (column,
                                          np.array(le.classes_.tolist(),
                                                  dtype=object))
                self.all_encoders_[idx] = le
                self.all_labels_[idx] = le
        else:
            # no columns specified; assume all are to be encoded
            self.columns = dframe.iloc[:, :].columns
            self.all_classes_ = np.ndarray(shape=self.columns.shape,
                                           dtype=object)
            for idx, column in enumerate(self.columns):
                le = LabelEncoder()
                dframe.loc[:, column] = le.fit_transform(
                        dframe.loc[:, column].values)
                self.all_classes_[idx] = (column,
                                          np.array(le.classes_.tolist(),
                                                  dtype=object))
                self.all_encoders_[idx] = le
        return dframe.loc[:, self.columns].values

    def transform(self, dframe):
        """
        Transform labels to normalized encoding.
        """
        if self.columns is not None:
            for idx, column in enumerate(self.columns):
                dframe.loc[:, column] = self.all_encoders_[
                    idx].transform(dframe.loc[:, column].values)
        else:
            self.columns = dframe.iloc[:, :].columns
            for idx, column in enumerate(self.columns):
                dframe.loc[:, column] = self.all_encoders_[idx]\
                    .transform(dframe.loc[:, column].values)
        return dframe.loc[:, self.columns].values

    def inverse_transform(self, dframe):
        """
        Transform labels back to original encoding.
        """
        if self.columns is not None:
            for idx, column in enumerate(self.columns):
                dframe.loc[:, column] = self.all_encoders_[idx]\
                    .inverse_transform(dframe.loc[:, column].values)
        else:
            self.columns = dframe.iloc[:, :].columns
            for idx, column in enumerate(self.columns):
                dframe.loc[:, column] = self.all_encoders_[idx]\
                    .inverse_transform(dframe.loc[:, column].values)
        return dframe.loc[:, self.columns].values

示例:

如果 dfdf_copy() 是混合类型的 pandas 数据帧,您可以按以下方式将 MultiColumnLabelEncoder() 应用于 dtype=object 列:

# get `object` columns
df_object_columns = df.iloc[:, :].select_dtypes(include=['object']).columns
df_copy_object_columns = df_copy.iloc[:, :].select_dtypes(include=['object']).columns

# instantiate `MultiColumnLabelEncoder`
mcle = MultiColumnLabelEncoder(columns=object_columns)

# fit to `df` data
mcle.fit(df)

# transform the `df` data
mcle.transform(df)

# returns output like below
array([[1, 0, 0, ..., 1, 1, 0],
       [0, 5, 1, ..., 1, 1, 2],
       [1, 1, 1, ..., 1, 1, 2],
       ..., 
       [3, 5, 1, ..., 1, 1, 2],

# transform `df_copy` data
mcle.transform(df_copy)

# returns output like below (assuming the respective columns 
# of `df_copy` contain the same unique values as that particular 
# column in `df`
array([[1, 0, 0, ..., 1, 1, 0],
       [0, 5, 1, ..., 1, 1, 2],
       [1, 1, 1, ..., 1, 1, 2],
       ..., 
       [3, 5, 1, ..., 1, 1, 2],

# inverse `df` data
mcle.inverse_transform(df)

# outputs data like below
array([['August', 'Friday', '2013', ..., 'N', 'N', 'CA'],
       ['April', 'Tuesday', '2014', ..., 'N', 'N', 'NJ'],
       ['August', 'Monday', '2014', ..., 'N', 'N', 'NJ'],
       ..., 
       ['February', 'Tuesday', '2014', ..., 'N', 'N', 'NJ'],
       ['April', 'Tuesday', '2014', ..., 'N', 'N', 'NJ'],
       ['March', 'Tuesday', '2013', ..., 'N', 'N', 'NJ']], dtype=object)

# inverse `df_copy` data
mcle.inverse_transform(df_copy)

# outputs data like below
array([['August', 'Friday', '2013', ..., 'N', 'N', 'CA'],
       ['April', 'Tuesday', '2014', ..., 'N', 'N', 'NJ'],
       ['August', 'Monday', '2014', ..., 'N', 'N', 'NJ'],
       ..., 
       ['February', 'Tuesday', '2014', ..., 'N', 'N', 'NJ'],
       ['April', 'Tuesday', '2014', ..., 'N', 'N', 'NJ'],
       ['March', 'Tuesday', '2013', ..., 'N', 'N', 'NJ']], dtype=object)

您可以通过索引访问用于拟合每个列的单独列类、列标签和列编码器:

mcle.all_classes_
mcle.all_encoders_
mcle.all_labels_


嗨,Jason,mcle.all_labels_似乎无法工作(Python 3.5,Conda 4.3.29,Sklearn 0.18.1,Pandas 0.20.1)。我得到的错误是:AttributeError:'MultiColumnLabelEncoder'对象没有属性'all_labels_'。 - Jason
@Jason 你好,抱歉今天才看到这个信息 :/ 但是如果我要猜的话,我会说你只是使用了上面的fit方法,直到你将其应用于数据(transform / fit_transform),它才会真正生成任何标签。 - Jason Wolosonovich
我认为你需要提供一个更好的例子 - 我无法重新运行你的所有代码。 - Areza

11

在pandas中直接进行这个操作是可行的,并且非常适合replace方法的独特功能。

首先,让我们创建一个字典,其中包含将列及其值映射到其新替换值的子字典。

transform_dict = {}
for col in df.columns:
    cats = pd.Categorical(df[col]).categories
    d = {}
    for i, cat in enumerate(cats):
        d[cat] = i
    transform_dict[col] = d

transform_dict
{'location': {'New_York': 0, 'San_Diego': 1},
 'owner': {'Brick': 0, 'Champ': 1, 'Ron': 2, 'Veronica': 3},
 'pets': {'cat': 0, 'dog': 1, 'monkey': 2}}

由于这将始终是一对一的映射,我们可以反转内部字典,以获取新值与原始值之间的映射关系。

inverse_transform_dict = {}
for col, d in transform_dict.items():
    inverse_transform_dict[col] = {v:k for k, v in d.items()}

inverse_transform_dict
{'location': {0: 'New_York', 1: 'San_Diego'},
 'owner': {0: 'Brick', 1: 'Champ', 2: 'Ron', 3: 'Veronica'},
 'pets': {0: 'cat', 1: 'dog', 2: 'monkey'}}

现在,我们可以使用replace方法的独特能力来获取嵌套的字典列表,并将外部键用作列,将内部键用作我们想要替换的值。

df.replace(transform_dict)
   location  owner  pets
0         1      1     0
1         0      2     1
2         0      0     0
3         1      1     2
4         1      3     1
5         0      2     1

我们可以通过再次链接 replace 方法,轻松地回到原始状态。

df.replace(transform_dict).replace(inverse_transform_dict)
    location     owner    pets
0  San_Diego     Champ     cat
1   New_York       Ron     dog
2   New_York     Brick     cat
3  San_Diego     Champ  monkey
4  San_Diego  Veronica     dog
5   New_York       Ron     dog

7

假设您只是想获取一个sklearn.preprocessing.LabelEncoder()对象,以用于表示您的列,那么您需要做的就是:

le.fit(df.columns)

在上面的代码中,每个列都有一个唯一的编号。更准确地说,您将拥有df.columnsle.transform(df.columns.get_values())之间的1:1映射关系。要获取列的编码,只需将其传递给le.transform(...)即可。例如,以下代码将获取每个列的编码:
le.transform(df.columns.get_values())

假设您想为所有行标签创建一个 sklearn.preprocessing.LabelEncoder() 对象,您可以执行以下操作:

le.fit([y for x in df.get_values() for y in x])

在这种情况下,您很可能有非唯一的行标签(如您的问题所示)。要查看编码器创建的类别,可以执行le.classes_。您会注意到,这应该与set(y for x in df.get_values() for y in x)中的元素相同。再次,要将行标签转换为编码标签,请使用le.transform(...)。例如,如果您想检索df.columns数组中的第一列和第一行的标签,可以执行以下操作:
le.transform([df.get_value(0, df.columns[0])])

您在评论中提出的问题有点复杂,但仍然可以完成:
le.fit([str(z) for z in set((x[0], y) for x in df.iteritems() for y in x[1])])

上述代码实现了以下功能:
  1. 创建所有(列,行)对的唯一组合。
  2. 将每个对表示为元组的字符串版本。这是一个解决方案,以克服LabelEncoder类不支持元组作为类名的问题。
  3. 将新项目适配到LabelEncoder中。
现在要使用这个新模型会有些复杂。假设我们想要提取与前面示例中查找的相同项的表示形式(df.columns的第一列和第一行),我们可以这样做:
le.transform([str((df.columns[0], df.get_value(0, df.columns[0])))])

记住,现在每次查找都是一个元组的字符串表示形式,其中包含(列,行)信息。

7
不,LabelEncoder 不会这样做。它接受类标签的一维数组并生成一维数组。它被设计用于处理分类问题中的类标签,而不是任意数据,任何试图将其强制用于其他用途都需要编写代码将实际问题转换为它解决的问题(以及将解决方案返回到原始空间)。

好的,鉴于此,你有什么建议可以让我以一次性编码整个 DataFrame 的字符串标签? - Bryan
@Bryan 看一下 LabelEncoder 的代码并进行调整。我自己不使用 Pandas,所以不知道这会有多难。 - Fred Foo
我会让其他的 pandas 专家也来解决这个问题 - 我相信我不是唯一一个面临这个挑战的人,所以我希望可能已经有预先构建好的解决方案。 - Bryan

6
这里是脚本。
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
col_list = df.select_dtypes(include = "object").columns
for colsn in col_list:
    df[colsn] = le.fit_transform(df[colsn].astype(str))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接