Skip to content
话题
Pandas
Pandas Drop Duplicates:如何在Python中删除重复行

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 删除所有副本
inplaceboolFalse如果为 True,就地修改 DataFrame 并返回 None
ignore_indexboolFalse如果为 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行中,尽管 namesignup_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
1M1列~50 ms
10M1列~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) 等工具可以让你无需编写图表代码即可可视化地探索结果,帮助你验证去重是否正确工作,并直接进入分析阶段。

📚