Pandas Drop Duplicates:如何在Python中删除重复行
Updated on
重复行是实际数据集中最常见的数据质量问题之一。它们通过重复的 API 调用、重叠的 CSV 导出、损坏的 ETL 管道或手动数据录入时的简单复制粘贴错误悄悄潜入。如果不加以控制,重复数据会膨胀行数、扭曲平均值和总和,并在机器学习模型中引入偏差。一个看起来有 10,000 条客户记录的数据集,实际上可能只有 8,200 个唯一客户和 1,800 个幽灵条目,在暗中破坏着基于它构建的每一个计算。
pandas 的 drop_duplicates() 方法是检测和删除这些冗余行的标准方法。本指南详细介绍每个参数,展示实际去重的常见模式,并涵盖大型数据集的性能注意事项。每个代码示例都可以直接复制使用,并包含预期输出。
为什么重复数据很重要
在进入代码之前,值得了解重复行究竟会破坏什么:
| 问题 | 发生了什么 | 示例 |
|---|---|---|
| 膨胀的计数 | len(df) 和 value_counts() 过多计数 | 一个客户出现3次,导致"总客户数"高出3倍 |
| 错误的平均值 | mean() 对重复行赋予更高权重 | 一个高价值订单被计算两次,使平均订单价值向上偏移 |
| 损坏的连接 | 在有重复的键上合并导致行爆炸 | 多对多合并产生笛卡尔积而不是1:1映射 |
| 糟糕的ML模型 | 包含重复样本的训练数据使模型产生偏差 | 模型记住重复的示例并过拟合 |
| 浪费的存储空间 | 冗余行消耗磁盘和内存 | 2GB的数据集去重后可能只有1.4GB |
解决方法很简单:找到重复项,决定保留哪个副本(如果有的话),然后删除其余的。
基本语法:df.drop_duplicates()
最简单的调用会删除每列值都相同的行:
import pandas as pd
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Alice', 'Charlie', 'Bob'],
'age': [30, 25, 30, 35, 25],
'city': ['NYC', 'LA', 'NYC', 'Chicago', 'LA']
})
print("Before:")
print(df)
df_clean = df.drop_duplicates()
print("\nAfter:")
print(df_clean)输出:
Before:
name age city
0 Alice 30 NYC
1 Bob 25 LA
2 Alice 30 NYC
3 Charlie 35 Chicago
4 Bob 25 LA
After:
name age city
0 Alice 30 NYC
1 Bob 25 LA
3 Charlie 35 Chicago第2行和第4行分别是第0行和第1行的精确副本,因此被删除了。注意原始索引值(0、1、3)被保留了。如果你想要一个干净的连续索引,请链式调用 .reset_index(drop=True)。
完整方法签名
DataFrame.drop_duplicates(subset=None, keep='first', inplace=False, ignore_index=False)| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
subset | 列标签或列表 | None(所有列) | 仅在识别重复时考虑这些列 |
keep | 'first'、'last'、False | 'first' | 保留哪个重复项;False 删除所有副本 |
inplace | bool | False | 如果为 True,就地修改 DataFrame 并返回 None |
ignore_index | bool | False | 如果为 True,将结果的索引从0重置到n-1 |
先用 df.duplicated() 查找重复项
在删除重复项之前,你通常想先检查它们。duplicated() 方法返回一个标记重复行的布尔 Series:
df = pd.DataFrame({
'order_id': [101, 102, 103, 101, 104, 102],
'product': ['Widget', 'Gadget', 'Widget', 'Widget', 'Gizmo', 'Gadget'],
'amount': [29.99, 49.99, 29.99, 29.99, 19.99, 49.99]
})
# Show which rows are duplicates
print(df.duplicated())输出:
0 False
1 False
2 False
3 True
4 False
5 True
dtype: bool要查看实际的重复行:
duplicates = df[df.duplicated(keep=False)]
print(duplicates)输出:
order_id product amount
0 101 Widget 29.99
1 102 Gadget 49.99
3 101 Widget 29.99
5 102 Gadget 49.99使用 keep=False 会将所有副本标记为重复(不仅仅是第二次出现),这样你就可以看到参与重复的每一行。
计算重复项数量
要快速计算存在多少重复行:
num_duplicates = df.duplicated().sum()
print(f"Number of duplicate rows: {num_duplicates}")
total_rows = len(df)
unique_rows = df.drop_duplicates().shape[0]
print(f"Total: {total_rows}, Unique: {unique_rows}, Duplicates: {total_rows - unique_rows}")输出:
Number of duplicate rows: 2
Total: 6, Unique: 4, Duplicates: 2这是清理前后的一个有用的完整性检查。
subset 参数:仅检查特定列
通常你会想基于关键列进行去重,而不是要求每一列都匹配。subset 参数允许你指定哪些列决定唯一性:
df = pd.DataFrame({
'email': ['alice@mail.com', 'bob@mail.com', 'alice@mail.com', 'charlie@mail.com'],
'name': ['Alice', 'Bob', 'Alice Smith', 'Charlie'],
'signup_date': ['2025-01-01', '2025-01-02', '2025-01-15', '2025-01-03']
})
print("Original:")
print(df)
# Deduplicate by email only
df_deduped = df.drop_duplicates(subset=['email'])
print("\nDeduplicated by email:")
print(df_deduped)输出:
Original:
email name signup_date
0 alice@mail.com Alice 2025-01-01
1 bob@mail.com Bob 2025-01-02
2 alice@mail.com Alice Smith 2025-01-15
3 charlie@mail.com Charlie 2025-01-03
Deduplicated by email:
email name signup_date
0 alice@mail.com Alice 2025-01-01
1 bob@mail.com Bob 2025-01-02
3 charlie@mail.com Charlie 2025-01-03第2行被删除了,因为 alice@mail.com 已经出现在第0行中,尽管 name 和 signup_date 的值不同。你也可以传递多个列:subset=['email', 'name'] 只有在两列都匹配时才会将行视为重复。
keep 参数:first、last 或 False
keep 参数控制保留哪个出现:
df = pd.DataFrame({
'sensor_id': ['S1', 'S2', 'S1', 'S2', 'S1'],
'reading': [22.5, 18.3, 23.1, 18.3, 24.0],
'timestamp': ['08:00', '08:00', '09:00', '09:00', '10:00']
})
print("keep='first' (default):")
print(df.drop_duplicates(subset=['sensor_id'], keep='first'))
print("\nkeep='last':")
print(df.drop_duplicates(subset=['sensor_id'], keep='last'))
print("\nkeep=False (drop all duplicates):")
print(df.drop_duplicates(subset=['sensor_id'], keep=False))输出:
keep='first' (default):
sensor_id reading timestamp
0 S1 22.5 08:00
1 S2 18.3 08:00
keep='last':
sensor_id reading timestamp
3 S2 18.3 09:00
4 S1 24.0 10:00
keep=False (drop all duplicates):
Empty DataFrame
Columns: [sensor_id, reading, timestamp]
Index: []| keep 值 | 行为 | 使用场景 |
|---|---|---|
'first' | 保留第一次出现,删除后续的 | 保留最早的记录 |
'last' | 保留最后一次出现,删除之前的 | 保留最新的记录 |
False | 删除所有有重复的行 | 查找真正唯一的行(完全没有副本) |
在上面的示例中,keep=False 返回一个空 DataFrame,因为两个传感器ID都出现了不止一次。
就地修改 vs 返回新 DataFrame
默认情况下,drop_duplicates() 返回一个新的 DataFrame,原始 DataFrame 保持不变。设置 inplace=True 会直接修改原始 DataFrame:
df = pd.DataFrame({
'id': [1, 2, 2, 3],
'value': ['a', 'b', 'b', 'c']
})
# Returns new DataFrame (original unchanged)
df_new = df.drop_duplicates()
print(f"Original length: {len(df)}, New length: {len(df_new)}")
# Modifies in place (returns None)
df.drop_duplicates(inplace=True)
print(f"After inplace: {len(df)}")输出:
Original length: 4, New length: 3
After inplace: 3现代 pandas 风格建议避免使用 inplace=True,改用赋值方式(df = df.drop_duplicates())。这使代码更易于阅读和调试,尤其是在链式操作中。
不区分大小写的重复检测
默认情况下,pandas 将 "Alice" 和 "alice" 视为不同的值。要进行不区分大小写的去重,请先对列进行标准化:
df = pd.DataFrame({
'name': ['Alice', 'alice', 'ALICE', 'Bob', 'bob'],
'score': [90, 85, 92, 78, 80]
})
# Create a normalized column for comparison
df['name_lower'] = df['name'].str.lower()
# Deduplicate on the normalized column, keep the first original-case entry
df_deduped = df.drop_duplicates(subset=['name_lower']).drop(columns=['name_lower'])
print(df_deduped)输出:
name score
0 Alice 90
3 Bob 78这种模式在正确识别不区分大小写的重复项的同时,保留了原始的大小写形式。
实际案例:清理客户数据库
这是一个现实的场景。你收到了 CRM 中的客户导出数据,同一客户被不同的销售代表多次录入:
import pandas as pd
customers = pd.DataFrame({
'customer_id': [1001, 1002, 1001, 1003, 1002, 1004],
'name': ['Acme Corp', 'Beta Inc', 'Acme Corp', 'Gamma LLC', 'Beta Inc.', 'Delta Co'],
'email': ['acme@mail.com', 'beta@mail.com', 'acme@mail.com', 'gamma@mail.com', 'beta@mail.com', 'delta@mail.com'],
'revenue': [50000, 30000, 52000, 45000, 30000, 20000],
'last_contact': ['2025-12-01', '2025-11-15', '2026-01-10', '2025-10-20', '2025-11-15', '2026-01-05']
})
print(f"Rows before cleaning: {len(customers)}")
print(f"Duplicate customer_ids: {customers.duplicated(subset=['customer_id']).sum()}")
# Sort by last_contact descending so the most recent entry is first
customers['last_contact'] = pd.to_datetime(customers['last_contact'])
customers = customers.sort_values('last_contact', ascending=False)
# Keep the most recent record for each customer_id
customers_clean = customers.drop_duplicates(subset=['customer_id'], keep='first')
customers_clean = customers_clean.sort_values('customer_id').reset_index(drop=True)
print(f"\nRows after cleaning: {len(customers_clean)}")
print(customers_clean)输出:
Rows before cleaning: 6
Duplicate customer_ids: 2
Rows after cleaning: 4
customer_id name email revenue last_contact
0 1001 Acme Corp acme@mail.com 52000 2026-01-10
1 1002 Beta Inc beta@mail.com 30000 2025-11-15
2 1003 Gamma LLC gamma@mail.com 45000 2025-10-20
3 1004 Delta Co delta@mail.com 20000 2026-01-05这里的关键模式是先排序,再用 keep='first' 去重。通过在去重之前按 last_contact 降序排序,每个客户的最新记录得以保留。
实际案例:网页抓取结果去重
网页爬虫在页面被多次抓取或分页重叠时,通常会产生重复数据:
import pandas as pd
scraped = pd.DataFrame({
'url': [
'https://shop.com/item/101',
'https://shop.com/item/102',
'https://shop.com/item/101',
'https://shop.com/item/103',
'https://shop.com/item/102',
'https://shop.com/item/104',
],
'title': ['Blue Widget', 'Red Gadget', 'Blue Widget', 'Green Gizmo', 'Red Gadget', 'Yellow Thing'],
'price': [19.99, 29.99, 19.99, 39.99, 31.99, 14.99],
'scraped_at': ['2026-02-01', '2026-02-01', '2026-02-02', '2026-02-02', '2026-02-02', '2026-02-02']
})
print(f"Total scraped rows: {len(scraped)}")
print(f"Unique URLs: {scraped['url'].nunique()}")
# Keep the latest scrape for each URL (prices may have changed)
scraped['scraped_at'] = pd.to_datetime(scraped['scraped_at'])
scraped = scraped.sort_values('scraped_at', ascending=False)
products = scraped.drop_duplicates(subset=['url'], keep='first').reset_index(drop=True)
print(f"\nCleaned rows: {len(products)}")
print(products)输出:
Total scraped rows: 6
Unique URLs: 4
Cleaned rows: 4
url title price scraped_at
0 https://shop.com/item/101 Blue Widget 19.99 2026-02-02
1 https://shop.com/item/102 Red Gadget 31.99 2026-02-02
2 https://shop.com/item/103 Green Gizmo 39.99 2026-02-02
3 https://shop.com/item/104 Yellow Thing 14.99 2026-02-02注意 Red Gadget 的价格在两次抓取之间从 29.99 更新到了 31.99。通过保留最新的条目,我们捕获了最新的价格。
drop_duplicates() vs groupby().first():何时使用哪个
两种方法都可以去重数据,但工作方式不同:
import pandas as pd
df = pd.DataFrame({
'user_id': [1, 1, 2, 2, 3],
'action': ['login', 'purchase', 'login', 'login', 'purchase'],
'timestamp': ['2026-01-01', '2026-01-02', '2026-01-01', '2026-01-03', '2026-01-01']
})
# Method 1: drop_duplicates
result1 = df.drop_duplicates(subset=['user_id'], keep='first')
print("drop_duplicates:")
print(result1)
# Method 2: groupby().first()
result2 = df.groupby('user_id').first().reset_index()
print("\ngroupby().first():")
print(result2)输出:
drop_duplicates:
user_id action timestamp
0 1 login 2026-01-01
2 2 login 2026-01-01
4 3 purchase 2026-01-01
groupby().first():
user_id action timestamp
0 1 login 2026-01-01
1 2 login 2026-01-01
2 3 purchase 2026-01-01结果看起来相似,但有重要的区别:
| 特性 | drop_duplicates() | groupby().first() |
|---|---|---|
| 速度 | 简单去重更快 | 由于分组开销更慢 |
| NaN 处理 | 保持 NaN 值不变 | first() 默认跳过 NaN |
| 索引 | 保留原始索引 | 重置为组键 |
| 聚合 | 不能聚合其他列 | 可以与 agg() 结合进行多列汇总 |
| 内存 | 内存使用量更低 | 创建中间 GroupBy 对象 |
| 使用场景 | 删除精确或部分重复 | 在计算聚合的同时去重 |
经验法则: 当你只是想删除重复行时,使用 drop_duplicates()。当你还需要聚合重复行的值时,使用 groupby().first()(或 groupby().agg())-- 例如,在保留第一个名称的同时对重复客户记录的收入求和。
大型 DataFrame 的性能技巧
处理数百万行时,去重可能成为瓶颈。以下是加速的实用方法:
1. 指定 subset 列
检查所有列比只检查关键列更慢:
# Slower: checks every column
df.drop_duplicates()
# Faster: checks only the key column
df.drop_duplicates(subset=['user_id'])2. 使用适当的数据类型
如果字符串列的基数较低,在去重之前将其转换为 category 类型:
df['status'] = df['status'].astype('category')
df['country'] = df['country'].astype('category')
df_clean = df.drop_duplicates(subset=['status', 'country'])3. 仅在必要时在去重前排序
仅为了控制 keep='first' 选择哪一行而对大型 DataFrame 排序会增加大量时间。如果你不在乎哪个重复项存留,就跳过排序。
4. 对超大文件考虑分块处理
对于无法放入内存的文件,按块处理:
chunks = pd.read_csv('large_file.csv', chunksize=100_000)
seen = set()
clean_chunks = []
for chunk in chunks:
chunk = chunk.drop_duplicates(subset=['id'])
new_rows = chunk[~chunk['id'].isin(seen)]
seen.update(new_rows['id'].tolist())
clean_chunks.append(new_rows)
df_clean = pd.concat(clean_chunks, ignore_index=True)5. 基准测试:典型性能
| 行数 | 检查的列 | 时间(约) |
|---|---|---|
| 100K | 全部(10列) | ~15 ms |
| 1M | 全部(10列) | ~150 ms |
| 1M | 1列 | ~50 ms |
| 10M | 1列 | ~500 ms |
这些数字因硬件和数据类型而异,但关键要点是 drop_duplicates() 线性扩展,大多数数据集在一秒内即可处理完成。
用 PyGWalker 探索清理后的数据
删除重复项后,下一步通常是探索清理后的数据集 -- 检查分布、发现异常值,并验证去重是否按预期工作。你可以使用 PyGWalker (opens in a new tab),而不是编写多个 matplotlib 或 seaborn 调用。这是一个开源 Python 库,可以将任何 pandas DataFrame 直接在 Jupyter Notebook 中转换为交互式的类 Tableau 可视化界面。
import pandas as pd
import pygwalker as pyg
# Your cleaned customer data
customers_clean = pd.DataFrame({
'customer_id': [1001, 1002, 1003, 1004],
'name': ['Acme Corp', 'Beta Inc', 'Gamma LLC', 'Delta Co'],
'revenue': [52000, 30000, 45000, 20000],
'region': ['East', 'West', 'East', 'South']
})
# Launch interactive visualization
walker = pyg.walk(customers_clean)使用 PyGWalker,你可以将 region 拖到 x 轴,将 revenue 拖到 y 轴,即刻查看按地区划分的收入分布。你可以通过拖放字段创建柱状图、散点图、直方图和热力图 -- 无需编写图表代码。在去重之后,当你想验证清理逻辑是否产生了合理结果时,它特别有用。
使用
pip install pygwalker安装 PyGWalker,或在 Google Colab (opens in a new tab) 中试用。
常见问题
drop_duplicates() 会修改原始 DataFrame 吗?
不会,默认情况下 drop_duplicates() 返回一个新的 DataFrame,原始 DataFrame 保持不变。要就地修改,请传递 inplace=True,但推荐的做法是使用赋值:df = df.drop_duplicates()。
如何仅基于一列删除重复项?
将列名传递给 subset 参数:df.drop_duplicates(subset=['email'])。这会保留每个唯一电子邮件的第一行,并删除具有相同电子邮件的后续行,无论其他列的差异如何。
duplicated() 和 drop_duplicates() 有什么区别?
duplicated() 返回一个布尔 Series,标记哪些行是重复的(对检查和计数很有用)。drop_duplicates() 返回一个删除了重复行的 DataFrame。先使用 duplicated() 了解数据,然后使用 drop_duplicates() 清理数据。
可以基于条件删除重复项吗?
不能直接用 drop_duplicates() 实现。相反,先按条件列对 DataFrame 排序,然后使用 keep='first' 调用 drop_duplicates()。例如,要保留每个客户收入最高的行:df.sort_values('revenue', ascending=False).drop_duplicates(subset=['customer_id'], keep='first')。
如何处理不区分大小写的重复项?
创建一个临时的小写列,在其上进行去重,然后删除辅助列:df['key'] = df['name'].str.lower(),接着 df.drop_duplicates(subset=['key']).drop(columns=['key'])。这在保留原始大小写的同时正确识别重复项。
总结
删除重复行是任何数据清理工作流程中的基础步骤。pandas 的 drop_duplicates() 方法仅用几个参数就能处理绝大多数去重任务:
- 使用
subset基于特定列而非所有列进行去重。 - 使用
keep='first'或keep='last'控制保留哪个出现;使用keep=False删除所有副本。 - 使用
duplicated()在删除前检查和计算重复项。 - 当需要保留特定行时(例如最新记录),在去重前先排序。
- 对于不区分大小写的去重,先在临时列中标准化为小写。
- 对于大型数据集,限制
subset列并使用适当的数据类型以保持快速性能。
数据清理完成后,PyGWalker (opens in a new tab) 等工具可以让你无需编写图表代码即可可视化地探索结果,帮助你验证去重是否正确工作,并直接进入分析阶段。