Python数据分析与处理(四):不同数据格式中筛出唯一元素(去重)的详细方法
本文详细介绍了Python中不同数据结构(list、NumPy数组、Pandas Series/DataFrame)的去重方法及其差异。重点包括:1)list去重时保序推荐使用dict.fromkeys();2)NumPy数组默认排序去重,需处理return_index保序;3)Pandas提供更灵活的表格数据去重方法。文章还探讨了特殊场景(NaN处理、不可哈希对象)和性能优化建议,强调根据数据类
文章目录
前言
去重(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.unique和set(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、不可哈希对象与多维数据的特殊处理方式。
更多推荐
所有评论(0)