如何有条件地从pandas数据框中删除重复项

15
考虑以下数据帧。
import pandas as pd
df = pd.DataFrame({'A' : [1, 2, 3, 3, 4, 4, 5, 6, 7],
                   'B' : ['a','b','c','c','d','d','e','f','g'],
                   'Col_1' :[np.NaN, 'A','A', np.NaN, 'B', np.NaN, 'B', np.NaN, np.NaN],
                   'Col_2' :[2,2,3,3,3,3,4,4,5]})
df
Out[92]: 
    A  B Col_1  Col_2
 0  1  a   NaN      2
 1  2  b     A      2
 2  3  c     A      3
 3  3  c   NaN      3
 4  4  d     B      3
 5  4  d   NaN      3
 6  5  e     B      4
 7  6  f   NaN      4
 8  7  g   NaN      5

我想删除所有在列 'A' 'B' 方面是重复的行。我想删除具有 NaN 条目的记录(我知道对于所有重复项,都会有一个 NaN 条目和一个非 NaN 条目)。最终结果应该是这样的。

    A  B Col_1  Col_2
 0  1  a   NaN      2
 1  2  b     A      2
 2  3  c     A      3
 4  4  d     B      3
 6  5  e     B      4
 7  6  f   NaN      4
 8  7  g   NaN      5

欢迎所有简洁易懂的一句话。


在反对者的辩护中,你本可以选择一个例子,其中简单地使用df.drop_duplicates就能给出你不想要的答案。 - cs95
1
是的,是的。我接受负评 :) - mortysporty
3个回答

9
如果目标只是删除NaN重复项,则需要稍微更复杂的解决方案。
首先,按照ABCol_1进行排序,这样每个组的NaN就会移动到底部。然后使用keep=first调用df.drop_duplicates
out = df.sort_values(['A', 'B', 'Col_1']).drop_duplicates(['A', 'B'], keep='first')
print(out)

   A  B Col_1  Col_2
0  1  a   NaN      2
1  2  b     A      2
2  3  c     A      3
4  4  d     B      3
6  5  e     B      4
7  6  f   NaN      4
8  7  g   NaN      5

也许可以,但这能保证Col_1的非NaN值是被保留的行吗? - mortysporty
1
@mortysporty 已编辑。 - cs95

8
这里有一个替代方案:
df[~((df[['A', 'B']].duplicated(keep=False)) & (df.isnull().any(axis=1)))]
#    A  B Col_1  Col_2
# 0  1  a   NaN      2
# 1  2  b     A      2
# 2  3  c     A      3
# 4  4  d     B      3
# 6  5  e     B      4
# 7  6  f   NaN      4
# 8  7  g   NaN      5

这里使用位运算符“not”~来取反满足以下联合条件的行:重复行(当参数keep=False时,该方法对于所有非唯一行都会返回True),并且包含至少一个空值。因此,当表达式df[['A', 'B']].duplicated(keep=False)返回以下Series时:
# 0    False
# 1    False
# 2     True
# 3     True
# 4     True
# 5     True
# 6    False
# 7    False
# 8    False

"...并且表达式df.isnull().any(axis=1)返回以下Series:"
# 0     True
# 1    False
# 2    False
# 3     True
# 4    False
# 5     True
# 6    False
# 7     True
# 8     True

我们用括号将两个表达式都包起来(当在索引操作中使用多个表达式时,Pandas 语法要求必须这样做),然后再次将它们用括号包起来,以便我们可以否定整个表达式(即 `~( ... )`),就像这样:
~((df[['A','B']].duplicated(keep=False)) & (df.isnull().any(axis=1))) & (df['Col_2'] != 5)

# 0     True
# 1     True
# 2     True
# 3    False
# 4     True
# 5    False
# 6     True
# 7     True
# 8    False

您可以使用逻辑运算符&|(或运算符)构建更复杂的条件。与SQL类似,必要时可以使用额外的括号对条件进行分组;例如,使用df[ ( (X) & (Y) ) | (Z) ]基于以下逻辑进行过滤:“同时满足条件X条件Y,或者条件Z成立”。

我认为你需要将 keep=False 传递给 duplicated 才能使其正常工作。 - ayhan
@ayhan 我也在想同样的事情 :) - cmaher
你好。如果我们想要除去除了NaN以外的其他值,这个方法还有效吗?我们能不能在“&”后面修正参数? - mortysporty
@mortysporty 是的,基本上是对的--不过我应该说明一下,根据你如何测试该值,最好取消条件的分组(即删除外部括号),这样你就可以做类似于 ~(df.duplicated) & (df.Col_2 != 5) 的操作。如果你直接将 df.Col_2 != 5 替换到上面的一行代码中,它会被否定(即True变为False,反之亦然),因为两个当前条件在 ~(...) 中被分组的方式。 - cmaher

3

或者你可以直接使用first()函数,通过这个函数,会返回第一个非空值,所以原始输入的顺序并不是很重要。

df.groupby(['A','B']).first()

Out[180]: 
    Col_1  Col_2
A B             
1 a   NaN      2
2 b     A      2
3 c     A      3
4 d     B      3
5 e     B      4
6 f   NaN      4
7 g   NaN      5

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