如何在Pandas中对DataFrame进行复杂条件的内连接或外连接
给定下面两个数据框:
>>> 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()
确实是解决方案的一部分,但我不能直接使用它,因为它不接受任意的“ON”条件(除非我漏掉了什么?)。
在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”列上使用merge(),再应用“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”中需要的额外索引ID,并从“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的想法是只对我们需要比较的那些列进行笛卡尔积,这基本上是正确的答案,尽管在我具体的情况下实现起来可能有点棘手(因为要通用化)。
问题:
你会如何在“c > a”上生成df_1和df_2的“连接”?你会采用相同的“笛卡尔积,过滤”方法,还是有更好的方法?
你会如何生成相同的“左外连接”?
3 个回答
conditional_join 是来自pyjanitor的一个功能,它在处理非等值连接时表现得非常好:
# pip install pyjanitor
import pandas as pd
import janitor
内连接
df_a.conditional_join(df_b, ('a', 'c', '<'))
left right
a b c d
0 1 4 2 7
1 1 4 3 8
2 2 5 3 8
左连接
df_a.conditional_join(df_b, ('a', 'c', '<'), how = 'left')
left right
a b c d
0 1 4 2.0 7.0
1 1 4 3.0 8.0
2 2 5 3.0 8.0
3 3 6 NaN NaN
这个功能可以接收多个条件的参数,这些条件是以元组的形式传入的,包含了三个部分:左边的列
、右边的列
和连接操作符
。
这可以通过广播和np.where来实现。你可以使用任何一个返回真或假的二元运算符:
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
我使用了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
这是内连接,因为这只计算了
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()
来找出所有在内连接中没有的
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