机器学习数据清洗实战:从缺失值到异常值处理

发布时间:2026/9/28 11:20:18
机器学习数据清洗实战:从缺失值到异常值处理 1. 为什么从项目入手的第一步就是跟脏数据搏斗去年我给一个朋友做电影推荐系统的小项目拿到数据后的第一反应不是急着选模型而是跟一份乱糟糟的表格整整对峙了两天。数据集看起来像模像样有导演、有票房、有评分可真要往模型里喂的时候问题全冒出来了评分列缺了快一成导演字段一堆未知还有好几条完全重复的记录。机器学习项目里有个几乎人人都听过、但真到用的时候才懂的规律——数据预处理的质量直接决定模型的性能上限。模型再先进喂进去的是垃圾出来的还是垃圾。这也是我为什么坚持用从项目入手中学习机器学习的方式来讲课。市面上的教材和课程大多从数学推导讲起线性回归、逻辑回归、损失函数……讲完一大轮真正落地一个项目的时候大家最先被卡住的往往是数据怎么整理。所以这个系列我就直接用一个大家都见过的场景——电影数据分类——走一遍机器学习的完整流程。这一篇先讲数据预处理的上半场数据清洗。至于特征工程、数据变换这些留给下那一篇。1.1 模型只负责后半场前半场的数据质量才是胜负手说个有点反直觉的事实在真实项目里模型训练和调参所占的时间往往只占 20% 左右剩下 80% 的时间全都花在数据处理上。我前几年在公司做用户流失预测项目时多轮跑下来发现真正让模型分数掉下去的不是哪个算法选得不对而是用户年龄字段里那些0和-1——它们在业务上根本不是真实年龄模型却把它们当成正常数值去算。所以在动手之前先建立一条基本认知数据预处理不是一个可有可无的步骤而是项目能否走通的地基。这一篇我聚焦的数据清洗主要解决四类问题——缺失值、重复值、异常值、还有数据类型不一致。把这些问题处理干净后面做特征工程和建模时才不会莫名其妙地报错或者跑出一个看似正常、实际上荒谬的结果。1.2 我用来贯穿全篇的示例数据一份电影数据集为了不让你看完就忘我用一个具体的例子串起全文。假设我们现在拿到了一份电影数据集包含以下字段movie_id电影编号主键理论上唯一title电影名称director导演genre类型动作、喜剧、科幻等rating用户评分1-10 分有些缺失box_office票房万元有些明显不合理比如负数或者异常大release_year上映年份数据大概长这样movie_idtitledirectorgenreratingbox_officerelease_year1电影A张导动作8.53200020152电影B未知喜剧缺失-50020183电影C李导动作6.0320002015.....................你没看错movie_id为 1 和 3 的电影票房完全一样很可能就是同一条数据录了两次box_office出现负数明显是录入错误director有未知rating有缺失。这些就是我们这一篇要解决的问题。1.3 先说清楚这一篇要处理的是数据清洗这一半机器学习的数据预处理通常可以拆成两大块数据清洗和特征工程。数据清洗解决的是数据本身能不能信的问题——缺失、重复、异常、格式错乱特征工程解决的是数据怎么用更好的问题——构造新特征、标准化、编码、降维。我故意把这两块分开讲是因为很多新手会把它混在一起。一上来又是标准化又是归一化结果底层数据还有一堆缺失值处理完的特征也是白算。这一篇作为上只讲数据清洗。等下一篇下再讲怎么把这份清洗干净的数据变成模型能吃的特征。这样一步一步来你才知道每个环节卡在哪里出了问题也知道回哪个环节去查。2. 用最小的环境跑起来先把数据看透很多人学机器学习的第一步是纠结环境PyTorch 还是 TensorFlow装 GPU 版还是 CPU 版其实在数据预处理阶段这些都不重要。我自己的经验是一份 Python 环境加 pandas 和 numpy再加一个 Jupyter Notebook就足够完成八成以上的清洗工作。2.1 环境准备一顿中文输入法的功夫如果你已经装好了 Anaconda那直接打开终端或者命令行确认一下几个关键库有没有装齐python --version pip install pandas numpy jupyter装完之后新建一个 Jupyter Notebook先把数据集读进来。我习惯用 pandas 的read_csv读取文件名假设叫movies.csvimport pandas as pd import numpy as np df pd.read_csv(movies.csv) print(df.shape)这里先看shape输出的就是数据的行数和列数。这个习惯我从一开始就养成了——不管什么数据先看一眼规模心里有个底。几百行和几百万行的处理思路完全不一样。2.2 四个函数把数据体检做完整个清洗流程中我每次都会先做一遍数据体检。体检的核心就是四个动作可以写在一个 cell 里一口气跑完# 1. 看前几行确认字段是否和预期一致 df.head(10) # 2. 看每个列的数据类型和非空值数量 df.info() # 3. 看数值列的统计描述均值、最大最小、分位数 df.describe() # 4. 看每列缺失值数量 df.isnull().sum()这几个函数每一个都有它存在的理由head(10)解决的是眼见为实的问题。很多时候字段名起得模棱两可不看真实数据根本猜不出它的含义。info()告诉你每个字段的类型。我见过大量因为类型不对导致的奇葩报错比如把年份存成了字符串后面一排序就乱套。describe()是发现异常值最快的方法。min和max一列出来诸如负数票房、超过合理范围的评分立刻就现形了。isnull().sum()是给缺失值计数。别相信直觉一定要精确到个数才知道后面该怎么处理。这一步做完你手里应该就有了一张问题清单。以我们的电影数据为例体检结果大概是rating有 12 个缺失值director有 5 个未知box_office有 2 个负数全表有 2 行完全重复release_year是int其他列看起来还算正常2.3 体检之后我习惯列一张待处理清单别急着动手清理。我强烈建议在 Jupyter Notebook 里先新建一个 Markdown cell把问题一条条列出来。比如处理rating缺失先判断是否为随机缺失再决定删除还是填充处理director未知考虑用众数填充还是单独标记处理box_office负数查看原始数据来源疑似录入错误删除删除完全重复的行为什么非要写这么一张清单因为我记性不好而且数据清洗的动作一做多就会乱。你想想几十个操作下来谁还记得哪一列做过什么有了清单每一步都有据可查后面代码跑错了也知道回去改哪一段。很多工程团队要求的数据清洗报告本质就是这张清单的延伸。3. 缺失值处理先想清楚为什么缺再决定怎么处理缺失值是数据清洗中最常见的拦路虎也是最容易被错误处理的问题。因为很多人一上来就喜欢直接用dropna()把所有包含空值的行全删了但如果缺失比例比较高删除会让样本量大幅缩水甚至让你的数据失去代表性。3.1 一个我踩过的坑拿到数据直接dropna我自己最早做项目时就是这么干的。数据集中有一列用户收入大约 30% 是空的我想都没想一行dropna()下去样本量直接砍掉三分之一。结果后面模型训练出来预测结果明显偏向高收入群体因为在删掉缺失行之后留下来的样本分布完全不平衡了。后来我才真正理解一个基本原则遇到缺失值先问为什么缺失再问怎么办。缺失一般有三种成因随机缺失数据录入时因为人为疏漏或系统故障导致和业务本身没有关系。比如有的用户忘了填评分这就是随机缺失。这种情况可以删也可以用合理值填充。非随机缺失缺失本身带有信息缺失的人和数据完整的人是两个群体。比如票房缺失的恰好都是没上映的影片那缺失本身就意味着未上映。这种情况不要轻易删最好单独标记成一个新类别。业务上根本不存在比如一部电影还没有上映它就没有票房累计值这种缺失是天然合理的在分析时要和应该填却没填的缺失区分开。这三种情况的处理策略完全不同所以第一步永远是做判断而不是直接写代码。3.2 判断缺失的三种情况对应三种策略在项目里我一般是这样决策的缺失比例小于 5% 且为随机缺失直接删除含缺失值的行对整体影响不大。缺失比例在 5%-20% 之间优先考虑填充。数值列用中位数或均值填充类别列用众数填充。优先用中位数是因为中位数对异常值不敏感比如我们这组数据里box_office如果有异常大的值均值会被拉高填充结果就失真了。缺失比例超过 20% 或者缺失本身有意义我倾向于保留这一列但把缺失单独标记出来比如director_unknown这个布尔特征。这样模型可以学到导演未知本身可能意味着什么而不是强行猜一个值填进去。听起来很复杂用一句话总结就是能少删就少删能保留信息就保留信息。数据是项目的资产删一行少一行填充的本质是尽力保住这份资产的价值。3.3 实操用pandas完成删除与填充我们以电影数据为例写一段可复用的清洗代码# 看一眼每列的缺失数量 print(df.isnull().sum()) # 情况一box_office 缺失比例很小2%左右且为随机缺失直接删除 df df.dropna(subset[box_office]) # 情况二rating 缺失约 8%用中位数填充 rating_median df[rating].median() df[rating].fillna(rating_median, inplaceTrue) # 情况三director 为类别字段且缺失不再少数单独标记 df[director_unknown] df[director].isna().astype(int)注意第三段代码我没有直接把未知填成某个导演而是新增了一列director_unknown。这一列在后续建模时会是很有用的特征因为导演未知往往意味着小众电影或者资料不全的电影评分行为可能和其他电影有差异。这就是把缺失本身变成信息的思路。3.4 缺失值处理策略速查我把决策过程整理成一张表方便你随时对照缺失比例缺失类型建议策略代码示例5%随机缺失直接删除行dropna(subset[col])5%-20%随机缺失中位数/众数填充fillna(df[col].median())5%-20%非随机缺失单独标记填充df[col_unknown]...同时可保留其他分析20%无明确业务意义考虑删除整列或单独标记drop(columns[col])或标记后保留这张表不是绝对的规则但它能帮你在忙乱中快速找到一个不坏的出发点。项目的特殊性永远优先于通用规则遇到真实业务数据一定要结合业务背景再判断。4. 重复值与异常值模型不报错但结果会骗你如果说缺失值的问题一眼就能看见那重复值和异常值就属于藏在暗处的坑了。模型不会因为数据里有重复值或者异常值就报错它只会默默地把这些错误信息学进去然后给你一个看起来正常但实际上是错的结果。这也是数据清洗最危险的地方——错误不报错而是静默污染你的模型。4.1 重复值也不是无脑去重subet和keep要看业务重复值听起来简单直接drop_duplicates()不就好了但如果你不假思索地用默认参数很可能会误删有效记录。先看清楚 pandas 的drop_duplicates里几个参数的含义subset判断重复时依据哪些列。默认是全部列也就是说只有整行完全一样才算重复。keep保留哪一条记录。first保留第一条last保留最后一条False是全部删除。回到我们的电影数据如果两条记录的title、director、rating完全一样那它们大概率是同一部电影录了两次这时候用全部列判断重复是合理的# 整行完全重复时保留第一条即可 df df.drop_duplicates()但如果你只看movie_id这一列那就有讲究了。因为movie_id是主键理论上绝对不可能重复。如果发现movie_id重复但其他字段不同这说明是数据录入时分配错了编号而不是简单删除一条。这时候你要做的是判断两条记录中哪一条是对的或者手动修正。再举一个更贴近业务的例子如果你的数据是用户-商品-购买行为同一个用户购买了同一个商品两次从订单表的角度这不是重复记录但从分析用户偏好的角度你可能希望去重。所以重复与否永远要问一句在业务上这两条数据是否代表同一件事4.2 异常值识别先用describe和画图再谈3σ异常值我们通常分两类看第一类是数据录入错误。比如box_office出现负数评分出现 11 分年份出现 2999 年。这类错误完全不合常理处理手段就是删除或者修正没有商量的余地。第二类是真实但极端的值。比如某部电影的票房特别高像一些爆款电影的票房可能是普通电影的几十倍。这类值不是错误但在数值计算时会拉偏均值、放大方差让模型对这个特征产生误判。怎么找出这些异常值最简单的方法分两步第一步用describe()看数值字段的min、max、四分位数。我们前面已经做过了。如果某个字段的max明显超出合理范围或者min是个负数那它就值得警惕。第二步画箱线图boxplot。箱线图的好处是直观一眼就能看出哪些点被判定为离群点import matplotlib.pyplot as plt plt.figure(figsize(8, 4)) df[[box_office, rating]].boxplot() plt.show()箱线图里那些在须须之外的散点就是我们要关注的候选异常值。如果数据分布近似正态还可以用3σ 原则——超过均值加减三个标准差的值视为异常mean df[box_office].mean() std df[box_office].std() upper mean 3 * std lower mean - 3 * std df df[(df[box_office] lower) (df[box_office] upper)]这个做法在入门阶段非常实用但它有个前提就是数据要近似正态分布。如果数据本身严重偏态比如票房这种长尾分布直接用均值加三倍标准差会误杀很多真实数据点。所以千万不要机械套用公式要用业务常识和图画结合着看。4.3 判断异常值的两条原则遇到一个可疑的异常值我给自己定了两条判断原则原则一看它是否违反客观规律。票房不可能为负数评分不可能超过满分。这类值直接视为错误数据删除或修正。原则二看它对整体分析是否会产生不成比例的影响。比如全表票房均值是 3000 万中间混了一个 30 亿的爆款它在describe()中可能直接把均值拉到 5000 万以上。即使它是真实数据在后续建模里如果不做处理模型会为了拟合这个极端值而扭曲整体规律。这时候可以考虑对该字段做对数变换放到下的特征处理里细说或者暂时剔除这个极值样本重新建模对比效果。异常值处理完再describe()一遍你会发现数值分布明显更合理了。这个处理-复查的循环才是数据清洗的正确节奏。5. 清洗留痕让每一步操作都能追溯在写代码的阶段我们会做很多次操作删行、填充、去重、改类型。每操作一次数据就变一次。跑完半小时之后你还能记得每一列最初是什么样吗你还能向别人解释清楚为什么最终数据集里有 986 行而不是原来的 1000 行吗这就是清洗日志存在的意义。5.1 为什么清洗日志在项目里这么重要你可能觉得这是工程团队才需要的流程自己做个练习项目用不着。但我在实际接手过几个半途而废的项目后深刻体会到没有留痕的数据清洗跟没有存档的游戏一样让人崩溃。你可能连着两天处理了几千行数据然后某天发现一开始的删除逻辑判断错了想回退结果发现自己根本不记得当时删了哪些数据。只能重新下载原始数据、重新跑一遍白白浪费时间。如果你将来要跟别人协作或者隔一段时间回过头来看自己的代码一份清洗日志会让整个项目的可读性提升一个档次。它本质上就是一份数据手术记录做了什么手术、为什么要做、影响了哪些样本。5.2 一个简版的清洗日志实现我的做法很简单在代码开头定义一个字典然后在每个清洗动作之后追加一条记录cleaning_log [] # 记录删除缺失票房记录 cleaning_log.append({ action: delete_rows, column: box_office, reason: missing_ratio_under_5_percent_random, rows_removed: df[box_office].isna().sum() }) df df.dropna(subset[box_office]) # 记录评分中位数填充 cleaning_log.append({ action: fillna_median, column: rating, reason: missing_ratio_around_8_percent_random, filled_value: rating_median }) df[rating].fillna(rating_median, inplaceTrue)中间不管做了多少操作最后我可以一次性输出这份日志log_df pd.DataFrame(cleaning_log) log_df.head(10)这样任何人看到这个项目的第一眼就知道数据是怎么从原始状态一步步变成最终状态的。我自己项目做到后来越发觉得这个习惯比任何花哨的代码技巧都重要。最后一次建议如果你用的是 Jupyter Notebook最好把原始数据单独保存一份命名为movies_raw.csv清洗后的数据另存为movies_clean.csv。原始数据是案发现场绝不能被覆盖。这样就算清洗流程出了问题随时可以回到原点重新来一遍。6. 清洗完成的验证与两个不得不说的边界问题清洗做到这里已经完成了大部分工作量。但我见过太多人在这时候早早地进入建模环节结果后面又回头补窟窿。清洗完成之后一定要做一次完整的验证确认数据真的能交给下一步。6.1 验证清单别只跑一个info()就完事我的验证流程固定四步每步都不能少# 第一步确认没有缺失值了 assert df.isnull().sum().sum() 0 # 第二步确认没有重复行 assert df.duplicated().sum() 0 # 第三步重新看统计描述确认数值范围合理 print(df.describe()) # 第四步确认总行数和清洗前一致等于清洗前总行数减去删除的行数 print(清洗后数据形状, df.shape)第一步用assert是最狠的办法一旦断言失败程序直接报错一目了然。千万别用print看一眼就完事人眼很容易漏掉。做完这四步之后再手动df.head(10)翻一翻数据看有没有异常的特征组合比如某部电影年份是 2099、票房是负数这类还没被抓出来的问题。这种肉眼看一遍的习惯能帮你抓到代码逻辑抓不到的业务错误。6.2 边界问题一先切分再清洗防止数据泄露到了这一步很多初学者会直接抱着全部清洗好的数据开始建模但这里藏着一个非常关键的坑必须在切分训练集和测试集之后再分别处理数据或者说填充等操作要根据训练集的信息来做。举个例子。假设我们用全局中位数填充了rating然后才切分训练集和测试集。那么测试集里的填充值其实已经看到了整个数据集的分布信息。这会导致模型评估结果偏乐观真实场景下表现没有这么好。这个问题在机器学习里叫作数据泄露。正确的做法是先切分数据from sklearn.model_selection import train_test_split train_df, test_df train_test_split(df, test_size0.2, random_state42)然后在训练集上计算出填充值比如中位数再用这个中位数去填充测试集。注意这个逻辑在入门阶段会稍微复杂一些因为对测试集的处理要晚于训练集。我会在下一篇讲建模流程时专门展开但这一步你一定要在清洗时就有意识地考虑进去不要等模型都训练好了才后悔。6.3 边界问题二训练集和测试集要步调一致第二个边界问题和第一个紧密相关测试集在提交给模型之前也要经过和训练集相同的清洗处理。很多新人会犯一个错误训练集经过各种填充、删除、异常值处理后直接切给模型训练而测试集还是原始的脏数据。这样模型在训练时学到的是干净数据长这样测试时面对的却是带缺失、带异常的另一套数据效果当然打折扣。正确的流程是先切分训练集和测试集分开。在训练集上做缺失值填充、异常值判断。把同样的填充规则套用到测试集上。比如训练集用rating中位数填充测试集也用训练集的中位数填充而不是测试集自己的中位数。两边都清洗完再做后续的特征工程和建模。实际操作中这通常需要把清洗逻辑封装成函数训练集和测试集各调用一次保证规则一致。这个意识一旦建立起来你会发现自己的模型评估结果变得更诚实、更可靠。数据清洗做到这一步我们的数据预处理上就算收尾了。清洗后的movies_clean.csv保存好后下一篇我会带着它走进特征工程类型字段怎么编码、评分和票房怎么缩放、要不要构造新特征。到那时候你会发现前期这张干净的数据表能让后面所有步骤都顺畅得不可思议。