Producto cartesiano performante (CROSS JOIN) con pandas.

El contenido de esta publicación originalmente estaba destinado a ser parte de Pandas Merging 101 , pero debido a la naturaleza y el tamaño del contenido requerido para hacer justicia a este tema, se ha trasladado a su propia QnA.

Dados dos cuadros de datos 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 

El producto cruzado de estos marcos se puede calcular, y se verá algo así como:

 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 

¿Cuál es el método más eficaz para calcular este resultado?

Empecemos por establecer un punto de referencia. El método más fácil para resolver esto es usar una columna “clave” temporal:

 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 

Cómo funciona esto es que a ambos DataFrames se les asigna una columna “clave” temporal con el mismo valor (por ejemplo, 1). luego merge realiza una merge muchos a muchos en “clave”.

Si bien el truco JOIN de muchos a muchos funciona para DataFrames de tamaño razonable, verá un rendimiento relativamente menor en datos más grandes.

Una implementación más rápida requerirá NumPy. Aquí hay algunas aplicaciones NumPy famosas de producto cartesiano 1D . Podemos aprovechar algunas de estas soluciones de rendimiento para obtener el resultado deseado. Mi favorito, sin embargo, es la primera implementación 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) 

Generalización: CROSS JOIN en marcos de datos indexados únicos o no únicos

Renuncia
Estas soluciones están optimizadas para DataFrames con tipos de datos escalares no mixtos. Si se trata de tipos de letras mixtas, úselo bajo su propio riesgo.

Este truco funcionará en cualquier tipo de DataFrame. Calculamos el producto cartesiano de los índices numéricos de los DataFrames usando el producto cartesian_product mencionado anteriormente, usamos esto para reindexar los DataFrames, y

 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 

Y, a lo largo de líneas similares,

 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 

Esta solución puede generalizar a múltiples DataFrames. Por ejemplo,

 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 

Simplificación adicional

Una solución más simple que no implique cartesian_product @ senderle es posible cuando se trata de solo dos DataFrames. Usando np.broadcast_arrays , podemos lograr casi el mismo nivel de rendimiento.

 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 

Comparación de rendimiento

Al evaluar estas soluciones en algunos DataFrames ideados con índices únicos, tenemos

introduzca la descripción de la imagen aquí

Tenga en cuenta que los tiempos pueden variar según la configuración, los datos y la elección de la función auxiliar cartesian_product , según corresponda.

Funciones de otras respuestas

 # Wen's answer: https://stackoverflow.com/a/53699198/4909087 # I've put my own spin on this to make it as fast as possible. def cartesian_product_itertools(left, right): return pd.DataFrame([ [*x, *y] for x, y in itertools.product( left.values.tolist(), right.values.tolist())]) 

Código de rendimiento de referencia
Este es el script de tiempo. Todas las funciones llamadas aquí se definen arriba.

 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', 'cartesian_product_itertools'], 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) if f in {'cartesian_product_itertools'} and c > 600: continue 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() 

Usando el product itertools y recrea el valor en el dataframe

 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 

Aquí hay un enfoque con 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 

introduzca la descripción de la imagen aquí