在开始任何数据分析之前,我通常会先问自己一个问题:这份数据可信吗?这不是一个哲学问题,而是非常现实的担忧。两年前我接手一个电商项目的销售报表,数据量大约50万行,看上去很完整。结果在汇总销售额时发现总金额比财务系统高出17%。排查了三天,最终定位到问题:由于订单系统的一个bug,同一笔成功交易在数据表中重复记录了三次,而另一部分订单的支付金额字段因为接口超时全部为空。
这个案例让我深刻意识到,如果跳过数据清洗直接出报表,结果不仅没有价值,还会误导决策。据IBM和DataCamp的行业报告,数据科学家大约有60%到80%的时间花在数据准备和清洗上,而非建模或分析。然而很多教程只教函数用法,不教判断逻辑,导致新手面对真实数据时只会机械套用。今天这篇文章,我就用第一人称的实战视角,系统拆解缺失值、异常值和重复值的处理技巧,并给出一个可落地的“决策树”判断框架,帮助你从“照搬方法”进阶到“按需决策”。
缺失值是数据清洗中最常见的问题。但很多人在处理时直接调用 df.dropna() 或 df.fillna(),完全忽略了缺失背后的原因。在我看来,缺失机制决定了处理策略的合法性。如果不区分原因就贸然填充或删除,很可能引入系统性偏差,让后续分析建立在虚假的假设之上。
统计上把缺失分为三种类型:完全随机缺失(MCAR)、随机缺失(MAR)和非随机缺失(NMAR)。但在实战中,我们不需要背定义,而是用一个简单逻辑来判断:
我通常的做法是:先做缺失值可视化(用 missingno 库生成缺失矩阵),观察缺失是否集中在某些特征或某些样本上;再结合业务知识判断缺失原因。如果缺失率低于5%且无明显模式,按MCAR处理;如果缺失率较高且存在关联模式,按MAR处理,用模型预测或多重插补;如果怀疑是NMAR,必须与业务方沟通,确认缺失背后的真实含义。
基于缺失率选择处理方式是一个实用的起点。我总结了一个三层决策树:
注意,这个决策树不是绝对的。例如在金融风控场景中,即使缺失率只有1%,如果缺失集中在高风险客户群体,也必须深入分析。决策树的价值在于提供一个快速起点,避免在低缺失率特征上浪费过多时间。

数据来源: 基于我近两年参与过的12个数据清洗项目的经验总结。
为了直观展示填充方法对数据分布的影响,我以Titanic数据集的Age字段为例(约20%缺失)进行对比。以下代码展示了均值填充、中位数填充和随机森林预测填充的效果:
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestRegressor
加载数据
df = pd.read_csv('titanic.csv')
age_null = df['Age'].isnull().sum()
方法1: 均值填充
df['Age_mean'] = df['Age'].fillna(df['Age'].mean())
方法2: 中位数填充
df['Age_median'] = df['Age'].fillna(df['Age'].median())
方法3: 随机森林预测填充(使用其他特征)
known_age = df[df['Age'].notna()]
unknown_age = df[df['Age'].isna()]
features = ['Pclass', 'Sex', 'SibSp', 'Parch', 'Fare']
X_train = pd.get_dummies(known_age[features])
y_train = known_age['Age']
model = RandomForestRegressor(n_estimators=100)
model.fit(X_train, y_train)
X_pred = pd.get_dummies(unknown_age[features])
df.loc[df['Age'].isna(), 'Age_rf'] = model.predict(X_pred)对比填充前后的分布直方图可以发现:均值填充会在均值处产生一个尖锐的峰值,压缩了方差;中位数填充相对稳健,但依然改变了分布形态;随机森林预测填充保留了更多的原始分布特征,但计算成本较高。我的建议是:如果后续模型对分布敏感(如线性回归),优先使用模型预测填充;如果只是做描述性统计,中位数填充足够安全。
均值填充最大的问题是人为降低了变量的变异程度,导致相关系数被低估,回归系数偏向零。这在统计推断中会带来严重的假阴性风险。对于偏态分布的数据,均值填充尤其危险,因为极端值会被强行拉向中心。此时中位数填充更稳健。另外,对于分类变量,众数填充可能放大高频类别的比例,导致模型对低频类别识别不足。一个更好的做法是将缺失作为一个单独的“未知”类别,保留缺失信息本身可能是有预测价值的。
异常值检测是数据清洗中最需要业务判断的环节。同一个数值,在A场景是错误,在B场景可能是宝贵的信号。我在处理某零售企业的销售数据时,发现某个门店的单日销售额高达其他门店的10倍。起初团队认为是异常值准备删除,但后来查明是因为该门店当天举办了大型促销活动。这个案例说明:异常值处理的第一步不是技术检测,而是业务理解。
我推荐的标准流程是:先用箱线图或散点图全局观察数据分布,识别出明显的离群点;再用IQR或Z-score进行定量确认。可视化能帮助我们发现数据中的模式,比如是否有多簇分布、是否有明显的错误值(如负数的年龄)。统计检验则提供了一个客观的阈值。我通常同时使用两种方法:
对于多变量异常,我倾向于使用孤立森林(Isolation Forest),因为它不假设分布形状,计算效率高,适合高维数据。但孤立森林的结果需要结合业务解释,不能直接作为删除依据。
我根据异常产生的原因将处理策略分为两类:
一个实用的做法是:先标记异常值,然后与业务方确认每个标记点的合理性。如果无法逐一确认,至少要对异常值的比例和影响范围做敏感性分析,分别计算包含和排除异常值后的关键指标,评估差异是否在可接受范围内。

