前言

去重(deduplicate)是数据清洗中最常见的操作之一。不同的数据结构(原生 list、NumPy ndarray、Pandas Series/DataFrame)在去重行为、性能与边界情况上有显著差别。本文把详细介绍了常见做法、背后的语义差异、容易踩的坑和实际建议,掌握这些方法与它们背后的语义差异,能避免很多隐晦的 bug,也能让你的代码既高效又语义清晰。


一、Python 原生 list 去重

保留原始出现顺序(推荐) — 简洁且 Python3.7+ 保证 dict 插入顺序:

lst = [3, 1, 2, 3, 2, 1, 4]
unique = list(dict.fromkeys(lst))
print(unique)  # [3, 1, 2, 4]

兼容写法(显式)

seen = set()
unique = []
for x in lst:
    if x not in seen:
        seen.add(x)
        unique.append(x)

不保序且最快(在允许顺序任意时)

unique = list(set(lst))

注意

  • set() 不保留顺序;dict.fromkeys() 从 Python 3.7 起保证插入顺序(CPython 3.6 有序是实现细节,但 3.7 起成为语言规范)。
  • 如果元素是不可哈希(比如子列表 [] 或字典 {}),则不能放到 set 或用作 dict key,需要转换(例如 tuple())或使用 O(n^2) 的逐项比较。
  • float('nan')set/dict 处理 NaN 时表现会比较怪(因为 nan != nan),见第 4 节。

二、NumPy ndarray 去重 —— np.unique

最常见用法(结果会排序)

import numpy as np
a = np.array([3, 1, 2, 3, 2, 1])
uniq = np.unique(a)
print(uniq)  # array([1, 2, 3])

np.unique 默认会对结果进行排序——这是它与 list/set 不同的主要语义差异。

保留原始出现顺序(保留第一个出现)

vals, idx = np.unique(a, return_index=True)
uniq_preserve = a[np.sort(idx)]
# 或者:uniq_preserve = vals[np.argsort(idx)]  # 两种方式等价

获取更多信息

vals, indices, inverse, counts = np.unique(a, return_index=True, return_inverse=True, return_counts=True)
# vals: 唯一值;indices: 每个唯一值第一次出现的位置;
# inverse: 原数组中每个元素对应在 vals 中的索引;counts: 每个唯一值的计数

多维数组按行/列去重(NumPy >= 1.13 支持 axis):

rows = np.array([[1,2],[1,2],[2,3]])
uniq_rows = np.unique(rows, axis=0)  # 会按字典序排序行

注意

  • np.unique 会把多个 NaN 视为一个唯一值(不同于纯 Python 的 set 行为)。
  • np.unique 对标量数组行为稳定,对 dtype=object 或混合类型数组时可能不如预期(排序/比较规则会影响输出)。
  • 性能上通常基于排序(O(n log n)),对数值数组底层实现有优化,很快但不是 O(n)。

三、Pandas 的去重(表格数据友好)

Series 去重

import pandas as pd
s = pd.Series([3,1,2,3,2,1])
s.unique()          # 返回 ndarray,通常保持“首次出现顺序”
s.drop_duplicates() # 返回 Series,保留第一个出现并保留原索引

DataFrame 去重

df = pd.DataFrame({
    'a': [1,1,2],
    'b': [10,10,20]
})
df_nodup = df.drop_duplicates()  # 按整行去重,保留首行
# 指定列组合去重:
df.drop_duplicates(subset=['a'], keep='first')

注意

  • Series.unique() 保持元素第一次出现的顺序(这一点常比 np.unique 更符合直觉)。
  • DataFrame.drop_duplicates() 非常适合表格数据,支持按列子集、保留第一个/最后一个、重置索引等。

四、特殊/边缘情况

NaN(浮点 NaN)

  • 在 Python 的 set 中,float('nan') 与自身比较是 False(nan != nan),因此使用 set 去重时行为不可靠。
  • np.unique 会把多个 NaN 视为一个唯一值。Pandas 对 NaN 的去重行为通常按“视为相等”的方式,把多个 NaN 归为一个。

