8wDlpd.png
8wDFp9.png
8wDEOx.png
8wDMfH.png
8wDKte.png

使用 Pandas 进行高性能笛卡尔积 (CROSS JOIN)

pavinan 1月前

25 0

这篇文章的内容原本是作为 Pandas Merging 101 的一部分,但由于需要充分阐述这一主题所需的内容的性质和大小,它已被移至...

的一部分 Pandas Merging 101 ,但由于需要充分阐述这一主题所需的内容的性质和大小,它已被移至其自己的问答中。

给定两个简单的 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

计算这个结果的最有效方法是什么?

帖子版权声明 1、本帖标题:使用 Pandas 进行高性能笛卡尔积 (CROSS JOIN)
    本站网址:http://xjnalaquan.com/
2、本网站的资源部分来源于网络,如有侵权,请联系站长进行删除处理。
3、会员发帖仅代表会员个人观点,并不代表本站赞同其观点和对其真实性负责。
4、本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
5、站长邮箱:yeweds@126.com 除非注明,本帖由pavinan在本站《dataframe》版块原创发布, 转载请注明出处!
最新回复 (0)
  • 让我们先建立一个基准。解决这个问题最简单的方法是使用临时的“关键”列:

    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

    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)的临时\'key\'​​ 列。 merge 然后对\'key\'​​ 执行多对多 JOIN。

    虽然多对多 JOIN 技巧适用于合理大小的 DataFrames,但您会看到较大数据的性能相对较低。

    更快的实现需要 NumPy。以下是一些著名 的 1D 笛卡尔积 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)  
    

    概括:在唯一 或非 唯一索引 DataFrames

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

    此技巧适用于任何类型的 DataFrame。我们使用前面提到的计算 DataFrames 数字索引的笛卡尔积 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
    

    进一步简化

    当仅处理 cartesian_product 两个 不涉及 @senderle 的更简单的解决方案 。使用 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
    

    性能比较

    在一些具有唯一索引的 DataFrames 上对这些解决方案进行基准测试,我们得到

    enter image description here

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

    • 第15页

    • p16

    • p17

    • p18

    * 你在这里

  • 为什么列名会变成整数?当我尝试重命名它们时,.rename() 会运行,但整数会保留。

  • 另一个问题。我正在使用 cartesian_product_simplified,当我尝试将 50K 行 df 连接到 30K 行 df 时,内存不足(这是可以预见的)。有什么解决内存问题的技巧吗?

  • @CameronTaylor 其他 cartesian_product_* 函数是否也会引发内存错误?我猜您可以在这里使用 cartesian_product_multi。

  • tpdi 1月前 0 只看Ta
    引用 6

    pandas 1.2.0 合并后现在有选项 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
    
  • 以下是三重方法 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
    

    enter image description here

  • 一个选项是使用 expand_grid pyjanitor :

    # pip install pyjanitor
    import pandas as pd
    import janitor as jn
    
    others = {'left':left, 'right':right}
    
    jn.expand_grid(others = others)
    
      left      right
      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
    
  • 对于大型数据集来说,这种方法是最好且最有效的方法。

    使用:pd.factorize + np.repeat + np.tile + np.hstack

    import pandas as pd
    import numpy as np
    
    
    df1 = pd.DataFrame({'col1': ['A', 'B', 'C'], 'col2': [1, 2, 3]})
    df2 = pd.DataFrame({'col1': ['X', 'Y', 'Z'], 'col2': [20, 30, 50]})
    print(df1)
    print(df2)
    df1['col1_codes'],col1_unique_df1 = pd.factorize(df1['col1'])
    df2['col1_codes'],col1_unique_df2 = pd.factorize(df2['col1'])
    df1_repeated = np.repeat(df1[['col1_codes','col2']].values,df2.shape[0],axis=0)
    print(df1_repeated)
    df2_tiled  =  np.tile(df2[['col1_codes','col2']].values, (df1.shape[0],1))
    print(df2_tiled)
    # Combine results
    nphstack = np.hstack([df1_repeated,df2_tiled])
    print(nphstack)
    res = pd.DataFrame(nphstack,columns = ['col1_codes_df1','col2_df1','col1_codes_df2','col2_df2'])
    print(res)
    """
      col1_codes_df1  col2_df1  col1_codes_df2  col2_df2
    0               0         1               0        20
    1               0         1               1        30
    2               0         1               2        50
    3               1         2               0        20
    4               1         2               1        30
    5               1         2               2        50
    6               2         3               0        20
    7               2         3               1        30
    8               2         3               2        50
    """
    # Map the codes back to the original strings
    
    res['col1_df1'] = res['col1_codes_df1'].map(dict(enumerate(col1_unique_df1)))
    res['col1_df2'] = res['col1_codes_df2'].map(dict(enumerate(col1_unique_df2)))
    print(res)
    """
      col1_codes_df1  col2_df1  col1_codes_df2   col2_df2   col1_df1    col1_df2
    0               0         1               0        20        A        X
    1               0         1               1        30        A        Y
    2               0         1               2        50        A        Z
    3               1         2               0        20        B        X
    4               1         2               1        30        B        Y
    5               1         2               2        50        B        Z
    6               2         3               0        20        C        X
    7               2         3               1        30        C        Y
    8               2         3               2        50        C        Z
    """
    # Drop the code columns if not needed
    res = res.drop(columns=['col1_codes_df1', 'col1_codes_df2'])
    
    print(res)
    """
       col2_df1  col2_df2 col1_df1 col1_df2
    0         1        20        A        X
    1         1        30        A        Y
    2         1        50        A        Z
    3         2        20        B        X
    4         2        30        B        Y
    5         2        50        B        Z
    6         3        20        C        X
    7         3        30        C        Y
    8         3        50        C        Z
    """
    
  • 引用 10

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

    left['dummy'] = 'a'
    right['dummy'] = 'a'
    
    cartesian = left.merge(right, how='inner', on='dummy')
    
    del cartesian['dummy']
    
  • 这在已接受的答案中已经讨论过了。但现在 left.merge(right, how=\'cross\') 已经做到了这一点,而不需要第二列。

返回
作者最近主题: