Le contenu de ce message devait à l'origine faire partie de Pandas Merging 101 , mais en raison de la nature et de la taille du contenu requis pour rendre justice à ce sujet, il a été déplacé vers son propre QnA.
Étant donné deux DataFrames simples;
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
Le produit croisé de ces cadres peut être calculé et ressemblera à quelque chose comme:
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
Quelle est la méthode la plus performante pour calculer ce résultat?
Commençons par établir un repère. La méthode la plus simple pour résoudre ce problème consiste à utiliser une colonne temporaire "clé":
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
Comment cela fonctionne-t-il, c'est qu'une colonne "clé" temporaire avec la même valeur est affectée aux deux DataFrames (par exemple, 1). merge
effectue ensuite une jointure plusieurs sur plusieurs sur "clé".
Bien que l'astuce JOIN de plusieurs en plusieurs fonctionne pour des DataFrames de taille raisonnable, vous constaterez des performances relativement inférieures sur des données plus volumineuses.
Une mise en œuvre plus rapide nécessitera NumPy. Voici quelques exemples célèbres implémentations NumPy du produit cartésien 1D . Nous pouvons nous appuyer sur certaines de ces solutions performantes pour obtenir le résultat souhaité. Mon préféré, cependant, est la première implémentation de @ 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)
Clause de non-responsabilité
Ces solutions sont optimisées pour les DataFrames avec des types scalaires non mélangés. Si vous utilisez des types mixtes, utilisez-le à vos risques et périls!
Cette astuce fonctionnera sur tout type de DataFrame. Nous calculons le produit cartésien des index numériques des DataFrames en utilisant le cartesian_product
, utilisez-le pour réindexer les DataFrames, et
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
Et, dans le même sens,
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
Cette solution peut généraliser à plusieurs DataFrames. Par exemple,
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
Une solution plus simple n'impliquant pas @ senderle's cartesian_product
est possible avec seulement deux DataFrames. En utilisant np.broadcast_arrays
, nous pouvons atteindre presque le même niveau de performance.
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
En comparant ces solutions à des DataFrames élaborés avec des index uniques, nous avons
Notez que les horaires peuvent varier en fonction de votre configuration, de vos données et du choix de cartesian_product
fonction d'assistance, le cas échéant.
Code d'analyse comparative des performances
Ceci est le script de synchronisation. Toutes les fonctions appelées ici sont définies ci-dessus.
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()
Utilisation de itertools
product
et recréer la valeur dans le cadre de données
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