首页 / 资讯中心 / 文章详情

用Pandas做数据清洗与数据预处理的完整实战指南

用Pandas做数据清洗与数据预处理的完整实战指南 ★ FEATURED ARTICLE
做了快五年数据相关的工作有个体会越来越深模型调参调到头也就那样真正决定上限的往往是训练集本身的质量。我记得有一次用一套挺复杂的模型做预测结构照论文搭的调参工具也用得很熟练可精度就是卡着不动。折腾了快一周最后把训练数据集翻了个底朝天才发现问题出在几十个样本的异常值和一列类型存错的日期上。这就是标题里那件事——用Pandas做数据清洗与数据预处理再把数据标准化成模型真正吃得动的样子。基本功不扎实后面所有花哨操作都是空中楼阁。这篇就聊聊我自己在这一整套流程里的完整做法和踩坑经验。1. 数据预处理为什么这么重要先说一个让我记忆深刻的教训1.1 模型效果差真的不全是模型的问题很多人一上来就抱着调参神器不放网格搜索、贝叶斯优化一通操作最后提升零点几个点就欢呼雀跃。可实际上训练集里的脏数据对模型的影响远比超参数大得多。举个例子我处理过一份销售数据里面有一列成交金额看着是数值型实际上有相当一部分是字符串格式还混着1,200元、暂无这种内容。如果直接拿去训练pandas会把这列当成object类型模型要么报错要么把这些文本当成了类别特征处理结果可想而知。还有一次是重复样本的问题。数据是从多个渠道合并的同一个用户出现了三回每回的标签还不一样。模型在训练集里见过这个用户到了测试集换了个渠道的收录方式特征分布就变了。这不是玄学是数据泄漏的一种变体。数据预处理阶段把这些清理干净后面能省下大量调模型的时间。1.2 数据预处理在整个机器学习流程里的位置如果你画一条流水线数据采集 → 数据清洗 → 特征工程 → 模型训练 → 评估 → 上线那么数据清洗和标准化就是连接原始数据和模型的桥梁。Pandas在这个阶段扮演的角色不是简单地去掉空值而是做三件大事整理结构类型转换、字段拆分、修正内容缺失值、重复值、异常值、统一尺度标准化、归一化。从这个角度看你就可以理解为什么很多招聘数据岗的JD里都会写熟悉Pandas具备数据清洗经验。这真不是门槛低而是数据预处理这件事直接决定了下游能不能顺利跑起来。你不会希望在训练的第五分钟才被一个TypeError打断思路对吧2. 数据体检先行训练集清洗前必做的四步摸底2.1 别急着清洗先用Pandas把数据全貌看透拿到一份陌生的训练集我的习惯是先做数据体检。和人体检一样目的是知道哪里有问题、问题有多严重而不是直接开药。第一步是加载数据后立刻查看规模和字段。示例代码import pandas as pd df pd.read_csv(train_data.csv) print(df.shape) print(df.columns.tolist())然后是逐列的类型与缺失情况我通常这么写df.info()df.info()会告诉你每一列的dtype、非空数量、内存占用。别小看这个输出它一次性解决了三个疑问有多少列是object类型大概率藏着脏数据、哪些列有缺失、整体内存够不够。如果你发现一个年龄字段被推断成object这就是类型不规范的第一个信号。2.2 数据类型检查里藏着的隐性问题这里我多说一句dtypes的问题。很多人在体检阶段会忽略的一件事是数值列里混入字符串会被pandas直接推断为object或字符串类型但反过来如果一列确实应该是数值却因为个别非法值变成了object那你后面做聚合、算均值都会出问题甚至连排序都会按字符串排。我习惯再跑一段代码把所有object列的独取值数量打出来object_cols df.select_dtypes(include[object]).columns for col in object_cols: print(col, df[col].nunique(dropnaFalse))nunique可以快速判断哪些列看起来是类别、哪些列其实是被当成了文本的数值。比如一个收入等级列有5个值那是正常类别如果收入列出现上千个不同的文本串那基本就是需要转换的数值字段只是被某些异常值污染了。2.3 数值型字段的统计画像做完类型摸底再对数值列做一个统计画像df.describe()describe()默认输出count、mean、std、min、四分之一分位数、中位数、四分之三分位数、max。这里面最值得留意的是min和max尤其是那些明摆着不合理的极值比如人的年龄出现999或者房价出现负数。describe阶段发现异常值比建模阶段发现要划算得多。3. 缺失值处理策略不要一个fillna走天下3.1 缺失率统计先判断该删还是该填缺失值几乎是每个真实数据集都逃不掉的问题但处理方法完全取决于缺失率。我通常用一行代码算缺失率missing df.isnull().sum() missing_pct (missing / len(df)) * 100 print(missing_pct.sort_values(ascendingFalse))我的经验是缺失率达30%以上的字段除非它是模型非常依赖的核心特征否则优先考虑删除因为这种高缺失率的列往往收集质量本身就很差填充出来的值反而是噪音。缺失率在5%以下的列删除缺失样本或者用简单填充都行。真正需要花心思设计填充方案的是5%到30%这一档。3.2 数值型缺失的几种填充方案与适用场景在这个区间里的数值型字段我用过这几种方案各有各的适用条件填充方式适用场景我的代码习惯均值填充数据近似正态分布缺失率低df[col].fillna(df[col].mean())中位数填充存在明显偏态或异常值df[col].fillna(df[col].median())前向/后向填充时间序列、顺序明显的样本df[col].ffill()分组填充有明确的组别维度如按地区填平均值df.groupby(region)[col].transform(lambda x: x.fillna(x.median()))均值填充有个容易忽视的问题它让这一列的方法差变小说白了就是人为压缩了数据波动。所以只要数据有偏态我就优先考虑中位数。另外分组填充的效果通常比全局填充好因为上海区域的缺失值用上海的平均水平来填比全国平均水平来填更符合业务逻辑模型也更容易学到区分度。Pandas 2.x版本需要注意一个坑fillna(methodffill)这种写法已经不建议了新版本直接调用df[col].ffill()或df[col].bfill()就好别拿着老代码硬跑。3.3 类别型缺失的填充逻辑类别型字段的缺失最常见也最简单的处理是单独填一个Unknown或缺失类别。有人会觉得填个Unknown会不会让模型学到奇怪的东西但我的看法是对于树模型这类能处理类别特征的算法缺失本身就是一个有效信息说明数据采集方在这条记录上没拿到值这往往是某种业务信号。另一种更讲究的做法是众数填充但只适用于类别分布极其倾斜的场景。比如某个字段95%都是A那填A不会引入太多噪音。如果类别分布相对均匀填哪个都可能引入错误这时候建一个缺失类别反而是最中性的选择。3.4 时间序列类数据的特殊处理时间序列样本的缺失需要考虑前后时间点的相关性所以前向填充几乎是默认选择。比如传感器数据第10秒缺失了用第9秒的值填比用整列均值填要合理得多。不过有个细节前向填充前一定要先按时间字段排序否则填充顺序错乱就完全失真了。遇到那种大段连续缺失的时间窗口我会做标记列而不是硬填。具体做法是新增一列is_missing记录该位置是否缺失这样即使数值本身填得不准模型至少知道这段数据不可靠。4. 重复值、异常值与一致性最容易放过也最坑人的环节4.1 重复样本不只会加大训练量还会扭曲评估指标数据从多个渠道合并时重复样本几乎是必然的。drop_duplicates这个函数大多数人都会用但坑在于它的参数。df df.drop_duplicates(subset[user_id, order_time], keepfirst)subset决定了怎么判断重复。如果所有列都相同才是重复那是完全重复样本删掉没有争议但更常见的是业务意义上的重复——同一个人、同一时间被录入了多条而其中某些字段比如备注、渠道来源不同。这时用keepfirst保留第一条或keeplast保留最后一条就有讲究了。我一般倾向于保留记录最完整的那一条df df.sort_values(record_completeness, ascendingFalse).drop_duplicates( subset[user_id, order_time], keepfirst )这个技巧在处理带质量评分的数据时特别有用。4.2 异常值的识别从IQR到业务规则异常值识别我常用的有两类方法。一类是纯统计的IQR法也叫四分位距法Q1 df[amount].quantile(0.25) Q3 df[amount].quantile(0.75) IQR Q3 - Q1 lower Q1 - 1.5 * IQR upper Q3 1.5 * IQR df df[(df[amount] lower) (df[amount] upper)]这套规则认为落在[lower, upper]之外的值是异常值简单、可复现是探索阶段的首选。另一类是基于业务规则比如金额不能为负、年龄不能大于150、温度不能超过物理上限等。业务规则的优先级永远高于统计规则因为统计规则可能会误删真实但极端的情况比如小额高收益的理财产品、大促期间的超大订单这些业务上完全合理。4.3 不一致数据的清洗正则表达式和统一格式是好朋友训练集里最容易被忽视的是文本字段的不一致。比如国家字段里同时存在中国、china、China 、中 国看起来是小事但切分训练集后模型会把这些当成完全不同的离散值既浪费了模型容量也破坏了特征分布的一致性。我的常规操作分三步去首尾空格、统一大小写、再做同义词映射。必要的时候用正则表达式做模式匹配之前遇到一批电话号码里混着138-1234-5678和13812345678两种格式全部用正则抽数字再统一一步搞定import re df[phone] df[phone].astype(str).str.replace(r\D, , regexTrue)这条正则的含义是把所有非数字字符去掉\D在正则里就是非数字的意思。字符串处理里str方法配合regexTrue能解决大量不一致问题。5. 数据类型转换Pandas里最不起眼但最要命的操作5.1 用to_numeric和astype兜住脏类型数据胰岛素检查完接下来要处理的就是为什么数值列不是数值的问题。最常见的场景一列数字因为夹杂了空格、逗号、单位变成了object类型。我的标准化套路是先清理再转换df[price] df[price].astype(str).str.replace(,, ).str.replace(元, ) df[price] pd.to_numeric(df[price], errorscoerce)注意errorscoerce这个参数。它表示转换失败的位置直接变成NaN而不是让程序崩溃。转换之后再统一走缺失值处理流程把那些无法解析的异常值一并处理掉。这一步是我处理任何真实数据集必写的一行因为它给数据质量兜了底。学会看dtype也是基本功。df.info()里object类型越少说明你的训练集越规整。但这里也有个反向操作如果一列确实是类别而且类别数量远小于样本量我建议转成category类型df[city] df[city].astype(category)category类型不仅省内存训练时还能让部分模型知道这是一个有序离散特征而不是连续值。另外当一列数值其实是等级1、等级2、等级3时也要考虑转成有序category而不是当普通数值用。5.2 把字符串类别变成模型能吃的数值模型基本不能直接处理字符串所以类别型字段要么转成category要么做编码。最基础的方式是factorizedf[city_code] pd.factorize(df[city])[0]这会把北京、上海、广州映射成0、1、2。但要注意这种方式编码出的数字本身没有大小含义树模型通常能容忍但线性模型或距离类模型就完全不行必须配合独热编码。独热编码用Pandas的get_dummies可以直接生成df pd.get_dummies(df, columns[city], drop_firstTrue)drop_firstTrue是为了避免多重共线性在线性回归里尤其重要不放出来会踩一个坑——模型训练出来系数诡异那就是独热编码没有去重导致的。5.3 日期时间转换与衍生特征日期列是另一个重灾区。pd.to_datetime是必须用的但格式问题比想象的复杂df[date] pd.to_datetime(df[date], format%Y-%m-%d)如果源数据里混了多种日期格式直接指定format会报错这时可以先不指定格式让pandas自动识别再处理解析失败的值。日期转换完不代表预处理结束我几乎总是会从日期里提取出更有用的特征df[year] df[date].dt.year df[month] df[date].dt.month df[day_of_week] df[date].dt.dayofweek df[days_since] (pd.Timestamp(today) - df[date]).dt.days距今多少天这个特征在很多场景里比日期本身更有效因为它给模型一个连续递减的趋势尤其适合做流失预测、生命周期分析这类任务。6. 标准化与归一化怎么选、怎么做、怎么避免数据泄漏6.1 标准化和归一化的区别一句话版本标准化与归一化经常被人混着叫但它们在预处理里的地位不同。标准化Standardization通常指Z-score把数据变成均值0、标准差1[ z \frac{x - \mu}{\sigma} ]归一化常指Min-Max缩放把数据压缩到[0,1]区间[ x \frac{x - min}{max - min} ]Pandas里沒有现成的StandardScaler但我通常配合scikit-learn一起用很少手写公式因为手写容易忘掉要保存训练集的均值和标准差再用同一套参数处理新数据这个关键点。6.2 用scikit-learn还是手写核心是别把参数算错数据集这里必须说一个重要概念数据泄漏。如果你直接用全量数据的均值和标准差做标准化等于把测试集的信息提前泄露到了训练集里模型精度的评估就失真了。正确做法是只用训练集拟合scalerfrom sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)fit_transform中的fit是在训练集上计算均值和标准差之后对测试集只做transform。这一步看起来很简单却有非常多人做错。我自己早期也犯过在验证集上看到指标不错但上线之后完全对不上后来排查才发现是标准化阶段把整个数据集一起fit了。6.3 哪些模型吃这套哪些模型不吃不是所有模型都需要标准化。距离类模型像KNN、SVM以及梯度下降类模型神经网络、线性回归、逻辑回归对特征尺度非常敏感量纲不一致会导致大数值特征主导训练过程必须标准化。而树模型决策树、随机森林、XGBoost、LightGBM本质上是对特征做切分不受单调变换影响所以标准不标准化对树模型的结果影响很小。但即便如此我还是建议在特征工程完成后做一次整体检查把量纲差异过大的特征拉到一个可比较的范围不仅是为了模型更是为了可解释性——所有特征都标准化后特征重要性排序会干净很多也更容易发现异常行为。7. 导出训练集之前最后一道校验工序和一些实在建议7.1 导出前跑一遍完整性校验清洗、转换、标准化全部做完之后我不会立刻导出训练集而是先跑一轮完整的校验脚本。这样做的好处是方便复现和排查也符合数据流水线可审计的思路。我的校验清单大概长这样检查是否还有object类型字段df.select_dtypes(include[object]).columns.tolist()如果有说明还有字段没转换完。检查缺失值是否清零df.isnull().sum().sum()理论上清洗完成后缺失应该归零。如果仍有残留说明某个环节漏处理了不建议带着NaN去训练。检查索引是否连续连续构建特征时很容易因为drop或者筛选产生索引空洞导出前用df.reset_index(dropTrue)重置掉能避免训练时出现索引错位。检查行数是否和预期一致清洗过程中哪些样本被删除、被填充最好用日志记下来比到最后发现样本少了3000条再倒查要省力得多。另外特征和目标列要分开存储。我通常直接输出两个文件特征X_train.csv和目标y_train.csv需要时再合并这样也更方便别人复用。之前见过有人把目标列混在特征里一起导出了训练时忘了去掉最后模型在训练集上准确率100%一验证就崩就是因为目标标签泄漏。7.2 我在真实项目里踩过的坑说几个给后来人第一个坑是处理重复值时没有指定subset结果把真正有价值的业务记录删了全表几千行直接变成几百行。训练集行数暴减之后模型方差变大效果根本无法稳定。后来我都是先确认业务主键再决定按哪个字段组合去重。第二个坑是填充缺失值时用了全局均值结果遗忘了数据本身的分组结构。某接口维度的数据一行都不缺但全表均值填充会把一组结构完全不同的样本偏移到不合理的数值上。后来我养成了先groupby再填充的习惯宁可多写几行不让数据失真太多。第三个坑是关于标准化的顺序问题。标准化必须在数据集切分之后做这话我说过很多次但真正出问题的是切分训练集和测试集之后如果你要先做特征工程再做标准化那么特征工程过程中的中间结果千万别拿去直接fit。我见过有同事在交叉验证里对每个fold都重新fit了标准化器导致每个fold的数值尺度都不一样指标一塌糊涂。正确做法始终一致所有统计参数均值、方差、min、max都只从当前fold的训练部分估计。还有一个容易被漏掉的细节做完独热编码之后测试集和训练集生成的列名可能不一致。如果测试集某个类别没有出现在训练集里get_dummies会产生不同的列模型会直接报维度错误。稳妥的做法是reindex一下X_test pd.get_dummies(X_test, columns[city]) X_test X_test.reindex(columnsX_train.columns, fill_value0)这两行测试集补零的逻辑能帮你在模型上线时省下大把对特征的对齐时间。我在实际项目里还有一个不成文的习惯数据预处理脚本永远以函数封装输入原始路径输出清洗后的训练集文件。一次清洗任务写成一个大流程后面每次换数据只需要改字段名和规则不用从头再读一遍几百行的处理逻辑。这样做既省时间也避免下次复制粘贴代码时漏掉某个清洗步骤。训练集的数据质量这件事不会因为你的模型深、算力强就自动变好。把Pandas的清洗和标准化步骤做扎实后面所有麻烦都会小很多。
阅读完成 · 觉得有帮助?
咨询建站