在 DataFrame 中搜索不完整的重复行时处理 NaN

Dealing with NaN while searching for incomplete duplicate rows in the DataFrame

有点难以解释,但请耐心等待。假设我们有以下数据集:

df = pd.DataFrame({'foo': [1, 1, 1, 8, 1, 5, 5, 5],
                   'bar': [2, float('nan'), 2, 5, 2, 3, float('nan'), 6],
                   'abc': [3, 3, 3, 7, float('nan'), 9, 9, 7],
                   'def': [4, 4, 4, 2, 4, 8, 8, 8]})
print(df)
>>>
   foo  bar  abc  def
0    1  2.0  3.0    4
1    1  NaN  3.0    4
2    1  2.0  3.0    4
3    8  5.0  7.0    2
4    1  2.0  NaN    4
5    5  3.0  9.0    8
6    5  NaN  9.0    8
7    5  6.0  7.0    8

我们的目标是找到所有重复的行。但是,其中一些重复项是不完整的,因为它们具有 NaN 值。尽管如此,我们也想找到这些重复项。所以预期的结果是:

   foo  bar  abc  def
0    1  2.0  3.0    4
1    1  NaN  3.0    4
2    1  2.0  3.0    4
4    1  2.0  NaN    4
5    5  3.0  9.0    8
6    5  NaN  9.0    8

如果我们尝试以直接的方式执行此操作,那只会给我们完整的行:

print(df[df.duplicated(keep=False)])
>>>
   foo  bar  abc  def
0    1  2.0  3.0    4
2    1  2.0  3.0    4

我们可以尝试通过只使用没有任何缺失值的列来规避它:

print(df[df.duplicated(['foo', 'def'], keep=False)])
>>>
   foo  bar  abc  def
0    1  2.0  3.0    4
1    1  NaN  3.0    4
2    1  2.0  3.0    4
4    1  2.0  NaN    4
5    5  3.0  9.0    8
6    5  NaN  9.0    8
7    5  6.0  7.0    8

非常接近,但不完全是。事实证明,我们遗漏了 'abc' 列中的一条重要信息,该信息使我们能够确定第 7 行不重复。所以我们想包括它:

print(df[df.duplicated(['foo', 'def', 'abc'], keep=False)])
>>>
   foo  bar  abc  def
0    1  2.0  3.0    4
1    1  NaN  3.0    4
2    1  2.0  3.0    4
5    5  3.0  9.0    8
6    5  NaN  9.0    8

它成功地删除了第 7 行。但是,它也删除了第 4 行。NaN 被认为是它自己的独立值,而不是可以等于任何东西的值,因此它出现在第 4 行中使我们无法检测到它重复。

现在,我知道我们不确定第 4 行是否真的是 [1, 2, 3, 4]。据我们所知,它可以是完全不同的东西,比如 [1, 2, 9, 4]。但是假设值 1 和 4 实际上是其他一些非常具体的值。例如,34900 和 23893。假设还有更多列也完全相同。此外,完整的重复行不仅仅是 0 和 2,它们有两百多个,然后另外 40 行在所有列中都具有这些相同的值,除了 'abc',其中它们具有 NaN。所以对于这组特定的重复项,这种巧合是极不可能的,这就是我们如何确定记录 [1, 2, 3, 4] 有问题,并且第 4 行几乎肯定是重复项。

但是,如果 [1, 2, 3, 4] 不是唯一的重复组,则可能其他一些组在 'foo' 和 [=36= 中具有非常不特定的值] 列,例如 1 和 500。碰巧在子集中包含列 'abc' 对解决此问题非常有帮助,因为 'abc' 列中的值几乎总是非常具体,并且允许几乎确定地确定所有重复项。但是有一个缺点 - 'abc' 列有缺失值,所以通过使用它我们牺牲了对一些带有 NaN 的重复项的检测。其中一些我们知道一个事实是重复的(比如前面提到的 40 个),所以这是一个艰难的困境。

处理这种情况的最佳方法是什么?如果我们能以某种方式使 NaN 等于所有内容,而不是在重复检测期间不等于任何内容,那就太好了,这将解决这个问题。但我怀疑这是可能的。我是不是应该逐组去手动检查一下?

感谢@cs95 帮助解决这个问题。当我们对值进行排序时,默认情况下将 NaN 放在排序组的末尾,如果不完整的记录与现有值而不是此 NaN 重复,它将在 NaN 的正上方结束。这意味着我们可以使用 ffill() 方法用该值填充此 NaN。所以我们用最接近它们的行中的数据向前填充缺失数据,这样我们就可以更准确地确定该行是否重复。

我最终使用的代码(根据这个可重现的示例进行了调整)如下所示:

#printing all duplicates
col_list = ['foo', 'def', 'abc', 'bar']
show_mask = df.sort_values(col_list).ffill().duplicated(col_list, keep=False).sort_index()
df[show_mask].sort_values(col_list)

#deleting duplicates, but keeping one record per duplicate group
delete_mask = df.sort_values(col_list).ffill().duplicated(col_list).sort_index()
df = df[~delete_mask].reset_index(drop=True)

可以使用 bfill() 而不是 ffill(),因为它是颠倒应用的相同原理。但它需要将一些使用的方法的默认参数更改为相反的参数,即na_position='first'keep='last'sort_index() 仅用于消除重建索引警告。

请注意,列出列的顺序非常重要,因为它用于排序优先级。为确保缺失值上方的记录是要复制的正确值,您必须首先枚举所有不具有任何缺失值的列,然后才枚举具有缺失值的列。对于前面的列,顺序并不重要。对于后者,至关重要的是从具有最多 diverse/specific 值的列开始并以最少 diverse/specific 结束(float -> int -> string -> bool 是一个很好的经验法则,但这在很大程度上取决于列在数据集中代表的确切变量类型)。在这个例子中,它们都是一样的,但即使在这里,如果你把 'bar' 放在 'abc' 之前,你也不会得到正确的答案。

即便如此,它也不是完美的解决方案。将记录的最完整版本放在顶部,并在需要时将其中的信息转移到下面不太完整的版本,它做得很好。但有可能完全不存在记录的完整版本。例如,假设有记录 [5 3 Nan 8] 和 [5 NaN 9 8](并且没有 [5 3 9 8] 记录)。该解决方案无法让他们相互交换丢失的部分。它会在前者中放入 9,但在后者中将保留 NaN 为空,并且会导致这些重复项被忽视。

如果您只处理一个不完整的列,这不是问题,但每个添加的不完整列都会使这种情况越来越频繁。但是,添加所有列仍然更可取,因为未能检测到某些重复项总比最终在列表中出现一些错误的重复项要好,除非您使用所有列,否则这是一种明显的可能性。