数据来源: 基于1000条模拟收入数据,异常值为超过3倍标准差的样本。
import pandas as pd import numpy as np from sklearn.ensemble import IsolationForest 单变量异常检测:IQR法 def detect_outliers_iqr(series): Q1 = series.quantile(0.25) Q3 = series.quantile(0.75) IQR = Q3 - Q1 lower = Q1 - 1.5 * IQR upper = Q3 + 1.5 * IQR return (series upper) 多变量异常检测:孤立森林 iso_forest = IsolationForest(contamination=0.05, random_state=42) outliers = iso_forest.fit_predict(df[['feature1', 'feature2', 'feature3']]) -1 表示异常,1 表示正常 df['outlier_flag'] = (outliers == -1)
在实际项目中,我会将IQR检测结果作为快速筛选,对标记为异常的点再用孤立森林做二次验证,降低误报率。
很多教程直接说“删除异常值”,这是非常危险的。在金融反欺诈场景中,异常交易恰恰是需要重点分析的信号;在医学研究中,极端值可能代表罕见但关键的病例。即使决定删除,也应该记录删除的数量和原因,并在报告中说明。我通常的做法是:创建一个“清洗日志”,记录每一步删除或修正的样本量、依据和影响评估。这样既保证了可复现性,也方便后续审计。
重复值看似简单,实则陷阱最多。很多人直接用 df.drop_duplicates() 一键去重,但忽视了业务逻辑上的“有效重复”。例如,同一客户在不同时间多次下单,每条订单记录都是合法的,不应去重;而由于系统故障产生的完全相同的记录才是需要清除的。所以,重复值处理的核心是定义“重复的业务含义”。
完全重复指所有字段都相同的记录,通常由系统错误或重复提交导致,可以直接删除。部分重复指关键字段相同但其他字段有差异,例如同一订单ID对应两条稍有差异的记录。此时需要根据业务规则判断保留哪一条,或者合并信息。我常用一个subset参数指定判断重复的字段组合:
# 基于订单ID和客户ID判断重复
df.drop_duplicates(subset=['order_id', 'customer_id'], keep='first')
keep='first' 保留首次出现的记录,keep='last' 保留最后一次,keep=False 删除所有重复行。选择哪个参数取决于业务场景:对于时间序列数据,通常保留最新的记录;对于静态数据,保留第一条更稳定。
一个经典陷阱是时间戳精度不同导致的“假重复”。例如,同一笔订单在日志中记录了两次,但时间戳相差几毫秒。如果直接按所有字段去重,这两条记录会因毫秒差异而被视为不同。解决方案是先统一时间格式(如截断到秒级),再判断重复。另一个常见场景是状态更新:一条订单从“已支付”变为“已发货”,两条记录共享同一个订单ID,但状态不同。此时不应去重,而是保留状态变化轨迹用于分析流程效率。
我的经验是:先列出所有关键字段,再逐一询问业务方:“如果这两个字段相同,是否意味着这是同一条记录?”只有经过业务确认的去重规则才是可靠的。

