有 Pandas 列包含列表,如何将唯一列表元素转换为列?

Have Pandas column containing lists, how to pivot unique list elements to columns?

我写了一个网络抓取工具来从 table 的产品中提取信息并构建一个数据框。数据 table 有一个描述列,其中包含描述产品的逗号分隔的属性字符串。我想在数据框中为每个唯一属性创建一个列,并用该属性的子字符串填充该列中的行。下面的示例 df。

PRODUCTS     DATE        DESCRIPTION
Product A    2016-9-12   Steel, Red, High Hardness
Product B    2016-9-11   Blue, Lightweight, Steel
Product C    2016-9-12   Red

我想第一步是将描述拆分成一个列表。

In: df2 = df['DESCRIPTION'].str.split(',')

Out:
DESCRIPTION
['Steel', 'Red', 'High Hardness']
['Blue', 'Lightweight', 'Steel']
['Red']

我想要的输出类似于下面的 table。列名不是特别重要。

PRODUCTS     DATE        STEEL_COL  RED_COL    HIGH HARDNESS_COL  BLUE COL   LIGHTWEIGHT_COL
Product A    2016-9-12   Steel      Red        High Hardness
Product B    2016-9-11   Steel                                    Blue       Lightweight
Product C    2016-9-12              Red

我相信可以使用 Pivot 设置列,但我不确定在建立列后填充列的最 Pythonic 方式。感谢任何帮助。

更新

非常感谢您的回答。我选择@MaxU 的响应是正确的,因为它看起来稍微灵活一些,但@piRSquared 的结果非常相似,甚至可以被认为是更 Pythonic 的方法。我测试了两个版本,都做了我需要的。谢谢!

如果产品具有该功能,在功能列中放置 'X' 怎么样?

下面创建了一个独特的特征列表('Steel'、'Red' 等),然后为原始 df 中的每个特征创建一列。然后我们遍历每一行,对于每个产品特征,我们在单元格中放置一个 'X'。

ml = []  

a = [ml.append(item) for l in df.DESCRIPTION for item in l]

unique_list_of_attributes = list(set(ml)) # unique features list

# place empty columns in original df for each feature
df = pd.concat([df,pd.DataFrame(columns=unique_list_of_attributes)]).fillna(value='')

# add 'X' in column if product has feature
for row in df.iterrows():
    for attribute in row[1]['DESCRIPTION']:
        df.loc[row[0],attribute] = 'X'

更新了示例输出:

    PRODUCTS       DATE                 DESCRIPTION Blue HighHardness  \
0  Product A  2016-9-12  [Steel, Red, HighHardness]                 X   
1  Product B  2016-9-11  [Blue, Lightweight, Steel]    X                
2  Product C  2016-9-12                       [Red]                     

  Lightweight Red Steel  
0               X     X  
1           X         X  
2               X       

你可以建立一个稀疏矩阵:

In [27]: df
Out[27]:
    PRODUCTS       DATE                DESCRIPTION
0  Product A  2016-9-12  Steel, Red, High Hardness
1  Product B  2016-9-11   Blue, Lightweight, Steel
2  Product C  2016-9-12                        Red

In [28]: (df.set_index(['PRODUCTS','DATE'])
   ....:    .DESCRIPTION.str.split(',\s*', expand=True)
   ....:    .stack()
   ....:    .reset_index()
   ....:    .pivot_table(index=['PRODUCTS','DATE'], columns=0, fill_value=0, aggfunc='size')
   ....: )
Out[28]:
0                    Blue  High Hardness  Lightweight  Red  Steel
PRODUCTS  DATE
Product A 2016-9-12     0              1            0    1      1
Product B 2016-9-11     1              0            1    0      1
Product C 2016-9-12     0              0            0    1      0

In [29]: (df.set_index(['PRODUCTS','DATE'])
   ....:    .DESCRIPTION.str.split(',\s*', expand=True)
   ....:    .stack()
   ....:    .reset_index()
   ....:    .pivot_table(index=['PRODUCTS','DATE'], columns=0, fill_value='', aggfunc='size')
   ....: )
Out[29]:
0                   Blue High Hardness Lightweight Red Steel
PRODUCTS  DATE
Product A 2016-9-12                  1               1     1
Product B 2016-9-11    1                         1         1
Product C 2016-9-12                                  1

使用pd.get_dummies

