有效地比较两列中的列表

时间:2020-01-08 19:49:07

标签: python pandas numpy dataframe

具有这样的Pandas DataFrame时:

import pandas as pd
import numpy as np
df = pd.DataFrame({'today': [['a', 'b', 'c'], ['a', 'b'], ['b']], 
                   'yesterday': [['a', 'b'], ['a'], ['a']]})
                 today        yesterday
0      ['a', 'b', 'c']       ['a', 'b']
1           ['a', 'b']            ['a']
2                ['b']            ['a']                          
... etc

但是有大约10万个条目,我希望逐行在两列中找到这些列表的添加和删除。

它可以与以下问题相提并论:Pandas: How to Compare Columns of Lists Row-wise in a DataFrame with Pandas (not for loop)?,但我正在研究差异,而Pandas.apply方法对于这么多条目似乎并不那么快。 这是我当前正在使用的代码。 Pandas.applynumpy's setdiff1d方法:

additions = df.apply(lambda row: np.setdiff1d(row.today, row.yesterday), axis=1)
removals  = df.apply(lambda row: np.setdiff1d(row.yesterday, row.today), axis=1)

这正常工作,但是大约需要一分钟才能完成12万个条目。那么有没有更快的方法来完成此任务?

5 个答案:

答案 0 :(得分:15)

不确定性能,但是在缺少更好的解决方案的情况下,这可能适用:

temp = df[['today', 'yesterday']].applymap(set)
removals = temp.diff(periods=1, axis=1).dropna(axis=1)
additions = temp.diff(periods=-1, axis=1).dropna(axis=1) 

删除:

  yesterday
0        {}
1        {}
2       {a}

添加项:

  today
0   {c}
1   {b}
2   {b}

答案 1 :(得分:7)

df['today'].apply(set) - df['yesterday'].apply(set)

答案 2 :(得分:5)

我建议您在相同的适用范围内计算additionsremovals

产生更大的例子

import pandas as pd
import numpy as np
df = pd.DataFrame({'today': [['a', 'b', 'c'], ['a', 'b'], ['b']], 
                   'yesterday': [['a', 'b'], ['a'], ['a']]})
df = pd.concat([df for i in range(10_000)], ignore_index=True)

您的解决方案

%%time
additions = df.apply(lambda row: np.setdiff1d(row.today, row.yesterday), axis=1)
removals  = df.apply(lambda row: np.setdiff1d(row.yesterday, row.today), axis=1)
CPU times: user 10.9 s, sys: 29.8 ms, total: 11 s
Wall time: 11 s

您的解决方案一次申请

%%time
df["out"] = df.apply(lambda row: [np.setdiff1d(row.today, row.yesterday),
                                  np.setdiff1d(row.yesterday, row.today)], axis=1)
df[['additions','removals']] = pd.DataFrame(df['out'].values.tolist(), 
                                            columns=['additions','removals'])
df = df.drop("out", axis=1)

CPU times: user 4.97 s, sys: 16 ms, total: 4.99 s
Wall time: 4.99 s

使用set

除非您的列表很大,否则您可以避免使用numpy

def fun(x):
    a = list(set(x["today"]).difference(set(x["yesterday"])))
    b = list((set(x["yesterday"])).difference(set(x["today"])))
    return [a,b]

%%time
df["out"] = df.apply(fun, axis=1)
df[['additions','removals']] = pd.DataFrame(df['out'].values.tolist(), 
                                            columns=['additions','removals'])
df = df.drop("out", axis=1)

CPU times: user 1.56 s, sys: 0 ns, total: 1.56 s
Wall time: 1.56 s

@ r.ook的解决方案

如果您乐于使用集而不是列表作为输出,则可以使用@ r.ook的代码

%%time
temp = df[['today', 'yesterday']].applymap(set)
removals = temp.diff(periods=1, axis=1).dropna(axis=1)
additions = temp.diff(periods=-1, axis=1).dropna(axis=1) 
CPU times: user 93.1 ms, sys: 12 ms, total: 105 ms
Wall time: 104 ms