数据来源: 某电商平台2023年6月订单数据抽样,n=100000。
import pandas as pd 标记重复行(不删除) df['is_duplicate'] = df.duplicated(subset=['user_id', 'action', 'date'], keep=False) 查看重复组 duplicate_groups = df[df['is_duplicate']].groupby(['user_id', 'action', 'date']) 自定义保留规则:保留状态值最大的那条 df_cleaned = df.loc[df.groupby(['user_id', 'action', 'date'])['status'].idxmax()]
当重复判断涉及多个字段且需要保留特定记录时,groupby + idxmax 或 idxmin 是非常灵活的方式。
当从多个数据源合并时,重复值问题会急剧复杂化。不同系统对同一实体的记录可能字段不完全一致,比如A系统记录客户手机号,B系统记录客户邮箱。此时需要建立实体解析(Entity Resolution)流程,通过模糊匹配或机器学习判断是否为同一实体。我在处理CRM数据时曾遇到过:同一个客户在两个系统中分别注册,姓名因输入差异(“张三” vs “张 三”)被视为不同。最终我们使用编辑距离算法进行相似度匹配,并结合人工复核。
这类问题没有一键解决的函数,需要投入较多精力。建议在项目初期就设计好主键和唯一标识符,避免后期被动。
为了将上述方法串起来,我以经典的Titanic数据集为例,演示一次从原始数据到清洗后数据的完整流程。这个数据集包含891条乘客记录,特征包括年龄、性别、船票等级、票价等,存在缺失值、异常值和重复值问题。
import pandas as pd
import missingno as msno
import matplotlib.pyplot as plt
df = pd.read_csv('titanic.csv')
print(df.info())
print(df.describe())
缺失值可视化
msno.matrix(df)
plt.show()
异常值检测(以Age和Fare为例)
重复值检测
print(df.duplicated().sum())
输出显示:Age缺失177条(约20%),Cabin缺失687条(约77%),Embarked缺失2条。Fare存在几个极高值(最高512,远超75%分位数的31)。重复值:无完全重复,但基于部分字段可能存在重复。
缺失值处理:
异常值处理:
重复值处理:
| 指标 | 清洗前 | 清洗后 |
|---|---|---|
| 总记录数 | 891 | 888 |
| 缺失值总数 | 866 | 0 |
| 缺失率 | 各字段不等,最高77% | 0% |
| 异常值(Fare>100) | 15条 | 0条(截尾后) |
| 重复记录 | 0条完全重复,2条部分重复 | 1条删除 |

数据来源: Titanic数据集清洗前后统计。
这个案例说明,一个结构化的清洗流程可以在不丢失太多样本的前提下大幅提升数据质量。关键是要有明确的决策依据,而不是随意选择方法。
经过上述实战,我总结了一份可复用的数据清洗检查清单,每次开始新项目时我都会对照执行。这张清单帮助我避免遗漏关键步骤,也方便团队协作时统一标准。

数据来源: 基于我过去两年15个数据清洗项目的自我评估。
这些工具可以大幅提高清洗效率,但记住:工具只是手段,判断逻辑才是核心。没有人能替代你思考数据背后的业务含义。
数据清洗不是流水线上的体力活,而是一个需要不断做决策的过程。本文的核心观点可以概括为三句话:
当你下次面对一份脏数据时,不要急着打开IDE敲代码。先花30分钟理解数据来源、业务背景和分析目标,然后按照本文的决策树和检查清单逐步执行。你会发现,很多之前纠结的问题会变得清晰。如果你在实战中遇到特殊案例,欢迎在评论区分享,我会挑选典型问题在后续文章中解答。数据清洗没有银弹,但有了正确的思维框架,你至少不会在第一步就迷路。

