web-dev-qa-db-ja.com

pandasを使用したパフォーマンスデカルト積(CROSS JOIN)

この投稿の内容は元々 Pandas Merging 101 の一部であることが意図されていましたが、このトピックを完全に正当化するために必要なコンテンツの性質とサイズのため、独自の場所に移動されましたQnA。

2つの単純な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

この結果を計算する最もパフォーマンスの高い方法は何ですか?

26
cs95

ベンチマークを確立することから始めましょう。これを解決する最も簡単な方法は、一時的な「キー」列を使用することです。

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)

  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は、「キー」に対して多対多の結合を実行します。

多対多のJOINトリックは、適度なサイズのDataFrameで機能しますが、大きなデータでは比較的パフォーマンスが低下します。

より高速な実装には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)  

一般化:一意のまたは非一意のインデックス付きデータフレームでのCROSS JOIN

免責事項
これらのソリューションは、非混合スカラーdtypeを持つDataFrame向けに最適化されています。混合型を扱う場合は、自己責任で使用してください!

このトリックは、あらゆる種類のDataFrameで機能します。前述のcartesian_productを使用してDataFramesの数値インデックスのデカルト積を計算し、これを使用して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を含まないより単純なソリューションは、ちょうど2つのDataFrameを扱う場合に可能です。 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

タイミングは、セットアップ、データ、および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()
34
cs95

itertoolsproductを使用して、データフレームに値を再作成します

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
7
WeNYoBen

トリプル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

4
Bharath M