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
Какой метод вычисления этого результата наиболее эффективен?






Начнем с установления эталона. Самый простой способ решить эту проблему - использовать временный «ключевой» столбец:
# 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)
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() запускается, но целые числа остаются.
@CameronTaylor, вы забыли вызвать переименование с аргументом оси = 1?
нету ... даже более плотно - я заключил числа в кавычки - спасибо
Другой вопрос. Я использую cartesian_product_simplified, и у меня (как и ожидалось) заканчивается память, когда я пытаюсь присоединить 50K row df к 30K row df. Какие-нибудь советы по решению проблемы с памятью?
@CameronTaylor другие функции cartesian_product_ * также вызывают ошибку памяти? Думаю, здесь можно использовать cartesian_product_multi.
cartesian_product_multi работает для меня, но все мои столбцы uint8, похоже, стали плавающими. Я могу использовать astype ('uint8'), чтобы вернуть их к меньшему типу данных, но для больших наборов данных это добавляет много времени. Есть ли способ для cartesian_product_multi сохранить типы данных столбца?
К сожалению, при объединении кадров со смешанными типами данных может оказаться невозможным сохранить тип исходных столбцов. Я постарался выделить эту разницу в ответе.
К вашему сведению, мой друг, pandas.pydata.org/docs/reference/api/pandas.merge.html, теперь есть крест ~ :-)
После 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
Я думаю, что самым простым способом было бы добавить фиктивный столбец к каждому фрейму данных, выполнить его внутреннее слияние, а затем удалить этот фиктивный столбец из результирующего декартового фрейма данных:
left['dummy'] = 'a'
right['dummy'] = 'a'
cartesian = left.merge(right, how='inner', on='dummy')
del cartesian['dummy']
это уже обсуждалось в принятом ответе. Но теперь left.merge(right, how = "cross") уже делает это без необходимости во втором столбце.
Как-то кросс у меня не работал. Может быть проблема с версией.
Не хотели бы вы также поделиться своим вкладом в Github, я думаю, что добавление
cross joinв pandas действительно хорошо, чтобы соответствовать всем функциям соединения в SQL. github.com/pandas-dev/pandas/issues/5401