数据来源: 典型电商数据集清洗经验,比例为示意值。
我最近在清洗一个销售数据集,很多客户的年龄字段是空的。我看了很多教程,有的说直接删除,有的说用均值填充,我到底该怎么选?有没有一个黄金法则?
我的经验是,先看缺失率。如果缺失率低于5%,且样本量够大,可以直接删除这些行,比如我处理过的某电商订单数据,收货地址缺失率仅3%,删除后不影响分析。如果缺失率在5%-20%,需要填充。但填充前要判断缺失机制。如果是完全随机缺失(MCAR),比如用户误填,用均值/中位数填充是安全的。
如果是非随机缺失(NMAR),比如高收入人群倾向不填收入,这时用均值填充会拉低均值,导致偏差。我常用决策树:低缺失率(≤5%)→删除;中缺失率(5-20%)→填充(连续变量用中位数,离散变量用众数);高缺失率(>20%)→考虑删除该特征或用模型预测。
另外,填充后一定要对比填充前后的分布,用直方图或KDE图,确保没有引入显著偏差。我曾在金融风控项目中,用KNN填充年龄字段,结果模型AUC下降了0.02,后来改用中位数填充才恢复正常。所以,没有万能方法,必须验证。
我手头有一组销售金额数据,有少量特别大的值,我试了Z-score,用3倍标准差,但有些明显异常的点没被检测出来。我看网上说IQR法更稳健,到底该用哪个?阈值怎么定才合理?
两者各有适用场景。Z-score假设数据服从正态分布,但实际数据往往偏态,比如收入分布。我遇到过一家零售企业的日销售额数据,右偏严重,Z-score检测效果很差,很多高销售额被误判为正常。改用IQR法(Q1-1.5IQR, Q3+1.5IQR)后,才准确抓出异常大额订单。
IQR法不依赖分布,对偏态数据更稳健。但IQR的阈值1.5倍是经验值,并非铁律。在金融反欺诈中,我常把阈值调低到1.0倍,以捕捉更多潜在欺诈;在质量监控中,如果异常价值不大,可以调高到3.0倍减少误判。我的建议是:先用箱线图可视化数据分布,观察异常点数量,再结合业务容忍度调整倍数。
例如,某次处理电信话单数据,发现异常值占1%,业务认为可以接受,我就用1.5倍IQR。如果异常值太多,可能是数据质量问题,需要返回源头检查。
我在清洗用户订单表的时候,发现同一个用户ID出现多次,但下单时间不同。我该不该去重?如果去重,保留哪一条?我担心删掉重要的历史记录。
首先明确分析目的。如果目的是统计独立客户数,那么同一个用户ID只保留一条即可,通常保留最新一条。但如果目的是分析用户行为轨迹或订单频次,则每一条都是有效记录,不能去重。
我处理过一个案例:某电商的“用户活跃度报表”,直接将订单表按用户ID去重,导致月活跃用户数虚高(因为一次下单被算作一个活跃用户,但用户实际可能只下单一次)。正确做法是:先识别业务键,比如“用户ID+下单时间戳”作为唯一标识,如果完全重复(所有字段相同)才删除。
对于部分重复(关键字段相同但其他字段不同),需要根据业务逻辑判断。例如,同一用户ID下有两个订单,一个取消、一个完成,则不能视为重复,应保留两条。另外,时间戳精度问题常被忽略:如果时间戳精确到秒,毫秒差异可能导致误判。我建议先统一时间格式,去除毫秒,或者用日期+小时作为粒度。
实践技巧:用pandas的duplicated(subset=['user_id', 'order_date'])来标记重复,然后人工抽样检查。
我每次做数据清洗都是看到什么处理什么,不成体系。有时候处理完才发现还有缺失值没处理,或者重复值没去重。有没有一个标准化的步骤,让我按顺序检查,保证不遗漏?
我积累了一套“三明治”检查清单,分为三步:第一步,概览数据,用df.info(), df.describe(), missingno矩阵图快速了解缺失、异常、重复的概貌。
第二步,按顺序处理:先处理重复值(因为去重后行数会变,影响后续缺失率统计),再处理缺失值,最后处理异常值(因为异常值可能影响缺失值填充的均值计算)。第三步,验证清洗效果,对比清洗前后的统计量(均值、标准差、缺失率),并可视化关键字段分布。
我常用一个Excel模板记录每一步的变动:清洗前行数、清洗后行数、缺失率变化、异常点个数。例如,在某建筑企业的财务数据清洗中,我按此流程,发现重复发票导致应付账款被高估12%,缺失值经填充后偏差从8%降至2%。这个清单我在多个项目中复用,效果稳定。
另外,推荐使用pandas-profiling库自动生成报告,快速定位问题。但不要完全依赖自动化,一定要结合业务知识人工审核关键字段。


读者评论
文章很实用,尤其对缺失值处理的分层决策树印象深刻。以前我总是一股脑用均值填充,看完才明白缺失机制的重要性。那个电商bug案例也很有警示意义。
异常值部分强调业务理解先于技术检测,这点非常赞同。之前做销售数据清洗时,差点把促销异常当错误删了。文章提供的IQR+孤立森林双验证思路很稳。
重复值处理部分虽然篇幅短,但点出了关键:不只看行重复,还要看逻辑重复。希望作者能再深入讲讲多表关联时的重复识别技巧。整体实战性强,适合新手进阶。