不可哈希对象(列表、字典)

  • 不能直接放入 set/dict。解决办法:

    • 把子列表转为 tuple(如果语义允许)。
    • 把复杂结构序列化为字符串(json.dumps)——注意序列化一致性与性能。
    • 使用 O(n^2) 的逐项比较(当数据量小或不可转换时采用)。

例:

lst = [[1,2], [1,2], [2,3]]
# 转 tuple
unique = [list(t) for t in set(tuple(x) for x in lst)]
# 或保序做法
seen = set()
result = []
for x in lst:
    tx = tuple(x)
    if tx not in seen:
        seen.add(tx)
        result.append(x)

多维数组去重

  • NumPy: np.unique(arr, axis=0)(按行去重,但结果会排序)
  • Pandas: DataFrame.drop_duplicates() 更灵活,保留顺序并支持部分列组合。

“保留哪一个重复项?” 的语义差异

  • 保留第一个出现:多数数据清洗场景需要这个(用 dict.fromkeys、Pandas 的 drop_duplicates(keep='first')np.unique(..., return_index=True) + 排序 idx)。
  • 保留任意一个/或排序后的np.uniqueset(list) 经常返回排序或任意顺序下的唯一元素——确保这是你想要的。

五、实战建议与性能注意事项

  • 小数据(几千条以下):优先考虑可读性(dict.fromkeys / Pandas),性能通常不是瓶颈。

  • 中大型数据(几万条以上):

    • 若允许不保序且元素为可哈希:set() 是最快的(平均 O(n))。
    • 若需保序:用 seen=set() 的一次遍历实现(O(n))。
    • 对数值型大数组:np.unique 高效且内存友好(但结果会被排序——若不想排序,使用 return_index=True 再排序索引)。
  • 数据含 NaN:优先用 np.unique 或 Pandas 去重。

  • 数据含复杂不可哈希类型:若可能,将结构改为可哈希(tuple),或使用 Pandas DataFrame(行作为记录)来借助 drop_duplicates

  • 表格(多列)去重:优先 Pandas drop_duplicates(subset=[...]),更直观、功能更强(可以保留最后出现的、重设索引等)。


六、常用代码汇总

# 1. list,保留顺序(推荐)
lst = [3,1,2,3,2,1]
unique_list = list(dict.fromkeys(lst))

# 2. list,不保序(最快)
unique_unordered = list(set(lst))

# 3. numpy array,默认(排序后)
import numpy as np
a = np.array([3,1,2,3,2,1])
uniq_sorted = np.unique(a)

# 4. numpy array,保留第一个出现的顺序
vals, idx = np.unique(a, return_index=True)
uniq_preserve_order = a[np.sort(idx)]

# 5. pandas Series / DataFrame
import pandas as pd
s = pd.Series([3,1,2,3,2,1])
s_unique = s.unique()            # ndarray, 保持出现顺序
s_nodup = s.drop_duplicates()    # Series, 保留第一个并保留索引

df = pd.DataFrame({'a':[1,1,2],'b':[10,10,20]})
df_nodup = df.drop_duplicates()              # 整行去重
df_nodup_subset = df.drop_duplicates(subset=['a'])  # 按列去重

# 6. list 中包含子列表(不可哈希)且保序
lst = [[1,2],[1,2],[2,3]]
seen = set()
unique = []
for x in lst:
    t = tuple(x)   # 转为可哈希
    if t not in seen:
        seen.add(t)
        unique.append(x)

总结

去重看起来是个小事,但细节很多:你必须明确 (1)要处理的数据类型(list/ndarray/Series/DataFrame)(2)是否需要保留原始出现顺序、以及 (3)数据里是否包含 NaN 或不可哈希元素

  • 小结要点:

    • dict.fromkeys(lst)seen+遍历:可读且保序(适合大多数 list 场景)。
    • set(lst):最快但不保序,且要求元素可哈希。
    • np.unique:对数值数组非常方便且高效,但默认对结果排序(如果要保序需额外处理 return_index)。
    • pandas.Series.unique() / drop_duplicates():处理表格数据最方便,drop_duplicates() 对多列场景特别友好。
    • 注意 NaN、不可哈希对象与多维数据的特殊处理方式。
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