この投稿の内容は元々 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
この結果を計算する最もパフォーマンスの高い方法は何ですか?
ベンチマークを確立することから始めましょう。これを解決する最も簡単な方法は、一時的な「キー」列を使用することです。
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)
免責事項
これらのソリューションは、非混合スカラー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でこれらのソリューションのベンチマークを行い、
タイミングは、セットアップ、データ、および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()
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