как выполнить внутреннее или внешнее соединение DataFrames с Pandas по неупрощенному критерию

Учитывая два фрейма данных, как показано ниже:

>>> import pandas as pd

>>> df_a = pd.DataFrame([{"a": 1, "b": 4}, {"a": 2, "b": 5}, {"a": 3, "b": 6}])
>>> df_b = pd.DataFrame([{"c": 2, "d": 7}, {"c": 3, "d": 8}])
>>> df_a
   a  b
0  1  4
1  2  5
2  3  6

>>> df_b
   c  d
0  2  7
1  3  8

мы хотели бы создать соединение в стиле SQL обоих фреймов данных, используя неупрощенный критерий, скажем, «df_b.c> df_a.a». Насколько я могу судить, хотя merge(), безусловно, является частью решения, я не могу использовать его напрямую, поскольку он не принимает произвольные выражения для критерия «ВКЛ» (если я чего-то не упускаю?).

В SQL результаты выглядят так:

# inner join
sqlite> select * from df_a join df_b on c > a;
1|4|2|7
1|4|3|8
2|5|3|8

# outer join
sqlite> select * from df_a left outer join df_b on c > a;
1|4|2|7
1|4|3|8
2|5|3|8
3|6||

мой текущий подход к внутреннему соединению - создать декартово произведение df_a и df_b, добавив столбец «1» к обоим, затем используя merge () в столбце «1» и затем применив «c> a» критерии.

>>> import numpy as np
>>> df_a['ones'] = np.ones(3)
>>> df_b['ones'] = np.ones(2)
>>> cartesian = pd.merge(df_a, df_b, left_on='ones', right_on='ones')
>>> cartesian
   a  b  ones  c  d
0  1  4     1  2  7
1  1  4     1  3  8
2  2  5     1  2  7
3  2  5     1  3  8
4  3  6     1  2  7
5  3  6     1  3  8
>>> cartesian[cartesian.c > cartesian.a]
   a  b  ones  c  d
0  1  4     1  2  7
1  1  4     1  3  8
3  2  5     1  3  8

для внешнего соединения я не уверен в лучшем способе, пока я играл с получением внутреннего соединения, затем применял отрицание критериев, чтобы получить все другие строки, а затем пытался отредактировать это "отрицание "установить на оригинал, но на самом деле это не работает.

Изменить. HYRY ответил на конкретный вопрос здесь, но мне нужно было что-то более общее и большее в Pandas API, так как моим критерием соединения может быть что угодно, а не только одно сравнение. Для внешнего соединения сначала я добавляю дополнительный индекс к «левой» стороне, который будет поддерживать себя после того, как я выполню внутреннее соединение:

df_a['_left_index'] = df_a.index

затем мы делаем декартово и получаем внутреннее соединение:

cartesian = pd.merge(df_a, df_b, left_on='ones', right_on='ones')
innerjoin = cartesian[cartesian.c > cartesian.a]

затем я получаю дополнительные идентификаторы индексов в «df_a», которые нам понадобятся, и получаю строки из «df_a»:

remaining_left_ids = set(df_a['_left_index']).\
                    difference(innerjoin['_left_index'])
remaining = df_a.ix[remaining_left_ids]

затем мы используем прямую concat (), которая заменяет отсутствующие столбцы на «NaN» для левого края (я думал, что раньше этого не делали, но думаю, что это так):

outerjoin = pd.concat([innerjoin, remaining]).reset_index()

Идея HYRY сделать декартово только для тех столбцов, которые нам нужно сравнить, в основном является правильным ответом, хотя в моем конкретном случае это может быть немного сложно реализовать (обобщенно и все).

вопросов:

  1. Как бы вы произвели «соединение» df_1 и df_2 на «c> a»? Вы бы применили тот же подход «декартово произведение, фильтр» или есть какой-то лучший способ?

  2. Как бы вы создали такое же «левое внешнее соединение»?


person zzzeek    schedule 23.03.2013    source источник


Ответы (2)


Я использую внешний метод ufunc для вычисления результата, вот пример:

Для начала немного данных:

import pandas as pd
import numpy as np
df_a = pd.DataFrame([{"a": 1, "b": 4}, {"a": 2, "b": 5}, {"a": 3, "b": 6}, {"a": 4, "b": 8}, {"a": 1, "b": 7}])
df_b = pd.DataFrame([{"c": 2, "d": 7}, {"c": 3, "d": 8}, {"c": 2, "d": 10}])
print "df_a"
print df_a
print "df_b"
print df_b

выход:

df_a
   a  b
0  1  4
1  2  5
2  3  6
3  4  8
4  1  7
df_b
   c   d
0  2   7
1  3   8
2  2  10

Внутреннее соединение, поскольку при этом вычисляется только декартово произведение c & a, использование памяти меньше, чем декартово произведение всего DataFrame:

ia, ib = np.where(np.less.outer(df_a.a, df_b.c))
print pd.concat((df_a.take(ia).reset_index(drop=True), 
                 df_b.take(ib).reset_index(drop=True)), axis=1)

выход:

   a  b  c   d
0  1  4  2   7
1  1  4  3   8
2  1  4  2  10
3  2  5  3   8
4  1  7  2   7
5  1  7  3   8
6  1  7  2  10

чтобы вычислить левое внешнее соединение, используйте numpy.setdiff1d(), чтобы найти все строки df_a, которых нет во внутреннем соединении:

na = np.setdiff1d(np.arange(len(df_a)), ia)
nb = -1 * np.ones_like(na)
oa = np.concatenate((ia, na))
ob = np.concatenate((ib, nb))
print pd.concat([df_a.take(oa).reset_index(drop=True), 
                 df_b.take(ob).reset_index(drop=True)], axis=1)

выход:

   a  b   c   d
0  1  4   2   7
1  1  4   3   8
2  1  4   2  10
3  2  5   3   8
4  1  7   2   7
5  1  7   3   8
6  1  7   2  10
7  3  6 NaN NaN
8  4  8 NaN NaN
person HYRY    schedule 23.03.2013
comment
все еще разбирая это, есть ли способ выполнить выражение с помощью серии Pandas (то есть, сгенерированного выражением типа df_a.a ‹df_b.c? У меня не обязательно будет a‹ c в качестве моего выражения, оно фактически может быть любым выражением (должно быть понятно, что я строю ...) - person zzzeek; 23.03.2013
comment
хотя просто идея сделать декартово на столбцах, мне нужно сохранить память, стоит изучить ... - person zzzeek; 23.03.2013

Это можно сделать с помощью broadcasting и np.where. Используйте любой желаемый бинарный оператор, который оценивает значение True / False:

import operator as op

df_a = pd.DataFrame([{"a": 1, "b": 4}, {"a": 2, "b": 5}, {"a": 3, "b": 6}])
df_b = pd.DataFrame([{"c": 2, "d": 7}, {"c": 3, "d": 8}])

binOp   = op.lt
matches = np.where(binOp(df_a.a[:,None],df_b.c.values))

print pd.concat([df.ix[idxs].reset_index(drop=True) 
                 for df,idxs in zip([df_a,df_b],matches)],
                axis=1).to_csv()

,a,b,c,d

0,1,4,2,7

1,1,4,3,8

2,2,5,3,8

person jharting    schedule 18.08.2016