【问题标题】:How to one-hot-encode from a pandas column containing a list?如何从包含列表的熊猫列中进行一次热编码?
【发布时间】:2023-03-05 16:55:01
【问题描述】:

我想将由元素列表组成的 pandas 列分解为与唯一元素一样多的列,即 one-hot-encode 它们(值 1 表示存在于一行中的给定元素,0在缺席的情况下)。

例如取数据帧df

Col1   Col2         Col3
 C      33     [Apple, Orange, Banana]
 A      2.5    [Apple, Grape]
 B      42     [Banana] 

我想把它转换成:

df

Col1   Col2   Apple   Orange   Banana   Grape
 C      33     1        1        1       0
 A      2.5    1        0        0       1
 B      42     0        0        1       0

如何使用 pandas/sklearn 来实现这一点?

【问题讨论】:

    标签: python pandas numpy scikit-learn sklearn-pandas


    【解决方案1】:

    使用get_dummies:

    df_out = df.assign(**pd.get_dummies(df.Col3.apply(lambda x:pd.Series(x)).stack().reset_index(level=1,drop=True)).sum(level=0))
    

    输出:

      Col1  Col2                     Col3  Apple  Banana  Grape  Orange
    0    C  33.0  [Apple, Orange, Banana]      1       1      0       1
    1    A   2.5           [Apple, Grape]      1       0      1       0
    2    B  42.0                 [Banana]      0       1      0       0
    

    清理列:

    df_out.drop('Col3',axis=1)
    

    输出:

      Col1  Col2  Apple  Banana  Grape  Orange
    0    C  33.0      1       1      0       1
    1    A   2.5      1       0      1       0
    2    B  42.0      0       1      0       0
    

    【讨论】:

    • +1 用于 **get_dummies,但由于 .stack() 和方法链接,这对于大型数据帧可能会很慢。
    • @BradSolomon 谢谢。
    • 我不确定这是否真的有效...之后尝试一下:df = pd.concat([df, df])
    【解决方案2】:

    您可以循环使用Col3apply 并将每个元素转换为以列表为索引的系列,从而成为结果数据框中的标题:

    pd.concat([
            df.drop("Col3", 1),
            df.Col3.apply(lambda x: pd.Series(1, x)).fillna(0)
        ], axis=1)
    
    #Col1   Col2    Apple   Banana  Grape   Orange
    #0  C   33.0      1.0      1.0    0.0     1.0
    #1  A    2.5      1.0      0.0    1.0     0.0
    #2  B   42.0      0.0      1.0    0.0     0.0
    

    【讨论】:

      【解决方案3】:

      您可以使用集合推导在Col3 中获取所有独特的水果,如下所示:

      set(fruit for fruits in df.Col3 for fruit in fruits)
      

      使用字典推导,您可以浏览每个独特的水果,看看它是否在列中。

      >>> df[['Col1', 'Col2']].assign(**{fruit: [1 if fruit in cell else 0 for cell in df.Col3] 
                                         for fruit in set(fruit for fruits in df.Col3 
                                                          for fruit in fruits)})
        Col1  Col2  Apple  Banana  Grape  Orange
      0    C  33.0      1       1      0       1
      1    A   2.5      1       0      1       0
      2    B  42.0      0       1      0       0
      

      时间

      dfs = pd.concat([df] * 1000)  # Use 3,000 rows in the dataframe.
      
      # Solution 1 by @Alexander (me)
      %%timeit -n 1000 
      dfs[['Col1', 'Col2']].assign(**{fruit: [1 if fruit in cell else 0 for cell in dfs.Col3] 
                                      for fruit in set(fruit for fruits in dfs.Col3 for fruit in fruits)})
      # 10 loops, best of 3: 4.57 ms per loop
      
      # Solution 2 by @Psidom
      %%timeit -n 1000
      pd.concat([
              dfs.drop("Col3", 1),
              dfs.Col3.apply(lambda x: pd.Series(1, x)).fillna(0)
          ], axis=1)
      # 10 loops, best of 3: 748 ms per loop
      
      # Solution 3 by @MaxU
      from sklearn.preprocessing import MultiLabelBinarizer
      mlb = MultiLabelBinarizer()
      
      %%timeit -n 10 
      dfs.join(pd.DataFrame(mlb.fit_transform(dfs.Col3),
                                columns=mlb.classes_,
                                index=dfs.index))
      # 10 loops, best of 3: 283 ms per loop
      
      # Solution 4 by @ScottBoston
      %%timeit -n 10
      df_out = dfs.assign(**pd.get_dummies(dfs.Col3.apply(lambda x:pd.Series(x)).stack().reset_index(level=1,drop=True)).sum(level=0))
      # 10 loops, best of 3: 512 ms per loop
      
      But...
      >>> print(df_out.head())
        Col1  Col2                     Col3  Apple  Banana  Grape  Orange
      0    C  33.0  [Apple, Orange, Banana]   1000    1000      0    1000
      1    A   2.5           [Apple, Grape]   1000       0   1000       0
      2    B  42.0                 [Banana]      0    1000      0       0
      0    C  33.0  [Apple, Orange, Banana]   1000    1000      0    1000
      1    A   2.5           [Apple, Grape]   1000       0   1000       0
      

      【讨论】:

        【解决方案4】:

        我们也可以使用sklearn.preprocessing.MultiLabelBinarizer:

        为了节省大量内存,我们通常希望对现实世界的数据使用 sparse DataFrame。

        稀疏解决方案(适用于 Pandas v0.25.0+)

        from sklearn.preprocessing import MultiLabelBinarizer
        
        mlb = MultiLabelBinarizer(sparse_output=True)
        
        df = df.join(
                    pd.DataFrame.sparse.from_spmatrix(
                        mlb.fit_transform(df.pop('Col3')),
                        index=df.index,
                        columns=mlb.classes_))
        

        结果:

        In [38]: df
        Out[38]:
          Col1  Col2  Apple  Banana  Grape  Orange
        0    C  33.0      1       1      0       1
        1    A   2.5      1       0      1       0
        2    B  42.0      0       1      0       0
        
        In [39]: df.dtypes
        Out[39]:
        Col1                object
        Col2               float64
        Apple     Sparse[int32, 0]
        Banana    Sparse[int32, 0]
        Grape     Sparse[int32, 0]
        Orange    Sparse[int32, 0]
        dtype: object
        
        In [40]: df.memory_usage()
        Out[40]:
        Index     128
        Col1       24
        Col2       24
        Apple      16    #  <--- NOTE!
        Banana     16    #  <--- NOTE!
        Grape       8    #  <--- NOTE!
        Orange      8    #  <--- NOTE!
        dtype: int64
        

        密集解决方案

        mlb = MultiLabelBinarizer()
        df = df.join(pd.DataFrame(mlb.fit_transform(df.pop('Col3')),
                                  columns=mlb.classes_,
                                  index=df.index))
        

        结果:

        In [77]: df
        Out[77]:
          Col1  Col2  Apple  Banana  Grape  Orange
        0    C  33.0      1       1      0       1
        1    A   2.5      1       0      1       0
        2    B  42.0      0       1      0       0
        

        【讨论】:

        • 你可能会觉得时间安排很有趣。
        • 这似乎非常消耗内存。我的 160 GiB 机器内存不足,有 1000000 行和 30000 列。
        • @DawidLaszuk,尝试使用MultiLabelBinarizer(sparse_output=True)
        • @MaxU 是的,我的错,问题不在于 MLB,而在于 pandas 本身(或者更有可能是我对它的使用)。为了进行测试,可能需要找到一种方法来丢弃 100 个最常见值之外的条目。
        • @DawidLaszuk,我认为打开一个新问题是有意义的,提供一个小的可重复样本数据集和您想要的数据集......
        【解决方案5】:

        选项 1
        简答
        pir_slow

        df.drop('Col3', 1).join(df.Col3.str.join('|').str.get_dummies())
        
          Col1  Col2  Apple  Banana  Grape  Orange
        0    C  33.0      1       1      0       1
        1    A   2.5      1       0      1       0
        2    B  42.0      0       1      0       0
        

        选项 2
        快速答复
        pir_fast

        v = df.Col3.values
        l = [len(x) for x in v.tolist()]
        f, u = pd.factorize(np.concatenate(v))
        n, m = len(v), u.size
        i = np.arange(n).repeat(l)
        
        dummies = pd.DataFrame(
            np.bincount(i * m + f, minlength=n * m).reshape(n, m),
            df.index, u
        )
        
        df.drop('Col3', 1).join(dummies)
        
          Col1  Col2  Apple  Orange  Banana  Grape
        0    C  33.0      1       1       1      0
        1    A   2.5      1       0       0      1
        2    B  42.0      0       0       1      0
        

        选项 3
        pir_alt1

        df.drop('Col3', 1).join(
            pd.get_dummies(
                pd.DataFrame(df.Col3.tolist()).stack()
            ).astype(int).sum(level=0)
        )
        
          Col1  Col2  Apple  Orange  Banana  Grape
        0    C  33.0      1       1       1      0
        1    A   2.5      1       0       0      1
        2    B  42.0      0       0       1      0
        

        计时结果
        代码如下


        def maxu(df):
            mlb = MultiLabelBinarizer()
            d = pd.DataFrame(
                mlb.fit_transform(df.Col3.values)
                , df.index, mlb.classes_
            )
            return df.drop('Col3', 1).join(d)
        
        
        def bos(df):
            return df.drop('Col3', 1).assign(**pd.get_dummies(df.Col3.apply(lambda x:pd.Series(x)).stack().reset_index(level=1,drop=True)).sum(level=0))
        
        def psi(df):
            return pd.concat([
                df.drop("Col3", 1),
                df.Col3.apply(lambda x: pd.Series(1, x)).fillna(0)
            ], axis=1)
        
        def alex(df):
            return df[['Col1', 'Col2']].assign(**{fruit: [1 if fruit in cell else 0 for cell in df.Col3] 
                                               for fruit in set(fruit for fruits in df.Col3 
                                                                for fruit in fruits)})
        
        def pir_slow(df):
            return df.drop('Col3', 1).join(df.Col3.str.join('|').str.get_dummies())
        
        def pir_alt1(df):
            return df.drop('Col3', 1).join(pd.get_dummies(pd.DataFrame(df.Col3.tolist()).stack()).astype(int).sum(level=0))
        
        def pir_fast(df):
            v = df.Col3.values
            l = [len(x) for x in v.tolist()]
            f, u = pd.factorize(np.concatenate(v))
            n, m = len(v), u.size
            i = np.arange(n).repeat(l)
        
            dummies = pd.DataFrame(
                np.bincount(i * m + f, minlength=n * m).reshape(n, m),
                df.index, u
            )
        
            return df.drop('Col3', 1).join(dummies)
        
        results = pd.DataFrame(
            index=(1, 3, 10, 30, 100, 300, 1000, 3000),
            columns='maxu bos psi alex pir_slow pir_fast pir_alt1'.split()
        )
        
        for i in results.index:
            d = pd.concat([df] * i, ignore_index=True)
            for j in results.columns:
                stmt = '{}(d)'.format(j)
                setp = 'from __main__ import d, {}'.format(j)
                results.set_value(i, j, timeit(stmt, setp, number=10))
        

        【讨论】:

        • 太棒了,真的! PS 我刚刚使用了我今天的最后一次投票;-)
        • 真快!就像你的时间表一样。我假设 x-axis 是数据框中的行数?
        • @Alexander thx,x 轴是 df 的倍数... 懒得贴标签了。所以 1000 是 pd.concat([df] * 1000, ignore_index=True)
        • 刚刚在您的代码中看到了这一点。感谢您的澄清。
        • @Alexander 我是一个坚持匹配输出以使苹果与苹果相匹配的人。
        【解决方案6】:

        您可以使用函数explode(0.25.0 版中的新功能。)和crosstab

        s = df['Col3'].explode()
        df[['Col1', 'Col2']].join(pd.crosstab(s.index, s))
        

        输出:

          Col1  Col2  Apple  Banana  Grape  Orange
        0    C  33.0      1       1      0       1
        1    A   2.5      1       0      1       0
        2    B  42.0      0       1      0       0
        

        【讨论】:

        • 这个答案应该更受欢迎......感谢这个巧妙的解决方案!
        • 我的一些行有空列表,应用上述代码后,新列得到 NaN 值。有什么办法可以将 Nan 设置为 0?
        • 这是迄今为止最干净的答案,但我无法解开 df。它不是特别大。
        • @harshpoddar 你可以使用fillna(0)
        • 感谢您的精彩解决方案! df1 似乎是 pd.Series,而不是 pd.DataFrame。只是想让它听到,以防df1这个名字让任何人感到困惑。
        猜你喜欢
        • 2020-09-30
        • 2019-02-10
        • 2023-03-31
        • 2017-12-02
        • 2018-05-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多