Производительное декартово произведение (CROSS JOIN) с пандами

The contents of this post were originally meant to be a part of Pandas Merging 101, but due to the nature and size of the content required to fully do justice to this topic, it has been moved to its own QnA.

Учитывая два простых DataFrames;

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

Какой метод вычисления этого результата наиболее эффективен?

Не хотели бы вы также поделиться своим вкладом в Github, я думаю, что добавление cross join в pandas действительно хорошо, чтобы соответствовать всем функциям соединения в SQL. github.com/pandas-dev/pandas/issues/5401

BENY 11.12.2018 21:58
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
61
1
19 336
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Ответ принят как подходящий

Начнем с установления эталона. Самый простой способ решить эту проблему - использовать временный «ключевой» столбец:

# pandas <= 1.1.X
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)
# pandas >= 1.2 (est)
left.merge(right, how = "cross")
  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

Это работает так: обоим DataFrames назначается временный «ключевой» столбец с одинаковым значением (скажем, 1). merge затем выполняет JOIN "многие ко многим" по "ключу".

Хотя трюк JOIN «многие ко многим» работает для DataFrames разумного размера, вы увидите относительно более низкую производительность для больших данных.

Для более быстрой реализации потребуется 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 на уникальных или неуникальных индексированных фреймах данных

Disclaimer
These solutions are optimised for DataFrames with non-mixed scalar dtypes. If dealing with mixed dtypes, use at your own risk!

Этот трюк будет работать с любым типом DataFrame. Мы вычисляем декартово произведение числовых индексов DataFrames, используя вышеупомянутый cartesian_product, используем его для переиндексации 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

Это решение можно распространить на несколько фреймов данных. Например,

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

Дальнейшее упрощение

При работе с только два DataFrames возможно более простое решение, не связанное с @senderle's cartesian_product. Используя 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

Сравнение производительности

Тестируя эти решения на некоторых надуманных DataFrames с уникальными индексами, мы имеем

Обратите внимание, что время может варьироваться в зависимости от ваших настроек, данных и выбора вспомогательной функции 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()


Продолжить чтение

Перейдите к другим темам в Pandas Merging 101, чтобы продолжить обучение:

* you are here

Почему имена столбцов становятся целыми числами? Когда я пытаюсь их переименовать, .rename() запускается, но целые числа остаются.

OverflowingTheGlass 11.01.2019 22:14

@CameronTaylor, вы забыли вызвать переименование с аргументом оси = 1?

cs95 11.01.2019 22:14

нету ... даже более плотно - я заключил числа в кавычки - спасибо

OverflowingTheGlass 11.01.2019 22:16

Другой вопрос. Я использую cartesian_product_simplified, и у меня (как и ожидалось) заканчивается память, когда я пытаюсь присоединить 50K row df к 30K row df. Какие-нибудь советы по решению проблемы с памятью?

OverflowingTheGlass 11.01.2019 22:54

@CameronTaylor другие функции cartesian_product_ * также вызывают ошибку памяти? Думаю, здесь можно использовать cartesian_product_multi.

cs95 11.01.2019 23:26
cartesian_product_multi работает для меня, но все мои столбцы uint8, похоже, стали плавающими. Я могу использовать astype ('uint8'), чтобы вернуть их к меньшему типу данных, но для больших наборов данных это добавляет много времени. Есть ли способ для cartesian_product_multi сохранить типы данных столбца?
Nimai 14.05.2020 05:00

К сожалению, при объединении кадров со смешанными типами данных может оказаться невозможным сохранить тип исходных столбцов. Я постарался выделить эту разницу в ответе.

cs95 02.01.2021 02:22

К вашему сведению, мой друг, pandas.pydata.org/docs/reference/api/pandas.merge.html, теперь есть крест ~ :-)

BENY 30.03.2021 15:43

После pandas 1.2.0 merge теперь имеет опцию cross

left.merge(right, how='cross')

Использование 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

Вот подход с тройным 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

Я думаю, что самым простым способом было бы добавить фиктивный столбец к каждому фрейму данных, выполнить его внутреннее слияние, а затем удалить этот фиктивный столбец из результирующего декартового фрейма данных:

left['dummy'] = 'a'
right['dummy'] = 'a'

cartesian = left.merge(right, how='inner', on='dummy')

del cartesian['dummy']

это уже обсуждалось в принятом ответе. Но теперь left.merge(right, how = "cross") уже делает это без необходимости во втором столбце.

cs95 19.11.2021 10:43

Как-то кросс у меня не работал. Может быть проблема с версией.

abhygag 20.11.2021 11:05

Другие вопросы по теме