Pandas 将列表的一列转换为虚拟值
- 2025-02-13 08:36:00
- admin 原创
- 33
问题描述:
我有一个数据框,其中一列是每个用户所属的组的列表。例如:
index groups
0 ['a','b','c']
1 ['c']
2 ['b','c','e']
3 ['a','c']
4 ['b','e']
我想要做的是创建一系列虚拟列来识别每个用户属于哪些组,以便运行一些分析
index a b c d e
0 1 1 1 0 0
1 0 0 1 0 0
2 0 1 1 0 1
3 1 0 1 0 0
4 0 1 0 0 0
pd.get_dummies(df['groups'])
不会起作用,因为这只会为我的列中的每个不同列表返回一列。
解决方案需要高效,因为数据框将包含 500,000+ 行。
解决方案 1:
用于:s
df['groups']
In [21]: s = pd.Series({0: ['a', 'b', 'c'], 1:['c'], 2: ['b', 'c', 'e'], 3: ['a', 'c'], 4: ['b', 'e'] })
In [22]: s
Out[22]:
0 [a, b, c]
1 [c]
2 [b, c, e]
3 [a, c]
4 [b, e]
dtype: object
这是一个可能的解决方案:
In [23]: pd.get_dummies(s.explode()).groupby(level=0).sum()
Out[23]:
a b c e
0 1 1 1 0
1 0 0 1 0
2 0 1 1 1
3 1 0 1 0
4 0 1 0 1
其逻辑是:
.explode()
将一系列列表展平为一系列单个值(索引跟踪原始行号)pd.get_dummies( )
创建假人.groupby(level=0).sum()
用于将应该为一行的不同行合并起来(通过按索引分组求和level=0
(即原始行号))
我不知道这是否足够有效,但无论如何,如果性能很重要,那么将列表存储在数据框中并不是一个好主意。
自原始答案以来的更新
自 0.25 版起,
s.explode()
可用于展平列表系列,而不是原始的s.apply(pd.Series).stack()
自 1.3.0 版起,在聚合中使用 level 关键字已被弃用,并将很快从新版本中删除,因此建议使用
df.groupby(level=0).sum()
而不是df.sum(level=0)
解决方案 2:
如果你有大量的数据框,那么解决方案非常快
使用sklearn.preprocessing.MultiLabelBinarizer
import pandas as pd
from sklearn.preprocessing import MultiLabelBinarizer
df = pd.DataFrame(
{'groups':
[['a','b','c'],
['c'],
['b','c','e'],
['a','c'],
['b','e']]
}, columns=['groups'])
s = df['groups']
mlb = MultiLabelBinarizer()
pd.DataFrame(mlb.fit_transform(s),columns=mlb.classes_, index=df.index)
结果:
a b c e
0 1 1 1 0
1 0 0 1 0
2 0 1 1 1
3 1 0 1 0
4 0 1 0 1
对我有用,也有人建议在这里和这里
解决方案 3:
这甚至更快:pd.get_dummies(df['groups'].explode()).sum(level=0)
使用.explode()
而不是.apply(pd.Series).stack()
与其他解决方案相比:
import timeit
import pandas as pd
setup = '''
import time
import pandas as pd
s = pd.Series({0:['a','b','c'],1:['c'],2:['b','c','e'],3:['a','c'],4:['b','e']})
df = s.rename('groups').to_frame()
'''
m1 = "pd.get_dummies(s.apply(pd.Series).stack()).sum(level=0)"
m2 = "df.groups.apply(lambda x: pd.Series([1] * len(x), index=x)).fillna(0, downcast='infer')"
m3 = "pd.get_dummies(df['groups'].explode()).sum(level=0)"
times = {f"m{i+1}":min(timeit.Timer(m, setup=setup).repeat(7, 1000)) for i, m in enumerate([m1, m2, m3])}
pd.DataFrame([times],index=['ms'])
# m1 m2 m3
# ms 5.586517 3.821662 2.547167
解决方案 4:
虽然这个任务已经得到解答,但我有一个更快的解决方案:
df.groups.apply(lambda x: pd.Series([1] * len(x), index=x)).fillna(0, downcast='infer')
而且,如果您有空组或NaN
,您可以:
df.loc[df.groups.str.len() > 0].apply(lambda x: pd.Series([1] * len(x), index=x)).fillna(0, downcast='infer')
工作原理
x
例如,在 lambda 内部是你的列表['a', 'b', 'c']
。因此pd.Series
将如下所示:
In [2]: pd.Series([1, 1, 1], index=['a', 'b', 'c'])
Out[2]:
a 1
b 1
c 1
dtype: int64
当一切都pd.Series
结合在一起时,它们就变成了pd.DataFrame
,并且它们的index
变成了columns
;缺失的index
变成了,column
正如NaN
您接下来所看到的:
In [4]: a = pd.Series([1, 1, 1], index=['a', 'b', 'c'])
In [5]: b = pd.Series([1, 1, 1], index=['a', 'b', 'd'])
In [6]: pd.DataFrame([a, b])
Out[6]:
a b c d
0 1.0 1.0 1.0 NaN
1 1.0 1.0 NaN 1.0
现在用fillna
以下方式填充:NaN
`0`
In [7]: pd.DataFrame([a, b]).fillna(0)
Out[7]:
a b c d
0 1.0 1.0 1.0 0.0
1 1.0 1.0 0.0 1.0
并且downcast='infer'
是从到float
向下int
:
In [11]: pd.DataFrame([a, b]).fillna(0, downcast='infer')
Out[11]:
a b c d
0 1 1 1 0
1 1 1 0 1
附言: 不需要使用.fillna(0, downcast='infer')
。
解决方案 5:
您可以使用str.join
将列表中所有元素串联成字符串,然后使用str.get_dummies
:
out = df.join(df['groups'].str.join('|').str.get_dummies())
print(out)
groups a b c e
0 [a, b, c] 1 1 1 0
1 [c] 0 0 1 0
2 [b, c, e] 0 1 1 1
3 [a, c] 1 0 1 0
4 [b, e] 0 1 0 1
解决方案 6:
您可以使用explode
和crosstab
:
s = pd.Series([['a', 'b', 'c'], ['c'], ['b', 'c', 'e'], ['a', 'c'], ['b', 'e']])
s = s.explode()
pd.crosstab(s.index, s)
输出:
col_0 a b c e
row_0
0 1 1 1 0
1 0 0 1 0
2 0 1 1 1
3 1 0 1 0
4 0 1 0 1