pandas - 通过另一个数据框的行元素过滤数据框

109

我有一个数据框 df1,它看起来像这样:

   c  k  l
0  A  1  a
1  A  2  b
2  B  2  a
3  C  2  a
4  C  2  d

还有另一个叫做df2的,就像这样:

   c  l
0  A  b
1  C  a

我想筛选df1,只保留不在df2中的值。需要过滤的值应该是(A,b)(C,a)元组。到目前为止,我尝试了应用isin方法:

d = df[~(df['l'].isin(dfc['l']) & df['c'].isin(dfc['c']))]

这对我来说太复杂了,它返回:

   c  k  l
2  B  2  a
4  C  2  d

但我期望:

   c  k  l
0  A  1  a
2  B  2  a
4  C  2  d

1
如何将两列“c”和“l”的值连接起来,并将其用作键? - IanS
7个回答

124

使用从所需列构建的多级索引,可以高效地使用 isin 进行此操作:

df1 = pd.DataFrame({'c': ['A', 'A', 'B', 'C', 'C'],
                    'k': [1, 2, 2, 2, 2],
                    'l': ['a', 'b', 'a', 'a', 'd']})
df2 = pd.DataFrame({'c': ['A', 'C'],
                    'l': ['b', 'a']})
keys = list(df2.columns.values)
i1 = df1.set_index(keys).index
i2 = df2.set_index(keys).index
df1[~i1.isin(i2)]

输入图像描述

我认为这比@IanS的相似解决方案更好,因为它不假设任何列类型(即它适用于数字和字符串)。


(上面的答案是编辑的。以下是我的初始答案)

有趣! 这是我以前没有遇到过的...我可能会通过合并两个数组,然后删除定义了df2的行来解决它。这是一个示例,它使用了一个临时数组:

df1 = pd.DataFrame({'c': ['A', 'A', 'B', 'C', 'C'],
                    'k': [1, 2, 2, 2, 2],
                    'l': ['a', 'b', 'a', 'a', 'd']})
df2 = pd.DataFrame({'c': ['A', 'C'],
                    'l': ['b', 'a']})

# create a column marking df2 values
df2['marker'] = 1

# join the two, keeping all of df1's indices
joined = pd.merge(df1, df2, on=['c', 'l'], how='left')
joined

enter image description here

# extract desired columns where marker is NaN
joined[pd.isnull(joined['marker'])][df1.columns]

图片描述

也许有一种方法可以不使用临时数组来完成这个操作,但我想不出来。只要你的数据不是非常庞大,上述方法应该是一个快速且足够的答案。


谢谢您的归功于 :) 我认为您应该将您的编辑作为新答案,希望它能被接受。我一定会投票支持它! - IanS
我将编辑后的内容更改为主要答案。谢谢! - jakevdp
好方法!我以为这很容易做到,感谢大家的帮助! - Fabio Lamanna
1
你的初始答案创建了一个标记列,但是pd.merge()现在包含一个名为'indicator'的参数。如果你选择indicator=True,则会添加一个额外的列(称为'_merge'),它本身就是新创建的合并df上的标记。然后,你可以过滤joined['_merge']=='left_only'。 - Sander van den Oord
这太棒了,正是我想要的。不过我很好奇:为什么不能直接比较这两个数据框呢?在我的情况下,我有两个具有相同列但可能索引和长度不相等的数据框。我想比较匹配的行而不考虑索引。这个解决方案可以工作,但似乎是多余的步骤,当 df1[~df1.isin(df2)] 或者甚至 df1[~df[keys].isin(df2[keys])] 应该达到相同的结果时。(剧透,它们没有) - Matt
显示剩余3条评论

44

这非常简洁并且工作得很好:

df1 = df1[~df1.index.isin(df2.index)]

14
虽然这段代码可能回答了问题,但是提供关于如何和/或为什么解决问题的附加上下文将会提升答案的长期价值。请阅读此 how-to-answer 以提供高质量的答案。 - thewaywewere
11
这仅适用于示例数据对齐的情况,如果键不对齐,这在任何方面都会失败。不确定为什么会获得14个赞。 - Erfan

29

使用DataFrame.mergeDataFrame.query

一种更优雅的方法是使用left join参数indicator=True,然后使用query过滤所有left_only的行:

d = (
    df1.merge(df2, 
              on=['c', 'l'],
              how='left', 
              indicator=True)
    .query('_merge == "left_only"')
    .drop(columns='_merge')
)

print(d)
   c  k  l
0  A  1  a
2  B  2  a
4  C  2  d

indicator=True会返回一个包含额外一列_merge的数据框,该列标识每行为left_only, both, right_only中的哪一种情况:

indicator=True會回傳一個額外包含一欄_merge的資料框,該欄會標記每一行為left_only, both, right_only中的哪一種情況:

df1.merge(df2, on=['c', 'l'], how='left', indicator=True)

   c  k  l     _merge
0  A  1  a  left_only
1  A  2  b       both
2  B  2  a  left_only
3  C  2  a       both
4  C  2  d  left_only

确实是迄今为止最优雅的解决方案。以前我只使用合并和过滤,但是.query()打开了一个全新的世界。 - malvoisen
这应该更高! - Ignacio Peletier

3

我认为这是一种非常简单的方法,当你想根据另一个数据框中的多列或甚至自定义列表来过滤数据框时使用。

df1 = pd.DataFrame({'c': ['A', 'A', 'B', 'C', 'C'],
                    'k': [1, 2, 2, 2, 2],
                    'l': ['a', 'b', 'a', 'a', 'd']})
df2 = pd.DataFrame({'c': ['A', 'C'],
                    'l': ['b', 'a']})

#values of df2 columns 'c' and 'l' that will be used to filter df1
idxs = list(zip(df2.c.values, df2.l.values)) #[('A', 'b'), ('C', 'a')]

#so df1 is filtered based on the values present in columns c and l of df2 (idxs)
df1 = df1[pd.Series(list(zip(df1.c, df1.l)), index=df1.index).isin(idxs)]

2
如何呢:
df1['key'] = df1['c'] + df1['l']
d = df1[~df1['key'].isin(df2['c'] + df2['l'])].drop(['key'], axis=1)

1
我觉得你的回答如果增加一些信息会更有说服力。你能否[编辑]这个答案,包括一些关于为什么某人应该使用这种方法,或者至少这段代码实现了什么的信息?如果你无法想到任何详细说明,可以考虑这样一个场景:如果我盲目地复制粘贴你的代码到我的应用程序中,是否应该担心任何边缘情况?什么情况下应该避免使用这种方法? - theB

0
另一个避免创建额外列或执行合并的选项是对df2进行groupby以获取不同的(c,l)对,然后仅使用该过滤器df1。
gb = df2.groupby(("c", "l")).groups
df1[[p not in gb for p in zip(df1['c'], df1['l'])]]]

对于这个小例子来说,它似乎比基于pandas的方法运行得更快一点(在我的机器上是666微秒对1.76毫秒),但我怀疑在更大的例子上可能会更慢,因为它要转入纯Python环境。

0

您可以将两个数据框连接起来并删除所有重复项:

df1.append(df2).drop_duplicates(subset=['c', 'l'], keep=False)

输出:

   c    k  l
0  A  1.0  a
2  B  2.0  a
4  C  2.0  d

如果在df1中有重复的subset=['c', 'l'],那么这种方法就不起作用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接