ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Pandas机器学习数据预处理全流程:从清洗到数据集划分

Pandas机器学习数据预处理全流程:从清洗到数据集划分 做机器学习训练数据集最耗时间的往往不是调模型而是数据预处理。这里面Pandas是绕不开的主力工具不管你是做结构化数据的分类回归还是准备YOLO、DOTA这类视觉模型的标注文件最终都要落到DataFrame上做清洗、校验、标准化、划分训练集。这篇文章就把我在实际项目中用Pandas处理训练数据的完整流程拆开讲从缺失值、重复值、异常值处理到特征标准化和数据集划分再到视觉任务里标注文件的清洗每一步都有可以直接抄作业的代码和判断标准适合刚入门机器学习、正在准备自己数据集的读者也适合想把手头数据清洗流程规范化的从业者。1. 数据预处理到底在解决什么问题1.1 脏数据是模型上限的隐形天花板很多初学者喜欢把精力放在调参和换模型上却忽略了一个基本事实模型是数据的函数喂进去的是垃圾吐出来的必然是垃圾。这里说的垃圾不一定是有明显错误的数据更多时候是格式不统一、缺失值处理不当、异常值没有被发现、特征之间的量纲差异过大、训练集和验证集存在泄漏等这些问题的存在会导致模型在训练阶段表现不错一旦上了线上数据就崩盘。我见过最典型的案例是某同学用一份包含用户年龄、收入、消费金额的表格训练回归模型原始数据里有几百行年龄为0的记录他直接没管就丢给了模型结果模型对年轻用户群体产生了严重的系统性偏差。还有人在做特征工程时先对整个数据集做了标准化再做训练集和验证集划分这其实已经造成了数据泄漏验证集的信息提前被模型偷看最终评估指标虚高。数据预处理的核心目标有三个第一是让数据格式统一、内容合法第二是让特征分布适合模型训练第三是保证评估的公平性。这三个目标分别对应清洗、标准化、数据集划分三块工作下面逐步展开。1.2 Pandas在这条流水线里扮演什么角色Pandas在整个数据流水线中承担的是枢纽角色。原始数据可能来自CSV、Excel、JSON、数据库导出甚至是一堆散落的标注文件Pandas把这些异构来源统一读入为DataFrame然后在这张表上完成过滤、补全、变换、聚合、拆分、导出确保最终交到模型训练代码里的是一份干干净净的数据。选择Pandas而不是直接写纯Python循环或只用NumPy原因是它的API设计太贴合数据清洗场景了。按条件过滤、分组统计、连接合并、透视表这类操作在Pandas里都是几行代码的事情而用原生Python要写很多代码且容易出错。更要命的是Pandas内置了大量处理脏情况的容错机制比如to_numeric可以用errors参数把非法值转成NaN而不是直接抛异常这在真实数据上非常实用。1.3 动手清洗之前先定方案我认为一个合格的数据处理流程在写第一行代码之前应该先回答三个问题数据规模有多大哪些列是后续模型训练真正需要的当前哪些字段的质量问题是无法补救的第一个问题决定你要不要用分块读取、dtype优化、并行处理这些进阶手段如果只有一万行数据直接用最朴素的Pandas写法就行。第二个问题关系到你是否需要保留所有原始字段很多时候业务库里导出的表有几十列但对当前模型真正有用的只有几列早一点把无关字段干掉后续清洗的工作量能少一半。第三个问题最关键比如一份数据里有80%的字段缺失那这个字段基本可以放弃不要浪费时间填补。先做取舍再做清洗不要指望Pandas能把所有烂数据都修复成可用状态。2. Pandas数据清洗核心实操2.1 缺失值先判断性质再决定删还是补缺失值处理的第一步永远是统计和定位不要凭感觉处理。用df.isnull().sum()可以快速看到每一列的缺失数量配合df.isnull().mean()算出缺失比例心里就有数了。处理策略上我的经验是分三种情况对待。第一种某列缺失比例超过50%一般建议直接删除该列除非这列是业务上的核心字段且你有领域知识可以合理补全。第二种缺失比例比较低比如5%以内行数足够多的情况下直接删除这些行最省事不会引入人为误差。第三种缺失比例不高但行数宝贵或者这列对模型很重要就需要用填充策略可以用该列的均值、中位数、众数填充也可以根据其他特征分组后填充组内均值还可以用前向填充或后向填充处理时间序列数据。import pandas as pd import numpy as np # 统计缺失情况 df pd.read_csv(train_data.csv) missing_stats df.isnull().sum() missing_ratio df.isnull().mean() # 删除缺失比例超过50%的列 df df.drop(columnsdf.columns[missing_ratio 0.5]) # 低缺失率场景直接删除有缺失的行 df df.dropna(subset[age, income]) # 高价值列按组填充中位数 df[income] df.groupby([city])[income].transform(lambda x: x.fillna(x.median()))还有一种插值方式适合数值型时间序列用df[temperature].interpolate(methodlinear)Pandas会基于前后有效值做线性插值。实操中要记住填充不是目的还原数据真实分布才是目的能用中位数就不用均值因为均值容易受异常值影响这点和下一节异常值处理要联动思考。2.2 重复值不是所有重复都该删Pandas里检测重复值用df.duplicated()返回一个布尔序列删除用df.drop_duplicates()。看起来简单但实际项目里有两个坑。第一个坑是误删看起来重复但实际有意义的记录。比如用户行为日志里同一用户在同一个时间点发生了多笔订单如果订单号不同这些记录就不是真正的重复应该保留。所以使用drop_duplicates时一定要用好subset参数只针对真正决定重复的主键列去重不要用全列去重。第二个坑是keep参数的语义默认keepfirst表示保留首条删除后面的keeplast则相反keepFalse会删除所有重复行。我处理用户画像表时习惯用keeplast因为后导入的记录往往是修正后的数据。# 按订单号去重保留最后一条记录 df df.drop_duplicates(subset[order_id], keeplast) # 查看哪些行在关键字段上重复 dup_mask df.duplicated(subset[user_id, date], keepFalse) print(df[dup_mask].sort_values(by[user_id, date]))还有一个细节有些重复不是因为数据本身重复而是因为拼接表时左连接产生了笛卡尔积这种问题在后续做数据集合并时要格外小心合并完一定要检查行数是否异常膨胀。2.3 异常值宁可多查不要放过异常值检测没有万能公式常见的方法有两类一类是基于统计分布的另一类是基于业务规则的。统计分布方法里最常用的是Z-score和四分位距法。Z-score认为超过3个标准差的点是异常点适合近似正态分布的数据IQR法用分位数做判断适合偏态分布判断标准是小于Q1-1.5IQR或大于Q31.5IQR。业务规则法更直接比如年龄字段范围必须在0到120之间收入必须大于0坐标必须落在图像尺寸范围内。我在实际项目中从来不会只依赖统计方法而是先用describe()看看每一列的最小值、最大值、分位数结合业务常识判断哪些值不合理再针对性处理。处理异常值同样有三种思路删除、截断、单独标记。如果异常值占比极少且明显是录入错误直接删除。如果异常值数量较多但分布偏极端比如收入字段有少数千万级的极端值用clip方法做截断会更稳妥。还有一种进阶玩法是把异常值单独标记成一个二值特征让模型自己学习异常模式。# 四分位距法检测年龄列异常 Q1 df[age].quantile(0.25) Q3 df[age].quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR df[age_outlier] ((df[age] lower_bound) | (df[age] upper_bound)).astype(int) # 对收入列做截断防止极端值干扰 df[income] df[income].clip(lower0, upperdf[income].quantile(0.99))2.4 数据类型转换很多诡异报错的根源Pandas里的数据类型问题非常隐蔽。最常见的场景是明明看起来是数字的列实际存储类型是object因为原始数据里有少数非数字字符比如1,200这种带千分位的字符串。直接对这一列做加减乘除就会报错或者参与训练时被当成类别特征处理。排查方法很简单打印df.dtypes看看每列类型。对数值列做转换用pd.to_numeric并设置errorscoerce非法值会自动变成NaN这样既不会报错又能定位到脏数据的位置。日期列的处理是另一个高频痛点pd.to_datetime配合format参数可以处理各种格式的日期字符串比如2024/01/15和20240115。# 字符串数字转为数值非法值变NaN df[salary] pd.to_numeric(df[salary].str.replace(,, ), errorscoerce) # 统一日期格式 df[date] pd.to_datetime(df[date], format%Y%m%d, errorscoerce) # 将类别列转为category类型节省内存并加速分组 df[city] df[city].astype(category)检查类型转换后产生的NaN也非常关键它们往往是脏数据的指纹。usingpd.isnull()筛选出来看看这些值到底是该删除还是修正要明确处理不能留着让模型报错。2.5 文本字段的初步清洗文本类特征在表格数据里也很常见比如商品标题、用户评论、地址信息等。我不展开讲NLP的完整流程只说在Pandas阶段必须做的几件事去首尾空白、统一大小写、去除特殊符号、正则提取关键片段。字符串操作在Pandas里通过.str访问器实现df[name].str.strip()、df[name].str.upper()都是高频操作。需要匹配复杂模式时用正则表达式和str.contains、str.extract比如从地址里提取省份、从订单号里提取日期片段。# 清洗商品标题去空白、去特殊符号 df[title] df[title].str.strip().str.replace(r[^\w\s\u4e00-\u9fa5], , regexTrue) # 抽取邮箱的域名部分 df[email_domain] df[email].str.extract(r([a-zA-Z0-9.-]))文本清洗最容易犯的错是在同一列上连续多次赋值每一步都可能引入新的问题比如统一小写后再用正则去匹配大小写混合模式就会失效。我的建议是先把清洗规则按顺序写成一个函数apply到Series上而不是东一榔头西一棒子地原地修改。3. 特征标准化与数据集划分3.1 为什么必须做标准化做了清洗之后数据在合法层面过关了但直接喂给很多模型还是不行。核心原因是量纲差异。举个例子一个人年龄是30年收入是30万收入的特征值比年龄大了四个数量级。对基于距离的算法如KNN、SVM还有神经网络这个差异会被放大模型会默认收入特征更重要而年龄特征几乎不影响距离计算。对梯度下降类的模型不同量纲的特征会导致损失函数在参数空间里的梯度方向扭曲训练不稳定、收敛慢。标准化就是解决这个问题的目标是让每个特征的尺度统一常见有两种Z-score标准化和Min-Max归一化。Z-score把数据变为均值为0、标准差为1的分布适合数据近似正态分布、存在极端值的场景Min-Max把数据压缩到[0, 1]区间适合数据分布比较均匀、没有极端离群点的场景。树模型不依赖特征缩放线性回归、逻辑回归、神经网络、KNN和SVM都需要。3.2 手写还是直接用sklearn很多初学者会在这一步纠结是直接用(df - df.mean()) / df.std()手写还是用sklearn.preprocessing里的StandardScaler。我的建议是尽量用sklearn不是因为它算得更好而是因为它和后面的数据划分能形成一套完整流程避免数据泄漏。手写方式很简单但有一个致命问题用整个数据集算出均值和标准差再对训练集和验证集分别做变换验证集的信息已经通过全局均值混进了训练过程这就是数据泄漏。正确的做法是只在训练集上做fit计算均值和标准差然后用这个均值和标准差去transform训练集和验证集。from sklearn.preprocessing import StandardScaler, MinMaxScaler cols_to_scale [age, income, amount] scaler StandardScaler() # 只对训练集fit X_train[cols_to_scale] scaler.fit_transform(X_train[cols_to_scale]) # 用训练集的参数transform验证集 X_valid[cols_to_scale] scaler.transform(X_valid[cols_to_scale])MinMaxScaler的用法完全一样只是底层公式不同。如果特征中存在大量离群点MinMax会把这些点压缩到很小的区间影响正常的特征分布这种情况优先选StandardScaler或RobustScaler。RobustScaler基于中位数和四分位距对异常值有天然的鲁棒性也是不错选择。3.3 类别特征处理表格数据里大量出现的是字符串类别字段比如城市、职业、产品类目。模型不认识字符串必须转换成数值。Pandas里的get_dummies是最方便的方式直接把类别列展开成多列0/1的独热编码。缺点是当类别维度特别多时特征矩阵会爆炸式膨胀所以类别超过几十个的最好先做频率编码或目标编码。# 独热编码 df_encoded pd.get_dummies(df, columns[city, category], dummy_naFalse) # 低频类别合并 city_counts df[city].value_counts() rare_cities city_counts[city_counts 10].index df[city] df[city].apply(lambda x: other if x in rare_cities else x)还有一种常见做法是LabelEncoder给每个类别一个整数编号但要注意这个编号隐含了大小关系树模型用它没有大问题线性模型和神经网络最好避免因为模型会认为编号大的类别天然比编号小的更重要这是无意义的假设。3.4 训练集、验证集、测试集划分数据划分这一步直接决定模型评估的可信度。标准做法是用train_test_split把数据按比例切分分类任务要加stratify参数做分层采样确保划分前后每个类别的比例一致。random_state的设定也不可或缺保证每次跑出来的划分结果一致方便复现。from sklearn.model_selection import train_test_split X df.drop(columns[label]) y df[label] X_train, X_temp, y_train, y_temp train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) X_valid, X_test, y_valid, y_test train_test_split( X_temp, y_temp, test_size0.5, stratifyy_temp, random_state42 )三个集合说完数据泄漏最关键的一句话是所有基于训练集计算得到的统计量比如均值、方差、缺失值填充的数值、类别编码的映射关系都必须只使用训练集的数据来拟合然后应用到验证集和测试集上。哪怕只是用全局均值填充缺失值这种看似无关的操作也会让验证集的评价乐观偏差。如果数据是时间序列就不能用随机划分了必须保证训练集在时间上先于验证集和测试集否则未来信息泄漏得毫无遮拦。Pandas里可以用sort_values按日期排序后按位置切片。4. 视觉训练数据集的Pandas清洗实践4.1 标注文件也是结构化数据现在很多人在用YOLOv8、mmrotate这些目标检测框架训练自己的数据集视觉任务的主战场是图像但标注信息比如类别、边界框坐标、图像尺寸、数据集划分本质上还是结构化数据。无论是Pascal VOC的XML、YOLO的TXT标签还是DOTA数据集的CSV格式都可以读进DataFrame做清洗。很多人直接在文件夹里手工改标注几百张图勉强能行上千张图就是灾难。我的习惯是第一步把所有标注文件解析成统一格式的DataFrame至少包含image_id、class_name、x1、y1、x2、y2这几列图像级别的元数据比如宽高、分组信息单独维护一张表。两张表通过image_id关联。这样之后所有清洗、统计、划分工作都在DataFrame上完成最后再按框架要求导出成对应的标注格式。4.2 坐标校验与无效框清理目标检测标注文件里最常见的脏数据有这样几类坐标顺序颠倒导致x2小于x1、坐标超出图像边界、边界框宽高为0或为负、类别名称拼写不一致、同一张图里出现了重复的标注框。这些问题不解决训练时轻则Loss异常重则直接报错中断。Pandas处理这种问题非常顺手。先排除x2 x1或y2 y1的非法框再拿图像尺寸表做一次merge检查坐标是否越界然后把越界的坐标clip回边界内。宽度和高度过小的框比如小于5个像素的基本可以判定是标注噪声直接删除因为它们对模型学习没有正向作用。annotations pd.read_csv(annotations.csv) annotations annotations[annotations[x2] annotations[x1]] annotations annotations[annotations[y2] annotations[y1]] # 合并图像尺寸检查越界 annotations annotations.merge( image_meta[[image_id, width, height]], onimage_id, howleft ) annotations[x1] annotations[x1].clip(lower0, upperannotations[width]) annotations[y1] annotations[y1].clip(lower0, upperannotations[height]) # 删除过小和重复的框 annotations[box_area] (annotations[x2] - annotations[x1]) * (annotations[y2] - annotations[y1]) annotations annotations[annotations[box_area] 25] annotations annotations.drop_duplicates(subset[image_id, class_name, x1, y1, x2, y2])4.3 类别平衡检查与重命名映射拿到标注DataFrame后第一件事就是按类别做统计用groupby就可以看出各类别的样本量分布。如果某些类别特别少比如几百个框对几千个框训练出来的模型对这些少数类基本处于瞎猜状态。这时候要么采集更多数据要么做类别合并要么对少数类采用重采样方案。我处理DOTA这类遥感旋转框数据集时类别统计还要额外小心同一种物体可能有不同名称比如small-vehicle和vehicle明明指同一类但标注名称不同。统一类别名称的常规操作是维护一个映射字典然后用Pandas的replace批量完成重命名。class_mapping { veh: vehicle, Vehicle: vehicle, small-vehicle: vehicle, large-vehicle: vehicle } annotations[class_name] annotations[class_name].replace(class_mapping) # 检查重命名后的类别分布 print(annotations.groupby(class_name)[image_id].count().sort_values())类别名统一之后再做过滤或合并避免不同后期目标的混淆。最后的category列还要检查有没有意外出现的空值或非常规字符。4.4 按图像粒度划分训练集和验证集和目标检测数据打交道时如果直接从标注DataFrame上做train_test_split会把同一张图像的不同标注框切到不同集合里这样训练集和验证集之间就出现了图像内容的重叠模型等于间接见过了验证集的图像评估分数虚高。正确做法是先按image_id做分组拿到唯一的图像列表在这个列表级别上做划分再用划分结果回关联到标注表。unique_images annotations[[image_id]].drop_duplicates() train_ids, valid_ids train_test_split( unique_images[image_id], test_size0.2, random_state42 ) train_anns annotations[annotations[image_id].isin(train_ids)] valid_anns annotations[annotations[image_id].isin(valid_ids)]这个陷阱我踩过好几次。之前有一次做车辆检测懒省事直接对标注行做随机切分训练出来的模型在验证集上mAP特别高结果在真实视频流上漏检严重。后来才发现就是对同一辆车在训练和验证里都出现过模型本质上记住了目标外观。4.5 大数据量下的内存与性能优化视觉标注文件动辄几十万行加上图像元数据做mergePandas内存占用很容易飙到几个GB。几个实用的优化手段读取CSV时指定usecols只读需要的列dtype参数把确实需要作为数值的列直接指定类型避免Pandas自动推断为int64或float64造成内存翻倍对类别列使用category类型内部会用整数编码存储内存能省很多如果文件非常巨大可以分块读取每块清理完毕再合并。如果必须在DataFrame里做复杂逐行计算比如对每个边界框计算面积后还要根据长宽比做过滤直接用apply循环往往很慢。优先把所有列拆开做向量化运算比如(x2-x1)和(y2-y1)整体计算这种写法比apply逐行取快一个数量级以上。5. 常见问题与排查技巧实录5.1 读取CSV中文乱码训练数据里带中文标签或中文类别名很常见read_csv读出乱码一般是编码问题。用encodingutf-8读的时候报错或者读出乱码可以试encodinggbk还不行就升级为encodinggb18030这个编码的字符集更全。如果导出的文件要给别人用写入时推荐encodingutf-8-sig带BOM头的UTF-8在Excel里打开不会乱码。我还遇到过一种情况分隔符不是逗号而是制表符或自定义符号。read_csv里用sep\t可以读TSV用sep;读分号分隔的文件。复杂的固定宽度文本可以试试read_fwf。如果文件有多行表头或注释行需要使用skiprows参数跳过。先读一小组数据试试看再决定最终参数不要一次性读完几百万行才发现格式不对。5.2 替换和清洗没生效很多人在做字符串替换时发现replace没有起作用最常见的原因是没有给regexTrue参数。Pandas的str.replace在旧版本里默认把替换模式当正则表达式处理新版本为了兼容显式要求regex参数。如果只是想替换普通字符串且替换值包含反斜杠或特殊正则字符反而应该设置regexFalse否则会出现替换了但什么都没变的困惑。另一个常见问题是忘记赋值。Pandas里大部分操作默认不修改原DataFrame而是返回新表。df.dropna(inplaceTrue)这类带inplace的写法可以原地修改但很多开发者并不建议依赖inplace因为它在某些链式操作里会失效甚至报警告。我习惯写显式赋值比如df df.dropna()逻辑清楚不易出错。5.3 布尔索引筛选时的NaN陷阱用df[df[age] 25]筛选时age列里如果有NaN这些行会被当成False被排除。有时候这正是我们想要的但如果你希望保留缺失值就需要显式加上条件。更隐蔽的是取反操作用~对布尔条件取反时NaN依然会被排除因为NaN不参与比较。如果我想保留年龄不大于25岁或年龄缺失的人正确写法是df[(df[age] 25) | (df[age].isnull())]。这个坑在处理异常标注时尤其常见当你写代码排除不符合条件的数据一不小心会把缺失的行也删掉。处理之前先统计一下筛选前后数据量的变化差距过大就说明逻辑有需要修正的地方。5.4 数据泄漏可能藏在不起眼的预处理步骤里数据泄漏是评估结果虚高的头号元凶而且它经常藏在不起眼的操作里。比如先对整个DataFrame做fillna再划分训练和验证集就属于典型的泄漏。再比如用全量数据的value_counts做低频类别合并再把other这个映射应用到训练验证集上验证集的信息也间接进入了训练集的特征构造过程。我现在的做法是把所有预处理封装成两个阶段第一个阶段只做不依赖全局统计的清洗比如去重、格式整理、类型转换第二个阶段做依赖统计量的变换比如缺失值填充均值、标准化、类别编码这些一律放在划分完成之后并且只fit在训练集上。项目里其他人接手代码时看到清晰的阶段划分也不容易再踩坑。5.5 性能太慢时的排查顺序数据量一大Pandas操作变慢是必然的。排查性能问题我建议按照这个顺序来第一步检查是否有不必要的深拷贝比如链式操作中频繁使用df[df[col] 3][df[col2] 5]这种写法会触发多次拷贝改成先定义条件掩码再用一次筛选。第二步检查是否用了逐行apply能向量化就向量化。第三步检查数据类型object类型列会把字符串当Python对象存储转成category后groupby和去重都会快很多。# 用掩码一次性筛选避免链式多次拷贝 mask (df[col] 3) (df[col2] 5) filtered_df df.loc[mask]如果上述优化之后仍然很慢考虑用分块读取例如pd.read_csv(..., chunksize50000)每个块清洗完再合并。还有一点是能用cudf就上cudfGPU版的Pandas API兼容性已经不错了但带来的加速效果在小数据上并不明显只有数据量真的很大时才值得切换。5.6 速查表场景推荐方法关键参数缺失值统计df.isnull().sum()无删除高缺失列df.drop(columns...)缺失比例50%的列直接删填充缺失值df.fillna(方法)数值列用中位数更稳健去重df.drop_duplicates()subset按主键列指定keep决定保留位置异常值检测IQR法或Z-score结合业务规则判断类型转换pd.to_numeric(errorscoerce)非法值会变为NaN标准化sklearn StandardScaler必须在训练集上fit类别编码pd.get_dummies()类别过多时先合并低频类数据集划分train_test_splitstratify分层采样random_state固定视觉标注清洗坐标排序、clip、面积过滤先合并图像尺寸再做越界检查我个人在实际项目中的体会是数据预处理不要做到一步到位而要做到每个步骤都可回溯。清洗脚本里每做一步操作最好打印一下当前数据的行数、列数、关键字段的分布变化一旦后续发现问题能迅速定位是哪一步引入的。另一点建议是把清洗逻辑封装成独立函数输入原始DataFrame输出处理后的DataFrame中间的状态可以保留在日志里。这样不同项目之间可以复用同一套清洗模板新数据进来只需要改业务规则不需要从零再写一遍。最后再分享一个小技巧给训练集做标准化后把scaler对象用joblib.dump保存到硬盘。模型上线推理时对线上特征用同一个scaler做transform而不是重新计算均值和方差。这一步看似不起眼但凡是跳过它的人线上推理效果和离线评估对不上的时候回头找原因十有八九就栽在标准化参数不一致上。数据预处理没那么炫酷却是整个机器学习项目里最不能偷懒的一环。
返回列表