【问题标题】:Performant cartesian product (CROSS JOIN) with pandaspandas 的高性能笛卡尔积(CROSS JOIN)
【发布时间】:2019-05-10 23:02:49
【问题描述】:

这篇文章的内容最初是为了成为 Pandas Merging 101, 但由于内容的性质和大小需要完全做 为了这个话题,它已被转移到自己的 QnA 中。

给定两个简单的 DataFrame;

left = pd.DataFrame({'col1' : ['A', 'B', 'C'], 'col2' : [1, 2, 3]})
right = pd.DataFrame({'col1' : ['X', 'Y', 'Z'], 'col2' : [20, 30, 50]})

left

  col1  col2
0    A     1
1    B     2
2    C     3

right

  col1  col2
0    X    20
1    Y    30
2    Z    50

这些帧的叉积可以计算出来,看起来像:

A       1      X      20
A       1      Y      30
A       1      Z      50
B       2      X      20
B       2      Y      30
B       2      Z      50
C       3      X      20
C       3      Y      30
C       3      Z      50

计算此结果的最高效方法是什么?

【问题讨论】:

标签: python pandas numpy dataframe merge


【解决方案1】:

产品 = (

df1.assign(key=1)
.merge(df2.assign(key=1), on="key")
.drop("key", axis=1)

)

【讨论】:

  • 这个解决方案已经讨论过了。
  • 但这是唯一对我有用的
【解决方案2】:

我认为最简单的方法是向每个数据框添加一个虚拟列,对其进行内部合并,然后从生成的笛卡尔数据框中删除该虚拟列:

left['dummy'] = 'a'
right['dummy'] = 'a'

cartesian = left.merge(right, how='inner', on='dummy')

del cartesian['dummy']

【讨论】:

  • 这已经在接受的答案中讨论过了。但是现在left.merge(right, how="cross") 已经这样做了,不需要第二个专栏。
  • 不知何故交叉对我不起作用。可能是版本问题。
【解决方案3】:

pandas 1.2.0 之后 merge 现在有选项 cross

left.merge(right, how='cross')

使用itertools product 并在数据框中重新创建值

import itertools
l=list(itertools.product(left.values.tolist(),right.values.tolist()))
pd.DataFrame(list(map(lambda x : sum(x,[]),l)))
   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

【讨论】:

    【解决方案4】:

    让我们从建立一个基准开始。解决此问题的最简单方法是使用临时“关键”列:

    # pandas <= 1.1.X
    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)
    
    # pandas >= 1.2 (est)
    left.merge(right, how="cross")
    
      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 implementations of 1D cartesian product。我们可以在其中一些高性能解决方案的基础上获得我们想要的输出。然而,我最喜欢的是@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)  
    

    泛化:唯一非唯一索引数据帧上的交叉连接

    免责声明
    这些解决方案针对具有非混合标量 dtype 的 DataFrame 进行了优化。如果处理混合数据类型,请在您的 风险自负!

    这个技巧适用于任何类型的 DataFrame。我们使用上述cartesian_product 计算 DataFrame 的数字索引的笛卡尔积,使用它来重新索引 DataFrame,并且

    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
    

    进一步简化

    在处理两个 DataFrame 时,可以使用不涉及@senderle 的cartesian_product 的更简单的解决方案。使用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 上对这些解决方案进行基准测试,我们有

    请注意,时间可能会根据您的设置、数据和选择的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 中的其他主题以继续学习:

    *你在这里

    【讨论】:

    • 为什么列名变成整数了?当我尝试重命名它们时,.rename() 运行,但整数仍然存在。
    • @CameronTaylor 您是否忘记使用 axis=1 参数调用重命名?
    • 不......更密集 - 我在整数周围加上引号 - 谢谢
    • 另一个问题。我正在使用 Cartesian_product_simplified,当我尝试将 50K 行 df 连接到 30K 行 df 时,我(可以预见地)内存不足。关于克服内存问题的任何提示?
    • @CameronTaylor 其他 cartesian_product_* 函数是否也会引发内存错误?我猜你可以在这里使用cartesian_product_multi。
    【解决方案5】:

    这是一个三元组concat的方法

    m = pd.concat([pd.concat([left]*len(right)).sort_index().reset_index(drop=True),
           pd.concat([right]*len(left)).reset_index(drop=True) ], 1)
    
        col1  col2 col1  col2
    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
    

    【讨论】:

      猜你喜欢
      • 2018-06-23
      • 2012-10-27
      • 2018-09-16
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多