在 pandas DataFrame 中将列表分成多列

3

我有一个源系统,给我提供这样的数据:

Name    |Hobbies
----------------------------------
"Han"   |"Art;Soccer;Writing"
"Leia"  |"Art;Baking;Golf;Singing"
"Luke"  |"Baking;Writing"

每个爱好列表都以分号分隔。我希望将其转换为类似表格的结构,每个爱好占据一列,并标记该人是否选择了该爱好:

Name    |Art     |Baking  |Golf    |Singing |Soccer  |Writing  
--------------------------------------------------------------
"Han"   |1       |0       |0       |0       |1       |1
"Leia"  |1       |1       |1       |1       |0       |0
"Luke"  |0       |1       |0       |0       |0       |1

以下是生成 pandas dataframe 中示例数据的代码:

>>> import pandas as pd
>>> df = pd.DataFrame(
...     [
...         {'name': 'Han',   'hobbies': 'Art;Soccer;Writing'},
...         {'name': 'Leia',  'hobbies': 'Art;Baking;Golf;Singing'},
...         {'name': 'Luke',  'hobbies': 'Baking;Writing'},
...     ]
... )
>>> df
                   hobbies  name
0       Art;Soccer;Writing   Han
1  Art;Baking;Golf;Singing  Leia
2           Baking;Writing  Luke

目前,我正在使用以下代码将数据获取到一个DataFrame中,它具有所需的结构,但是速度非常(实际数据集大约有150万行):

>>> df2 = pd.DataFrame(columns=['name', 'hobby'])
>>>
>>> for index, row in df.iterrows():
...     for value in str(row['hobbies']).split(';'):
...         d = {'name':row['name'], 'value':value}
...         df2 = df2.append(d, ignore_index=True)
...
>>> df2 = df2.groupby('name')['value'].value_counts()
>>> df2 = df2.unstack(level=-1).fillna(0)
>>>
>>> df2
value  Art  Baking  Golf  Singing  Soccer  Writing
name
Han    1.0     0.0   0.0      0.0     1.0      1.0
Leia   1.0     1.0   1.0      1.0     0.0      0.0
Luke   0.0     1.0   0.0      0.0     0.0      1.0

有没有更有效的方式来完成这个任务?

你知道所有可能的兴趣爱好吗?如果不知道,将兴趣爱好作为单独的一列可能更有效,然后对于每个兴趣爱好,为该角色添加一行(因此Han会有三行,每行一个兴趣爱好,包括艺术、足球和写作)。 - RagingRoosevelt
很遗憾,不行。源系统允许插入(通过代码和数据加载)实际下拉列表中没有的值。 - Andy
3个回答

3

为什么不直接在DataFrame中进行更改呢?

for idx, row in df.iterrows():
    for hobby in row.hobbies.split(";"):
        df.loc[idx, hobby] = True

df.fillna(False, inplace=True)

谢谢!这大大提高了性能。我在一个5000行的样本集上进行了测试,你的“原地”代码比之前快了十倍。 - Andy

2
你可以尝试不在每个迭代中添加列,而是在循环运行后一次性添加所有列:
df3 = pd.DataFrame(columns=['name', 'hobby'])
d_list = []

for index, row in df.iterrows():
    for value in str(row['hobbies']).split(';'):
        d_list.append({'name':row['name'], 
                       'value':value})
df3 = df3.append(d_list, ignore_index=True)
df3 = df3.groupby('name')['value'].value_counts()
df3 = df3.unstack(level=-1).fillna(0)
df3

我查看了您的示例数据框需要多长时间。使用我提出的改进方案,速度将会提升 ~50倍。


1
在我的测试中,它看起来比我最初的方法快了近70倍。太棒了! - Andy
经过仔细检查,虽然它确实更快,但实际上它无法工作:它不能创建和添加列。 - Andy
应该可以工作。我更新了我的答案,包含了所有应该产生结果的代码,除了“df”的定义。请再次检查。 - Georgy
抱歉,我一直在开会,没有机会检查更新。测试5000个数据需要大约500毫秒。我之前写的代码需要近30秒,所以你的代码速度大约快了60倍。我的实际生产数据集有2416664行,所以(基于500毫秒5,000个数据)这应该在四分钟左右完成。我用timeit启动了它,但如果需要运行三次才能给我最佳时间,那么它完成之前还需要一点时间。稍后我会发布另一个更新。 - Andy
1
因此,使用完整数据集的三个最佳结果为3分37秒(仅针对循环,而不是附加、分组和取消堆叠),这太棒了。我的代码看起来只需要不到四个小时!据我所知,差异仅在于何时以及如何将新列的数据添加到新数据集中。通过将数据放入字典列表中,然后将其附加到数据框中,您大大提高了性能!谢谢。 :) - Andy

1

实际上,使用.str.split.melt应该比使用iterrows循环更快一些。

  1. Splitting to multiple columns:

    >>> df = pd.DataFrame([{'name': 'Han', 'hobbies': 'Art;Soccer;Writing'}, 
                           {'name': 'Leia', 'hobbies': 'Art;Baking;Golf;Singing'},
                           {'name': 'Luke', 'hobbies': 'Baking;Writing'}])
    >>> hobbies = df['hobbies'].str.split(';', expand=True)
    >>> hobbies
        0          1       2       3
    0 Art     Soccer Writing    None
    1 Art     Baking    Golf Singing
    2 Baking Writing    None    None 
    
  2. Unpivoting hobbies by names:

    >>> df = df.drop('hobbies', axis=1)
    >>> df = df.join(hobbies)
    >>> stacked = df.melt('name', value_name='hobby').drop('variable', axis=1)
    >>> stacked
       name   hobby
     0  Han     Art
     1 Leia     Art
     2 Luke  Baking
     3  Han  Soccer
     4 Leia  Baking
     5 Luke Writing
     6  Han Writing
     7 Leia    Golf
     8 Luke    None
     9  Han    None
    10 Leia Singing
    11 Luke    None
    
  3. Counting the values:

    >>> counts = stacked.groupby('name')['hobby'].value_counts()
    >>> result = counts.unstack(level=-1).fillna(0).astype(int)
    >>> result
    hobby Art Baking Golf Singing Soccer Writing
    name                        
     Han    1      0    0       0      1       1
    Leia    1      1    1       1      0       0
    Luke    0      1    0       0      0       1
    

针对第 2 步和第 3 步,还有替代方案可以使用,如使用 get_dummiescrosstab,这些方案在此文中有所讨论:Pandas get_dummies on multiple columns,但第一个方案会消耗大量内存,而第二个方案则要慢得多。


参考资料:
如何使用Pandas将一列拆分成多个列
如何使用Pandas将多个列的值堆叠到单个列中


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