在scikit-learn中对多个列进行标签编码

325 投票
25 回答
383448 浏览
提问于 2025-04-18 11:22

我正在尝试使用 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)

错误追踪(最近的调用最后): 文件 "", 第 1 行, 在 文件 "/Users/bbalin/anaconda/lib/python2.7/site-packages/sklearn/preprocessing/label.py", 第 103 行, 在 fit y = column_or_1d(y, warn=True) 文件 "/Users/bbalin/anaconda/lib/python2.7/site-packages/sklearn/utils/validation.py", 第 306 行, 在 column_or_1d raise ValueError("输入形状不正确 {0}".format(shape)) ValueError: 输入形状不正确 (6, 3)

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

25 个回答

13

这已经是事情发生一年半后的讨论了,不过我也需要能够一次性对多个 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_

21

我们不需要使用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'}}
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

131

正如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)
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实例的解决方案。

撰写回答