数据是乱的,但咱们手里的活不能乱。真正在业务里跑Pandas,最先遇到的不是那些花哨算法,而是重复行、异常值这种“脏东西”。今天咱们就静下心来,把这两个高频问题掰开揉碎,讲一套能直接用到生产环境的标准流程。
一、应用场景:什么时候你需要清洗数据
先说重复值。最常见的就是从数据库导出的明细表,因为一次查询重试、多个接口拼接、甚至上游表本身就有重复键,导致同样一行数据出现好几次。如果你不去重,后面的汇总统计、透视表、建模训练全都会偏。比如算订单金额总和,重复的订单会被算两遍,老板一看数字翻倍,那就尴尬了。
再说异常值。异常值不一定是错误,也可能是真实的极端情况。比如电商平台突然来了一笔十万元的大订单,这在统计上可能把平均值拉得很高,让你误以为整体客单价提升了。又比如传感器数据里某个点突变成负值,那基本就是采集故障。异常值处理得好不好,直接影响你对业务规律的理解。生产环境里,清洗这一步做得扎实,后面分析才靠谱。
1.1 重复值的典型场景
重复值分两种:一种是整行完全一样,另一种是某几个关键列重复,比如订单号一样但收货地址不同。实际业务里,第二种更常见。你需要根据业务规则决定,到底按哪几列判断重复。
1.2 异常值的典型场景
异常值也分两类:一类是明显违反常识的错误值,比如年龄是负数、温度是1000度;另一类是统计意义上的离群点,比如大部分用户下单金额在几十到几百元,突然冒出一个一万多的。前者要直接修正或删除,后者要结合业务判断是否保留。
二、处理重复值:基础但容易踩坑
去重在Pandas里就两个方法:duplicated()用来找重复,drop_duplicates()用来删重复。别看它们简单,用不好会出大问题。
2.1 查找重复:duplicated()
duplicated()会为每一行返回一个布尔值,True表示这一行是重复出现的行,而且默认情况下第一个出现的不算重复,后面的才算。咱们先看一段代码。
# 技术栈:Python + Pandas
import pandas as pd
# 构造一份订单数据,故意放几行重复
df = pd.DataFrame({
'订单号': ['A001', 'A001', 'A002', 'A003', 'A003', 'A003'],
'客户': ['张三', '张三', '李四', '王五', '王五', '王五'],
'金额': [100, 100, 200, 300, 300, 300]
})
# 查看哪些行是重复行
print(df.duplicated())
# 输出结果:
# 0 False
# 1 True
# 2 False
# 3 False
# 4 True
# 5 True
# dtype: bool
注意,第0行和第1行完全一样,duplicated()把第0行标记为False,第1行标记为True。第3、4、5三行重复,第3行是第一个出现,标记False,后面两行是True。这样设计的目的是让你能通过布尔筛选,把重复的行抓出来看看。
2.2 删除重复:drop_duplicates() 的用法细节
drop_duplicates()是真正干活的。它有3个关键参数:subset、keep、inplace。
subset:指定按哪几列判断重复。不传的话,必须整行所有列都一样才算重复。keep:控制保留哪一个。-keep='first'是默认值,保留第一个;keep='last'保留最后一个;keep=False一个都不留。inplace:是否在原DataFrame上直接改。生产环境我强烈建议不要用inplace=True,因为不经意在函数里把原数据改了,后面的逻辑全会被影响。还是用返回值更好。
咱们用真实一点的例子来演示。
# 技术栈:Python + Pandas
import pandas as pd
df = pd.DataFrame({
'订单号': ['A001', 'A001', 'A002', 'A003', 'A003'],
'金额': [100, 200, 200, 300, 500],
'日期': ['2025-01-01', '2025-01-01', '2025-01-02', '2025-01-03', '2025-01-03']
})
# 只按订单号去重,保留第一个出现的记录
df_unique = df.drop_duplicates(subset=['订单号'], keep='first')
print(df_unique)
# 输出:
# 订单号 金额 日期
# 0 A001 100 2025-01-01
# 2 A002 200 2025-01-02
# 3 A003 300 2025-01-03
这里要注意,A001订单出现了两次,金额不一样。按订单号去重后,保留了第一行,第二行被干掉了。如果业务上要求保留最新的一条,那就得用keep='last'。所以去重前先想清楚:哪几列能唯一标识一条记录,以及该保留哪一条。
2.3 实际案例:订单表去重
生产环境中,订单表往往有很多列,咱们做的是“按业务主键去重,但保留各列最新值”。比如同一订单被重复同步,但收货地址可能被修改过。这时直接用drop_duplicates就不够灵活了,需要先排序,再去重。
# 技术栈:Python + Pandas
import pandas as pd
# 模拟上游同步导致重复
df = pd.DataFrame({
'订单号': ['SO001', 'SO001', 'SO002', 'SO002'],
'状态': ['已支付', '已发货', '已下单', '已关闭'],
'更新时间': ['2025-06-01 08:00:00', '2025-06-01 12:30:00',
'2025-06-02 09:00:00', '2025-06-02 18:00:00']
})
# 按订单号和更新时间排序,让最新的记录排在后面
df_sorted = df.sort_values('更新时间')
# 按订单号去重,保留最后一个(即最新状态)
df_deduplicated = df_sorted.drop_duplicates(subset=['订单号'], keep='last')
print(df_deduplicated)
# 输出:
# 订单号 状态 更新时间
# 1 SO001 已发货 2025-06-01 12:30:00
# 3 SO002 已关闭 2025-06-02 18:00:00
这就是生产里最常见的“保留最新状态”的去重姿势。
三、处理异常值:别急着删
遇到异常值,第一反应不应该是删行,而是先搞清楚它为什么异常。删数据是最简单,但也最容易把有效信息丢掉的办法。通常会按这个顺序走:先发现,再判断,最后处理。
3.1 怎么发现异常值
最直观的方法是看统计描述。
# 技术栈:Python + Pandas
import pandas as pd
df = pd.DataFrame({'订单金额': [50, 80, 120, 150, 100, 200, 10000, -30, 90, 110]})
# 查看均值、标准差、四分位数
print(df.describe())
# 输出:
# 订单金额
# count 10.000000
# mean 1057.000000
# std 3132.848085
# min -30.000000
# 25% 85.000000
# 50% 105.000000
# 75% 162.500000
# max 10000.000000
一眼就能看出两个问题:最小值是负数,肯定不对;最大值10000,比其他数据大了一个量级。这时候就能用统计方法把它们标出来。
3.2 用IQR方法识别异常值
IQR(四分位距)是常用的一种方法。它认为超出Q1 - 1.5 * IQR和Q3 + 1.5 * IQR范围的点是异常值。IQR的好处是抗干扰,不受极端值影响,所以很适合初筛。
# 技术栈:Python + Pandas
import pandas as pd
df = pd.DataFrame({'订单金额': [50, 80, 120, 150, 100, 200, 10000, -30, 90, 110]})
# 计算四分位数
q1 = df['订单金额'].quantile(0.25)
q3 = df['订单金额'].quantile(0.75)
iqr = q3 - q1
# 确定上下限
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
print(f'正常范围:{lower:.1f} ~ {upper:.1f}')
# 输出:正常范围: -6.2 ~ 253.7
# 标记异常值
df['是否异常'] = (df['订单金额'] < lower) | (df['订单金额'] > upper)
print(df)
# 输出:
# 订单金额 是否异常
# 0 50 False
# 1 80 False
# 2 120 False
# 3 150 False
# 4 100 False
# 5 200 False
# 6 10000 True
# 7 -30 True
# 8 90 False
# 9 110 False
这样一下就找出来了,-30肯定不合理,10000在这个数据里也显得异常。但注意,IQR只是一个统计规则,不是说异常就一定错,只是提示你“这数据需要被仔细看”。
3.3 用Z-Score识别异常值
Z-Score是另一个常用方法,它算的是每个点和均值的距离有几个标准差。一般绝对值超过3就认为是异常。Z-Score适合数据比较对称、接近正态分布的场景。如果数据本身偏态很强,IQR会更稳妥。
# 技术栈:Python + Pandas
import pandas as pd
df = pd.DataFrame({'订单金额': [50, 80, 120, 150, 100, 200, 250, 180, 90, 110]})
# 计算Z-Score
mean = df['订单金额'].mean()
std = df['订单金额'].std()
df['z_score'] = (df['订单金额'] - mean) / std
# 标记异常,阈值取3
df['是否异常'] = df['z_score'].abs() > 3
print(df)
# 输出:
# 订单金额 z_score 是否异常
# 0 50 -1.173487 False
# 1 80 -0.474090 False
# 2 120 0.225307 False
# 3 150 0.735111 False
# 4 100 -0.188379 False
# 5 200 1.629033 False
# 6 250 2.618531 False
# 7 180 1.247128 False
# 8 90 -0.331234 False
# 9 110 -0.045523 False
这段数据没有超过3的,所以没有异常。如果某个值非常大,Z-Score就会跑到3以上。这个方法的缺点就是均值、标准差本身会受极端值影响,所以有时候异常值反而把自己的分数拉低了。生产环境可以把这两种方法结合着用。
3.4 异常值的处理策略:修、换、删
找到异常值之后,有三个方向:修正、替换、删除。
- 修正:如果是明显错误,比如负金额、日期格式不对,按照业务规则改成合理的值。比如金额小于0,直接用0替代。
- 替换:用同一列的均值、中位数或边界值替换。比如某个指标因为传感器抖动出现了一个999,可以把它替换成正常范围的上限值。用
clip方法最方便。 - 删除:如果异常值占比极小,而且确实是录入错误,对整体分析没帮助,那就删掉。但删之前一定要留个备份,或者记下删除的行数。
先看看替换示例。
# 技术栈:Python + Pandas
import pandas as pd
df = pd.DataFrame({'订单金额': [50, 80, 120, 150, 100, 200, 10000, -30, 90, 110]})
# 设置合理上下限,比如3%到97%分位数
lower = df['订单金额'].quantile(0.03)
upper = df['订单金额'].quantile(0.97)
# 用clip把超过边界的值截断到边界
df['订单金额_清理后'] = df['订单金额'].clip(lower=lower, upper=upper)
print(df)
# 输出:
# 订单金额 订单金额_清理后
# 0 50 50.0
# 1 80 80.0
# 2 120 120.0
# 3 150 150.0
# 4 100 100.0
# 5 200 200.0
# 6 10000 2170.0
# 7 -30 17.0
# 8 90 90.0
# 9 110 110.0
看到没,10000被压到了2170,-30被抬到了17。这样做比直接删行稳,保留了“这行数据存在”这个事实,只是把数值拉回合理范围。
四、生产级流程:把规则打包成函数
前面都是碎片操作,生产环境需要的是可复用、可维护的流程。我们把这些步骤整合成一个清洗函数,输入一个DataFrame,输出一个清理后的DataFrame,并且每一步都有日志记录。
4.1 设计清洗管线
一个标准的清洗流程应该是这样的:先处理重复值,再处理异常值。因为先去重可以减少后续计算量,而且重复行里的异常值可能会干扰分位数计算,先去掉更干净。
4.2 完整示例代码
下面是一个比较完整的清洗函数,它支持传入要去重的列名、要检查异常值的数值列、以及处理方式。
# 技术栈:Python + Pandas
import pandas as pd
import numpy as np
def data_cleaning_pipeline(df, key_cols=None, numeric_cols=None, abnormal_method='iqr'):
"""
生产级数据清洗流程
参数:
df: DataFrame 原始数据
key_cols: list 按哪些列去重
numeric_cols: list 需要做异常值处理的数值列
abnormal_method: str 异常值检测方法,可选 'iqr' 或 'zscore'
"""
# 复制一份,避免修改原数据
clean_df = df.copy()
# ========== 第一步:去重 ==========
before_dup = len(clean_df)
if key_cols is not None:
clean_df = clean_df.drop_duplicates(subset=key_cols, keep='first')
else:
clean_df = clean_df.drop_duplicates(keep='first')
after_dup = len(clean_df)
print(f'去重复:{before_dup}行 -> {after_dup}行,删除{before_dup - after_dup}行')
# ========== 第二步:异常值处理 ==========
if numeric_cols is not None:
for col in numeric_cols:
# 先去掉该列中的空值,空值单独处理
series = clean_df[col].dropna()
if abnormal_method == 'iqr':
q1 = series.quantile(0.25)
q3 = series.quantile(0.75)
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
mask = (clean_df[col] < lower) | (clean_df[col] > upper)
else: # z-score
mean = series.mean()
std = series.std()
# 防止标准差为0
if std == 0:
print(f'列 {col} 的标准差为0,跳过异常检测')
continue
z = (clean_df[col] - mean) / std
mask = z.abs() > 3
count = mask.sum()
if count > 0:
print(f'列 {col} 发现 {count} 个异常值,使用上下限截断处理')
# 这里统一采用截断方式,避免删除过多行
lower_val = series.quantile(0.05)
upper_val = series.quantile(0.95)
clean_df.loc[mask, col] = clean_df.loc[mask, col].clip(lower=lower_val, upper=upper_val)
return clean_df
# 构造一份乱糟糟的数据
df_raw = pd.DataFrame({
'订单号': ['A001', 'A001', 'A002', 'A003', 'A003', 'A004', 'A005'],
'金额': [100, 100, 250, -50, 300, 99999, 120],
'数量': [2, 2, 3, 1, 3, 5, 4]
})
# 执行清洗
df_clean = data_cleaning_pipeline(
df_raw,
key_cols=['订单号'],
numeric_cols=['金额', '数量']
)
# 清洗结果
print(df_clean)
这段代码在控制台会输出类似这样的结果:
去重复:7行 -> 5行,删除2行
列 金额 发现 1 个异常值,使用上下限截断处理
然后打印的DataFrame里,-50和99999会被截断到5%和95%分位数附近。这样整个流程就算跑通了。以后面对新数据,只需要调用同一个函数,传入不同参数就行。
五、技术优缺点与注意事项
5.1 优点
这套流程最大的优点就是稳。重复值处理用drop_duplicates,简单直接,性能也好。异常值用IQR或Z-Score作为初筛工具,不需要机器学习的复杂门槛。把所有逻辑封装成函数后,很容易接入定时任务或调度平台,比如每天凌晨自动清洗一次昨天的数据。
另外,Pandas处理这类问题有天然优势:代码短、写法直观、内置方法多。duplicated、clip、quantile这些方法都是经过大规模验证的,不容易出错。比起自己写循环去判断,效率高得多。
5.2 缺点和坑
再好的工具也有坑。drop_duplicates最大的坑是默认按所有列判断重复,你不指定subset,很可能一条重复都去不掉。因为两个订单的主键一样,但时间戳差了几秒,整行就不完全相等。
异常值检测的坑更多。IQR假设数据分布比较稳定,如果数据本身是偏态分布,比如收入数据,那正常的高收入人群可能也会被标成异常。Z-Score又怕极端值,极端值会把均值和标准差拉偏,导致检测失效。所以在生产环境里,不能只依赖某一种方法,最好先画个箱线图或者直方图看看分布。
clip方法虽然好用,但要注意它只截断数值,不会改变该行其他列。如果一个订单金额异常,但数量正常,截断金额后,这个订单总额可能和数量不匹配,这时需要结合业务再调整。
5.3 注意事项
第一,清洗前一定要备份原始数据。哪怕你有清洗函数,也得把原始数据留一份,否则出了问题没法追溯。
第二,每一步都要留日志。删了多少行,改了几个值,全部记录下来。生产环境最怕“默默无闻”的数据变化,出了问题排查起来很痛苦。
第三,空值要单独处理。不要和异常值混在一起。空值的处理方式可能是填充、删除或向前向后填充,和异常值逻辑完全不同。所以上面的示例里,先用dropna把空值剔除了,避免干扰分位数计算。
第四,阈值不是死的。IQR的1.5倍系数,Z-Score的3倍阈值,这些只是经验值。你可以根据业务调整,比如金额类数据,可能超过5倍标准差才算异常。一定要结合业务来定。
六、文章总结
重复值和异常值是Pandas数据清洗面对的最常见问题,但它们并不可怕。去重用duplicated和drop_duplicates,重点在于选对subset和keep;异常值用IQR或Z-Score识别,再用clip截断或删除。生产环境里,一定要把这些操作封装成函数,做好日志和备份,让清洗流程标准化、可复用。数据干净了,下游的分析和建模才敢放手去做。
评论
围绕“Pandas数据清洗高频问题:处理重复值与异常值的生产级标准流程”参与讨论