cols = ['PRODUCTS', 'DATE']
pd.get_dummies(
    df.set_index(cols).DESCRIPTION \
      .str.split(',\s*', expand=True).stack()
).groupby(level=cols).sum().astype(int)

@piRSquared 和@MaxU 发布的答案非常有效。

但是, 仅当数据没有任何 NaN 值时。我使用的数据非常稀少。它有大约 100 万行,在应用上述方法后减少到只有大约 100 行,因为它删除了任何列中带有 NaN 的所有行。我花了一天多的时间来找出修复方法。分享稍微修改过的代码,以节省其他人的时间。

假设你有 df 如上所述的 DataFrame,

  • 首先将所有出现的 NaN 替换为任何其他列中不期望的内容,因为稍后您必须将其替换回 NaN

    cols = ['PRODUCTS', 'DATE']
    col = "DESCRIPTION"
    df.loc[:, cols] = df.loc[:, cols].fillna("SOME_UNIQ_NAN_REPLACEMENT")
    

    这是必需的,因为 groupby 会删除所有具有 NaN 值的行。 :/

  • 然后我们 运行 对其他答案中的建议稍作修改 stack(dropna=False)。默认情况下,dropna=True

    df = pd.get_dummies(df.set_index(index_columns[col]\
            .str.split(",\s*", expand=True).stack(dropna=False), prefix=col)\
            .groupby(index_columns, sort=False).sum().astype(int).reset_index()
    
  • 然后将 NaN 放回 df 以不更改其他列的数据。

    df.replace("SOME_UNIQ_NAN_REPLACEMENT", np.nan, inplace=True)
    

希望这可以为某人节省数小时的挫败感。

这是我从我已经在处理的问题扩展而来的解决方案。

def group_agg_pivot_df(df, group_cols, agg_func='count', agg_col=None):

    if agg_col is None:
        agg_col = group_cols[0]

    grouped = df.groupby(group_cols).agg({agg_col: agg_func}) \
        .unstack().fillna(0)
    # drop aggregation column name from hierarchical column names
    grouped.columns = grouped.columns.droplevel()

    # promote index to column (the first element of group_cols)
    pivot_df = grouped.reset_index()
    pivot_df.columns = [s.replace(' ', '_').lower() for s in pivot_df.columns]
    return pivot_df

def split_stack_df(df, id_cols, split_col, new_col_name):
    # id_cols are the columns we want to pair with the values
    # from the split column

    stacked = df.set_index(id_cols)[split_col].str.split(',', expand=True) \
        .stack().reset_index(level=id_cols)
    stacked.columns = id_cols + [new_col_name]
    return stacked

stacked = split_stack_df(df, ['PRODUCTS', 'DATE'], 'DESCRIPTION', 'desc')
final_df = group_agg_pivot_df(stacked, ['PRODUCTS', 'DATE', 'desc'])

我还在具有 11592 行的 pandas 数据框和包含具有 2681 个唯一值的列表的列上对 @MaxU、@piRSquared 和我的解决方案进行了基准测试。显然,测试数据框中的列名不同,但我将它们与问题中的列名保持一致。

以下是每种方法的基准

In [277]: %timeit pd.get_dummies(df.set_index(['PRODUCTS', 'DATE']) \
 ...:                        .DESCRIPTION.str.split(',', expand=True) \
 ...:                        .stack()) \
 ...:     .groupby(['PRODUCTS', 'DATE']).sum()
 ...: 

1 个循环,3 个循环中的最佳:每个循环 1.14 秒

In [278]: %timeit df.set_index(['PRODUCTS', 'DATE']) \
 ...:     .DESCRIPTION.str.split(',', expand=True) \
 ...:     .stack() \
 ...:     .reset_index() \
 ...:     .pivot_table(index=['PRODUCTS', 'DATE'], columns=0, fill_value=0, aggfunc='size')

1 个循环,3 个循环中的最佳:每个循环 612 毫秒

In [286]: %timeit stacked = split_stack_df(df, ['PRODUCTS', 'DATE'], 'DESCRIPTION', 'desc'); \
 ...:     final_df = group_agg_pivot_df(stacked, ['PRODUCTS', 'DATE', 'desc'])

1 个循环,3 个循环中的最佳:每个循环 62.7 毫秒

我的猜测是聚合和拆分比 pivot_table() 或 pd.get_dummies() 更快。