web-dev-qa-db-fra.com

Produit cartésien performant (CROSS JOIN) avec pandas

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?

26
cs95

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)  

Généraliser: CROSS JOIN sur Unique ou Non-Unique Indexed DataFrames

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

Simplification supplémentaire

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

Comparaison de performance

En comparant ces solutions à des DataFrames élaborés avec des index uniques, nous avons

enter image description here

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()
34
cs95

Utilisation de itertoolsproduct 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
7
WeNYoBen

Voici une approche avec triple 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