@Andreas K.的解决方案

%%time
df['additions'] = (df['today'].apply(set) - df['yesterday'].apply(set))
df['removals'] = (df['yesterday'].apply(set) - df['today'].apply(set))

CPU times: user 161 ms, sys: 28.1 ms, total: 189 ms
Wall time: 187 ms

,您最终可以添加.apply(list)以获得相同的输出

答案 3 :(得分:1)

这里是将计算部件卸载到矢量化NumPy工具的想法。我们将为每个标头将所有数据收集到单个数组中,对NumPy执行所有必需的匹配,最后切回到所需的行条目。在承担繁重任务的NumPy上,我们将使用基于np.searchsorted的组ID和每个组中的ID的哈希。我们也利用数字,因为使用NumPy可以更快。实现看起来像这样-

t = df['today']
y = df['yesterday']
tc = np.concatenate(t)
yc = np.concatenate(y)

tci,tcu = pd.factorize(tc)

tl = np.array(list(map(len,t)))
ty = np.array(list(map(len,y)))

grp_t = np.repeat(np.arange(len(tl)),tl)
grp_y = np.repeat(np.arange(len(ty)),ty)

sidx = tcu.argsort()
idx = sidx[np.searchsorted(tcu,yc,sorter=sidx)]

s = max(tci.max(), idx.max())+1
tID = grp_t*s+tci
yID = grp_y*s+idx

t_mask = np.isin(tID, yID, invert=True)
y_mask = np.isin(yID, tID, invert=True)

t_se = np.r_[0,np.bincount(grp_t,t_mask).astype(int).cumsum()]
y_se = np.r_[0,np.bincount(grp_y,y_mask).astype(int).cumsum()]

Y = yc[y_mask].tolist()
T = tc[t_mask].tolist()

A = pd.Series([T[i:j] for (i,j) in zip(t_se[:-1],t_se[1:])])
R = pd.Series([Y[i:j] for (i,j) in zip(y_se[:-1],y_se[1:])])

在计算t_masky_mask的步骤中可以进行进一步的优化,其中np.searchsorted可以再次使用。

我们还可以使用简单的数组分配作为isin步骤的替代方案,以获取t_masky_mask,就像这样-

M = max(tID.max(), yID.max())+1
mask = np.empty(M, dtype=bool)

mask[tID] = True
mask[yID] = False
t_mask = mask[tID]

mask[yID] = True
mask[tID] = False
y_mask = mask[yID]

答案 4 :(得分:1)

您的解决方案

%timeit additions = df.apply(lambda row: np.setdiff1d(row.today, row.yesterday), axis=1)

每个循环590 µs±13 µs(平均±标准偏差,共运行7次,每个循环1000次)

%timeit removals  = df.apply(lambda row: np.setdiff1d(row.yesterday, row.today), axis=1)

每个循环609 µs±28 µs(平均±标准偏差,共运行7次,每个循环1000次)

使用地图功能或numpy向量化以获得更好的性能,在某些情况下地图功能会失败。

使用numpy向量化功能

vector = np.vectorize(lambda x,y:set(x)-set(y))
%timeit additions = vector(df.today,df.yesterday)

每个循环56.6 µs±256 ns(平均±标准偏差,共运行7次,每个循环10000个

%timeit removals = vector(df.yesterday,df.today)

每个循环58.1 µs±2.04 µs(平均±标准偏差,共运行7次,每个10000个循环)

使用地图功能

首先设置要转换的列表

df.today = list(map(set,df.today))
df.yesterday = list(map(set,df.yesterday))

然后使用lambda和map函数

%timeit additions = list(map(lambda x:x[0]-x[1],zip(df.today,df.yesterday)))

每个循环15.3 µs±1.63 µs(平均±标准偏差,共运行7次,每个循环100000次)

%timeit removals = list(map(lambda x:x[1]-x[0],zip(df.today,df.yesterday)))

每个循环15.1 µs±502 ns(平均±标准偏差,共运行7次,每个循环100000次)

因此可以使用map函数或np.vectorize函数

相关问题