让我们从建立一个基准开始。解决此问题的最简单方法是使用临时“键”列:
熊猫
def cartesian_product_basic(left, right):
return (
left.assign(key=1).merge(right.assign(key=1), on='key').drop('key', 1))
cartesian_product_basic(left, right)
熊猫 >= 1.2
left.merge(right, how="cross") # implements the technique above
col1_x col2_x col1_y col2_y
0 A 1 X 20
1 A 1 Y 30
2 A 1 Z 50
3 B 2 X 20
4 B 2 Y 30
5 B 2 Z 50
6 C 3 X 20
7 C 3 Y 30
8 C 3 Z 50
其工作原理是为两个 DataFrame 分配一个具有相同值(例如 1)的临时“键”列。merge
然后对“key”执行多对多 JOIN。
虽然多对多 JOIN 技巧适用于大小合理的 DataFrame,但您会发现较大数据的性能相对较低。
更快的实现需要 NumPy。这里有一些著名的一维笛卡尔积的 NumPy 实现。我们可以在其中一些高性能解决方案的基础上构建以获得我们想要的输出。然而,我最喜欢的是 @senderle 的第一个实现。
def cartesian_product(*arrays):
la = len(arrays)
dtype = np.result_type(*arrays)
arr = np.empty([len(a) for a in arrays] + [la], dtype=dtype)
for i, a in enumerate(np.ix_(*arrays)):
arr[...,i] = a
return arr.reshape(-1, la)
泛化:Unique 上的 CROSS JOINor非唯一索引数据帧
免责声明
这些解决方案针对具有非混合标量数据类型的 DataFrame 进行了优化。如果处理混合数据类型,请在您的位置使用
个人风险!
这个技巧适用于任何类型的 DataFrame。我们使用上述方法计算 DataFrame 数值索引的笛卡尔积cartesian_product
,用它来重新索引 DataFrames,并且
def cartesian_product_generalized(left, right):
la, lb = len(left), len(right)
idx = cartesian_product(np.ogrid[:la], np.ogrid[:lb])
return pd.DataFrame(
np.column_stack([left.values[idx[:,0]], right.values[idx[:,1]]]))
cartesian_product_generalized(left, right)
0 1 2 3
0 A 1 X 20
1 A 1 Y 30
2 A 1 Z 50
3 B 2 X 20
4 B 2 Y 30
5 B 2 Z 50
6 C 3 X 20
7 C 3 Y 30
8 C 3 Z 50
np.array_equal(cartesian_product_generalized(left, right),
cartesian_product_basic(left, right))
True
并且,沿着类似的思路,
left2 = left.copy()
left2.index = ['s1', 's2', 's1']
right2 = right.copy()
right2.index = ['x', 'y', 'y']
left2
col1 col2
s1 A 1
s2 B 2
s1 C 3
right2
col1 col2
x X 20
y Y 30
y Z 50
np.array_equal(cartesian_product_generalized(left, right),
cartesian_product_basic(left2, right2))
True
该解决方案可以推广到多个 DataFrame。例如,
def cartesian_product_multi(*dfs):
idx = cartesian_product(*[np.ogrid[:len(df)] for df in dfs])
return pd.DataFrame(
np.column_stack([df.values[idx[:,i]] for i,df in enumerate(dfs)]))
cartesian_product_multi(*[left, right, left]).head()
0 1 2 3 4 5
0 A 1 X 20 A 1
1 A 1 X 20 B 2
2 A 1 X 20 C 3
3 A 1 X 20 D 4
4 A 1 Y 30 A 1
进一步简化
一个不涉及@senderle的更简单的解决方案cartesian_product
处理时有可能just two数据框。使用np.broadcast_arrays
,我们可以达到几乎相同水平的性能。
def cartesian_product_simplified(left, right):
la, lb = len(left), len(right)
ia2, ib2 = np.broadcast_arrays(*np.ogrid[:la,:lb])
return pd.DataFrame(
np.column_stack([left.values[ia2.ravel()], right.values[ib2.ravel()]]))
np.array_equal(cartesian_product_simplified(left, right),
cartesian_product_basic(left2, right2))
True
性能比较
在一些具有独特索引的人为 DataFrame 上对这些解决方案进行基准测试,我们有
![enter image description here](https://i.stack.imgur.com/S92LX.png)
请注意,时间可能会根据您的设置、数据和选择而有所不同cartesian_product
适用的辅助函数。
性能基准代码
这是计时脚本。这里调用的所有函数都在上面定义。
from timeit import timeit
import pandas as pd
import matplotlib.pyplot as plt
res = pd.DataFrame(
index=['cartesian_product_basic', 'cartesian_product_generalized',
'cartesian_product_multi', 'cartesian_product_simplified'],
columns=[1, 10, 50, 100, 200, 300, 400, 500, 600, 800, 1000, 2000],
dtype=float
)
for f in res.index:
for c in res.columns:
# print(f,c)
left2 = pd.concat([left] * c, ignore_index=True)
right2 = pd.concat([right] * c, ignore_index=True)
stmt = '{}(left2, right2)'.format(f)
setp = 'from __main__ import left2, right2, {}'.format(f)
res.at[f, c] = timeit(stmt, setp, number=5)
ax = res.div(res.min()).T.plot(loglog=True)
ax.set_xlabel("N");
ax.set_ylabel("time (relative)");
plt.show()
继续阅读
跳转到 Pandas Merging 101 中的其他主题继续学习:
-
合并基础知识 - 连接的基本类型
-
基于索引的连接
-
泛化到多个 DataFrame
-
Cross join *
* you are here