วิธีดำเนินการรวมภายในหรือภายนอกของ DataFrames กับ Pandas บนเกณฑ์ที่ไม่ง่าย

รับสอง dataframes ดังนี้:

>>> 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 ของ dataframe ทั้งสองโดยใช้เกณฑ์ที่ไม่ง่าย สมมติว่า "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" ให้กับทั้งคู่ จากนั้นใช้การผสาน () ในคอลัมน์ "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 เนื่องจากเกณฑ์การเข้าร่วมของฉันอาจเป็นอะไรก็ได้ ไม่ใช่แค่การเปรียบเทียบเพียงอย่างเดียว สำหรับ outerjoin ก่อนอื่นฉันจะเพิ่มดัชนีพิเศษที่ด้าน "ซ้าย" ซึ่งจะคงตัวเองไว้หลังจากที่ฉันเข้าร่วมภายใน:

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 Series (นั่นคือสร้างโดยนิพจน์เช่น df_a.a ‹ df_b.c หรือไม่ ฉันไม่จำเป็นต้องมี ‹ c เป็นนิพจน์ของฉัน จริงๆ แล้วสามารถทำได้ เป็นการแสดงออกใด ๆ (ควรชัดเจนว่าฉันกำลังสร้างอะไร...) - person zzzeek; 23.03.2013
comment
แม้ว่าจะเป็นเพียงแค่ความคิดที่จะทำคาร์ทีเซียนกับคอลัมน์ แต่ฉันต้องบันทึกหน่วยความจำซึ่งควรค่าแก่การพิจารณา... - person zzzeek; 23.03.2013

สิ่งนี้สามารถทำได้ด้วยการออกอากาศและ 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