计算两列中任一列中字符串出现次数的矢量化方法
Posted
技术标签:
【中文标题】计算两列中任一列中字符串出现次数的矢量化方法【英文标题】:Vectorized way to count occurrences of string in either of two columns 【发布时间】:2018-08-30 23:55:34 【问题描述】:我有一个与this question 类似的问题相似,但只是不同而已,无法用相同的解决方案解决...
我有两个数据框,df1
和 df2
,如下所示:
import pandas as pd
import numpy as np
np.random.seed(42)
names = ['jack', 'jill', 'jane', 'joe', 'ben', 'beatrice']
df1 = pd.DataFrame('ID_a':np.random.choice(names, 20), 'ID_b':np.random.choice(names,20))
df2 = pd.DataFrame('ID':names)
>>> df1
ID_a ID_b
0 joe ben
1 ben jack
2 jane joe
3 ben jill
4 ben beatrice
5 jill ben
6 jane joe
7 jane jack
8 jane jack
9 ben jane
10 joe jane
11 jane jill
12 beatrice joe
13 ben joe
14 jill beatrice
15 joe beatrice
16 beatrice beatrice
17 beatrice jane
18 jill joe
19 joe joe
>>> df2
ID
0 jack
1 jill
2 jane
3 joe
4 ben
5 beatrice
我想做的是在df2
中添加一列,其中 count 行在df1
中可以在 either 中找到给定的名称strong> 列ID_a
或ID_b
,结果如下:
>>> df2
ID count
0 jack 3
1 jill 5
2 jane 8
3 joe 9
4 ben 7
5 beatrice 6
这个循环得到了我需要的东西,但对于大型数据帧效率低下,如果有人能提出一个替代的、更好的解决方案,我将非常感激:
df2['count'] = 0
for idx,row in df2.iterrows():
df2.loc[idx, 'count'] = len(df1[(df1.ID_a == row.ID) | (df1.ID_b == row.ID)])
提前致谢!
【问题讨论】:
我添加了更多选项。对@jpp 的时间持保留态度,当您对少数行的解决方案进行基准测试时,时间真的毫无意义。您可能想在更大的数据帧上尝试这些解决方案,然后您会真正看到不同之处。 我注意到了,我真的很感激。我的实际数据框显然比我发布的要大得多,但不是巨大,所以为了优雅,我可以承受一点效率的损失。然而,我原来的解决方案似乎既低效和不优雅,这就是为什么我想要一些输入... 【参考方案1】:“任一”部分使事情复杂化,但应该仍然可行。
选项 1 由于其他用户决定把它变成一场速度竞赛,这是我的:
from collections import Counter
from itertools import chain
c = Counter(chain.from_iterable(set(x) for x in df1.values.tolist()))
df2['count'] = df2['ID'].map(Counter(c))
df2
ID count
0 jack 3
1 jill 5
2 jane 8
3 joe 9
4 ben 7
5 beatrice 6
176 µs ± 7.69 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
选项 2
(原答案)stack
基于
c = df1.stack().groupby(level=0).value_counts().count(level=1)
或者,
c = df1.stack().reset_index(level=0).drop_duplicates()[0].value_counts()
或者,
v = df1.stack()
c = v.groupby([v.index.get_level_values(0), v]).count().count(level=1)
# c = v.groupby([v.index.get_level_values(0), v]).nunique().count(level=1)
还有,
df2['count'] = df2.ID.map(c)
df2
ID count
0 jack 3
1 jill 5
2 jane 8
3 joe 9
4 ben 7
5 beatrice 6
选项 3基于repeat
的重塑和计数
v = pd.DataFrame(
'i' : df1.values.reshape(-1, ),
'j' : df1.index.repeat(2)
)
c = v.loc[~v.duplicated(), 'i'].value_counts()
df2['count'] = df2.ID.map(c)
df2
ID count
0 jack 3
1 jill 5
2 jane 8
3 joe 9
4 ben 7
5 beatrice 6
选项 4concat
+ mask
v = pd.concat(
[df1.ID_a, df1.ID_b.mask(df1.ID_a == df1.ID_b)], axis=0
).value_counts()
df2['count'] = df2.ID.map(v)
df2
ID count
0 jack 3
1 jill 5
2 jane 8
3 joe 9
4 ben 7
5 beatrice 6
【讨论】:
感谢所有答案!我不得不说,就可读性而言,我实际上最喜欢您的原始答案。可能不是最快的,但就像我说的那样,df
的大小适中,我正在使用它,它可以很好地完成工作。
@sacul,我要补充一点,所有 cᴏʟᴅsᴘᴇᴇᴅ 的答案(以及您的整个工作流程)都可以通过使用分类数据得到改善。这是我们在第一轮答案中都错过的东西。顺便说一句,这也说明了为什么在一两天内不接受此类问题通常是个好主意。
@jpp,也感谢您的所有回答,我非常感谢,如果可以选择接受两个答案,我也会接受您的。我必须阅读categorical data,这是我真的不熟悉的 dtype!【参考方案2】:
以下是基于numpy
数组的几种方法。下面进行基准测试。
重要提示:对这些结果持保留态度。请记住,性能取决于您的数据、环境和硬件。在您的选择中,您还应该考虑可读性/适应性。
分类数据:jp2
中分类数据的卓越性能(即通过内部类似字典的结构将字符串分解为整数)是依赖于数据的,但如果它有效,它应该是适用的在以下所有算法中具有良好的性能和内存优势。
import pandas as pd
import numpy as np
from itertools import chain
from collections import Counter
# Tested on python 3.6.2 / pandas 0.20.3 / numpy 1.13.1
%timeit original(df1, df2) # 48.4 ms per loop
%timeit jp1(df1, df2) # 5.82 ms per loop
%timeit jp2(df1, df2) # 2.20 ms per loop
%timeit brad(df1, df2) # 7.83 ms per loop
%timeit cs1(df1, df2) # 12.5 ms per loop
%timeit cs2(df1, df2) # 17.4 ms per loop
%timeit cs3(df1, df2) # 15.7 ms per loop
%timeit cs4(df1, df2) # 10.7 ms per loop
%timeit wen1(df1, df2) # 19.7 ms per loop
%timeit wen2(df1, df2) # 32.8 ms per loop
def original(df1, df2):
for idx,row in df2.iterrows():
df2.loc[idx, 'count'] = len(df1[(df1.ID_a == row.ID) | (df1.ID_b == row.ID)])
return df2
def jp1(df1, df2):
for idx, item in enumerate(df2['ID']):
df2.iat[idx, 1] = np.sum((df1.ID_a.values == item) | (df1.ID_b.values == item))
return df2
def jp2(df1, df2):
df2['ID'] = df2['ID'].astype('category')
df1['ID_a'] = df1['ID_a'].astype('category')
df1['ID_b'] = df1['ID_b'].astype('category')
for idx, item in enumerate(df2['ID']):
df2.iat[idx, 1] = np.sum((df1.ID_a.values == item) | (df1.ID_b.values == item))
return df2
def brad(df1, df2):
names1, names2 = df1.values.T
v2 = df2.ID.values
mask1 = v2 == names1[:, None]
mask2 = v2 == names2[:, None]
df2['count'] = np.logical_or(mask1, mask2).sum(axis=0)
return df2
def cs1(df1, df2):
c = Counter(chain.from_iterable(set(x) for x in df1.values.tolist()))
df2['count'] = df2['ID'].map(Counter(c))
return df2
def cs2(df1, df2):
v = df1.stack().groupby(level=0).value_counts().count(level=1)
df2['count'] = df2.ID.map(v)
return df2
def cs3(df1, df2):
v = pd.DataFrame(
'i' : df1.values.reshape(-1, ),
'j' : df1.index.repeat(2)
)
c = v.loc[~v.duplicated(), 'i'].value_counts()
df2['count'] = df2.ID.map(c)
return df2
def cs4(df1, df2):
v = pd.concat(
[df1.ID_a, df1.ID_b.mask(df1.ID_a == df1.ID_b)], axis=0
).value_counts()
df2['count'] = df2.ID.map(v)
return df2
def wen1(df1, df2):
return pd.get_dummies(df1, prefix='', prefix_sep='').sum(level=0,axis=1).gt(0).sum().loc[df2.ID]
def wen2(df1, df2):
return pd.Series(Counter(list(chain(*list(map(set,df1.values)))))).loc[df2.ID]
设置
import pandas as pd
import numpy as np
np.random.seed(42)
names = ['jack', 'jill', 'jane', 'joe', 'ben', 'beatrice']
df1 = pd.DataFrame('ID_a':np.random.choice(names, 10000), 'ID_b':np.random.choice(names, 10000))
df2 = pd.DataFrame('ID':names)
df2['count'] = 0
【讨论】:
谢谢!我喜欢这些选项,而且我对时代感到惊讶!我原以为.stack().groupby...
方法会最快......
它并不像问题所问的那样完全“矢量化”,但仍然是一个答案。 @sacul 可悲的是,堆栈比它需要的慢得多。我一直希望这些人能够在未来的版本中优化该代码。它非常有用,但速度不够快。有时优雅的单衬是要付出代价的。
@cᴏʟᴅsᴘᴇᴇᴅ,完全同意 - 矢量化解决方案应该更快。但目前即使是简单的任务也存在较大的相对开销。另一个微不足道的是 pd.Series.replace
和 dict
- pandas 不遗余力地消除任何性能优势。
您是否介意我的解决方案的测试时间?谢谢
由于您一直在跟踪时间安排,能否请您更新一下我添加的所有 3 个新选项?【参考方案3】:
通过使用get_dummies
pd.get_dummies(df1, prefix='', prefix_sep='').sum(level=0,axis=1).gt(0).sum().loc[df2.ID]
Out[614]:
jack 3
jill 5
jane 8
joe 9
ben 7
beatrice 6
dtype: int64
我认为这应该很快......
from itertools import chain
from collections import Counter
pd.Series(Counter(list(chain(*list(map(set,df1.values)))))).loc[df2.ID]
【讨论】:
【参考方案4】:这是一个解决方案,您可以通过从df2
扩展ID
的维度以利用NumPy 广播,从而有效地执行嵌套的“in”循环:
>>> def count_names(df1, df2):
... names1, names2 = df1.values.T
... v2 = df2.ID.values[:, None]
... mask1 = v2 == names1
... mask2 = v2 == names2
... df2['count'] = np.logical_or(mask1, mask2).sum(axis=1)
... return df2
>>> %timeit -r 5 -n 1000 count_names(df1, df2)
144 µs ± 10.4 µs per loop (mean ± std. dev. of 5 runs, 1000 loops each)
>>> %timeit -r 5 -n 1000 jp(df1, df2)
224 µs ± 15.5 µs per loop (mean ± std. dev. of 5 runs, 1000 loops each)
>>> %timeit -r 5 -n 1000 cs(df1, df2)
238 µs ± 2.37 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
>>> %timeit -r 5 -n 1000 wen(df1, df2)
921 µs ± 15.3 µs per loop (mean ± std. dev. of 5 runs, 1000 loops each)
面具的形状将是(len(df1), len(df2))
。
【讨论】:
这不是速度赛跑。可悲的是,一位用户自己承担了责任。 更新了一些对于大框架应该具有相当可扩展性的东西@cᴏʟᴅsᴘᴇᴇᴅ “发布你能想到的最性感的解决方案”? :D @jpp 有趣的是,选项 1 是我鄙视的选项。它不漂亮,也不优雅。就是这样。如果你问我,我仍然坚持选项 2,因为这确实是我首先想到的。这很直观。还有另一个涉及 factorize 和 bincount 的 piResque 选项,我后来删除了(我的意思是,4 个选项已经过大了),但我也很喜欢那个。以上是关于计算两列中任一列中字符串出现次数的矢量化方法的主要内容,如果未能解决你的问题,请参考以下文章