ARTICLE DETAIL

资讯详情

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

数据预处理全流程实战:从数据清洗到特征工程的完整指南

数据预处理全流程实战:从数据清洗到特征工程的完整指南 数据预处理这件事说它是建模中最没技术含量、却最决定成败的环节一点都不过分。你看网上那些公开数据集整洁得像试卷答案真实业务里的数据却像厨房刚用完的灶台——油渍、残渣、调料瓶乱放你得先擦干净、归好位才好开火炒菜。我做了这些年数据相关的工作攒了不少实操经验这次就用一个真实的客户数据预处理案例把从拿到原始数据到灌进模型之前的完整流程捋一遍。这篇内容不是教科书式的介绍而是我在项目里实际跑过、踩过坑、最后沉淀下来的一套流程适合正在学数据分析、刚接触机器学习、或者工作中经常要处理脏数据的同学参考。你不需要一次性懂所有统计原理跟着步骤走把方法复制过去改一改就能用。1. 内容整体设计与思路拆解1.1 为什么要盯死“预处理”这一步先说个直觉模型吃进去的是Tidy Data吐出来的才可能是稳定可用的结论。很多同学拿到一份Excel就急着调包训练最后accuracy低得离谱回头一看缺失值没处理、异常值是把数据导错行造成的、类别特征被当成数值塞了进去。这不是模型的锅是你喂给它的数据本身就有问题。我习惯把数据预处理的价值分成三层来看第一层是保底保证数据格式正确、类型合理、没有明显的导错和重复让训练过程能跑通。第二层是提升让特征分布更符合模型偏好缩放到合理范围模型收敛更快、效果更稳定。第三层是防爆雷提前规避数据泄漏、样本不均衡、时间顺序错乱这类隐蔽问题避免模型上线后表现崩塌。这三层对应的工作量也不一样。实际项目中预处理往往占掉整个周期的五到七成时间如果你一开始就规划好流程后面能省掉大量返工。这一篇我按自己的经验把完整流程拆成八个环节数据探查、数据清洗、字段类型校正、特征变换、类别编码、数据集成、数据集划分、流程固化。每一步都有它的目的和适用场景下面边讲边演示。1.2 先说清楚这篇案例的背景为了让你有代入感我构造了一个电商客户行为数据集字段包括客户ID、注册日期、最近一次下单日期、累计下单金额、累计下单次数、活跃渠道、所在城市等级。这种数据在真实的CRM系统、用户画像项目里很常见脏起来也很有代表性——缺失、异常、格式混杂、量纲差异、类别文本不规范全都占齐了。这个数据集大约有一万条记录几十个字段我用它演示的每一步你都可以直接替换成自己的数据场景。工具方面我用的是Python生态里的三件套pandas做表格处理numpy做数值运算scikit-learn做编码、缩放和流水线封装。后面所有代码我默认你已经装好这些库版本不用特别讲究pandas 1.5以上、scikit-learn 1.2以上就可以。提示预处理不是一次性的它是一个“探查—处理—验证—再探查”的循环过程。我见过太多人上来就删缺失值删完才发现这个字段的缺失本身就有业务含义。2. 摸底数据探查与质量判断2.1 拿到数据先别动手三分钟摸家底拿到原始数据我建议你先做三件事看形状、看字段、看样本。这三件事能让你在动手清洗之前就对数据长什么样、哪里有坑有一个整体判断。import pandas as pd import numpy as np df pd.read_csv(customer_raw.csv) print(df.shape) print(df.dtypes) print(df.head(10))这三行代码看起来简单但信息量不小。df.shape告诉你数据规模df.dtypes告诉你哪些字段被识别成了数字、哪些被识别成了字符串而df.head(n)能让你用肉眼扫一遍记录的样子。我见过不少新手在这一步就直接跳过结果后面发现“订单金额”被读成了object类型——因为原始表格里混进了“待退款”这类文本加总、求均值全报废只能回头重新处理。还有一遍动作强烈建议做统计每个字段的缺失率、唯一值个数、样本重复情况。这是数据探查的核心环节直接决定后续清洗策略。def quick_report(df): report pd.DataFrame({ 缺失数: df.isnull().sum(), 缺失率: (df.isnull().sum() / len(df)).round(4), 唯一值数: df.nunique(), 类型: df.dtypes }) return report.sort_values(缺失率, ascendingFalse) print(quick_report(df))用一个简单的函数把关键质量指标汇总出来所有字段的“健康状况”一览无余。你看缺失率最高的字段优先决定清洗方案唯一值数量异常的字段往往暗示着数据粒度有问题。2.2 缺失值类型判断随机缺失和非随机缺失的拆解拿到缺失率之后不要急着dropna()。先判断缺失有没有规律这决定你是删、是填、还是保留缺失这个状态本身。统计学里把缺失机制分三类完全随机缺失MCAR、随机缺失MAR、非随机缺失MNAR。MCAR就是缺失跟任何变量都没关系删掉或均值填补都问题不大MAR是缺失跟其他已观测变量有关需要有条件地填补MNAR是缺失本身就跟缺失的这个值有关比如高收入用户故意不填收入字段这种最麻烦直接填均值会引入系统性偏差。业务里最常见的其实是MAR。拿我的案例数据来说“所在城市等级”有8%的缺失我交叉对比了一下发现缺失比例在不同注册渠道上差异明显特别是老用户里缺失更多——这说明缺失不是偶然的跟用户生命周期有关。遇到这种情况最简单的处理方式是按渠道分组填充众数而不是全表填一个值df[city_level] df.groupby(channel)[city_level].transform(lambda x: x.fillna(x.mode().iloc[0]))不过要注意mode()可能返回多个众数取iloc[0]时要确认一下确实有值。如果某个分组全是缺失会直接报错所以代码要加一层兜底判断。对于数值字段比如“累计下单金额”如果缺失率低于3%用中位数填充通常就够了如果缺失率偏高我会考虑用随机森林或KNN这种简单模型去预测填充。模型填补听起来高级但成本高、解释性差我通常只在缺失率超过15%且字段业务含义重要时才用。2.3 异常值识别箱线图和3σ法则怎么选异常值有两种来源一种是真实的业务极端值比如大客户一次性下单几十万元另一种是数据录入错误比如把月份按成了金额。这俩处理策略完全不同。识别异常值我最常用的是两个方法箱线图法IQR把超过Q3 1.5 * IQR或低于Q1 - 1.5 * IQR的点标为异常。这个方法不依赖数据分布假设稳健性好适合大多数场景。3σ法则假设数据近似正态分布把超过均值±3倍标准差的值标为异常。适合数据本身接近正态的字段。两个方法我都写出来给你对比一下def detect_outliers_iqr(s): q1 s.quantile(0.25) q3 s.quantile(0.75) iqr q3 - q1 lower q1 - 1.5 * iqr upper q3 1.5 * iqr return (s lower) | (s upper) def detect_outliers_zscore(s, thresh3): z (s - s.mean()) / s.std() return z.abs() thresh在“累计下单金额”这个字段上我用两个方法各跑了一遍发现IQR法标出异常值多一些3σ法则少一些。原因很简单电商订单金额是右偏分布少数大客户会把标准差拉得很大3σ的边界随之变宽很多其实偏高的值反而不算异常了。遇到强偏态分布我优先用IQR法并且会结合业务去判断异常值是不是“真异常”。处理方式上我建议先分箱看分布再决定是删除、截尾还是单独标记。对于确认为录入错误的值直接删对于真极端但业务合理的值单独做一层二值特征标记而不是粗暴删除这样模型既能学到正常规律又不会把真实的大客户信息丢掉。3. 动手清理字段校正、格式统一与重复数据3.1 字段类型检查与日期时间解析原始数据里最让人头疼的坑就是类型不对。最常见的是“日期被读成字符串”“数值里混了文本”“ID被读成了浮点数”。以日期为例我在案例数据里的“注册日期”列dtypes显示是object。原因可能是Excel里存的是“2023/1/5”和“2023-01-05”混在一起加上个别空行就会导致pandas解析失败退化成字符串类型。修复方法df[signup_date] pd.to_datetime(df[signup_date], formatmixed, errorscoerce)formatmixed是pandas 2.0之后新增的参数允许同列里混用不同日期格式。没有这个参数的老版本你只能自己分批解析后拼接。errorscoerce解析失败的会变成NaT正好暴露出来方便后面统一填充。日期类型的价值不只是看着舒服而是你后面要做特征工程时它是分桶、算间隔、提取年月日星期几的基础。比如我们可以从“注册日期”里直接挖出注册年份、注册月份、距离今天的天数这些新特征都是自动化的。再看“客户ID”这种字段如果原始Excel里有文本型ID读进来可能出现“10001.0”这种浮点形式。处理方式就是用astype转换并去尾df[customer_id] df[customer_id].astype(str).str.replace(.0, , regexFalse)注意这里顺序很重要先转字符串再替换直接astype(int)在有空值时会直接报错。这些细节就是实战跟教程的区别。3.2 重复数据处理的两个层次重复数据处理我建议分成两层去看。一层是完全相同行的去重对应pandas的drop_duplicates()这个最简单。但我见过很多业务数据里行内容并不完全一样而是同一个ID出现了多次每次的状态字段不一样这时候机械地drop_duplicates()就会误删。另一层是关键维度重复。我的案例数据里“客户ID”理论上应该是唯一键。我查了一下重复情况dup_ids df[df.duplicated(customer_id, keepFalse)] print(f重复客户ID数量: {dup_ids[customer_id].nunique()})查出大约有30多个ID重复出现进一步观察发现有些是系统里重复建档有些是数据导出时把不同渠道的记录拼接重复了。对于同一ID多条记录我的处理原则是按业务规则保留最新一条或者把多条记录聚合。df df.sort_values(signup_date).drop_duplicates(customer_id, keeplast)先用注册日期排序再按客户ID去重、保留最后一条相当于保留“最新状态”。如果你需要的是历史变化信息那就该走聚合路线agg df.groupby(customer_id).agg({ order_amount: sum, order_count: count, signup_date: first }).reset_index()聚合还是去重取决于下游是训练模型用还是出报表用。训练模型需要的是每个用户的最终状态出报表则往往需要保留历史明细。3.3 文本字段清洗与渠道名称标准化类别字段的文本清洗是最容易被忽视的环节。“活跃渠道”这一列我抽出来一看五花八门“APP”“ App”“APP(安卓)”“app”“安卓端App”。这些其实都是同一类但模型的独热编码会把它们当成五个不同类别白白增加了维度又稀释了样本。处理思路是先统一大小写、去空格再手工整理映射表df[channel_clean] df[channel].str.strip().str.lower() channel_map { app: app, app(安卓): app, 安卓端app: app, web: web, wechat: wechat, 小程序: wechat } df[channel_clean] df[channel_clean].replace(channel_map)如果你面对的是海量文本且规律不明显可以用fuzzywuzzy或difflib做字符串相似度匹配先把高频的文本聚类再根据聚类结果定义映射表。这一步做得越干净后续的编码维度越少模型训练的稳定性和可解释性都更好。注意文本标准化最好在编码之前做。我曾经在项目里先做了独热编码、后做了文本合并结果白白生成了几十个空维度删起来非常痛苦。顺序错了返工是必然的。4. 变换与编码让数据长成模型喜欢的样子4.1 数值特征缩放标准化还是归一化特征缩放的意义我用一句话概括它解决的是量纲和尺度的问题让优化过程一视同仁地对待每个特征。比如“累计下单金额”动辄几万“累计下单次数”可能只有几十如果不缩放梯度下降时大数值特征会主导权重更新小数值特征几乎学不到东西。缩放方法主要有两种标准化StandardScaler减去均值再除以标准差处理后数据均值0、方差1适合数据近似正态分布的场景也是大多数机器学习模型的默认选择。归一化MinMaxScaler压到0到1区间适合数据有明确边界、或后续要喂给神经网络这类对输入范围敏感的场景。我这边用标准化的原因很简单后面的模型以树模型和线性模型为主树模型其实不要求缩放但线性模型需要而标准化对不同分布的数据更稳一点。另外记得一个关键点缩放器必须在训练集上fit再transform训练集和测试集绝对不能在全量数据上fit。否则测试集的信息会提前泄露到训练过程中评估指标会虚高。from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train[[order_amount, order_count]] scaler.fit_transform(X_train[[order_amount, order_count]]) X_test[[order_amount, order_count]] scaler.transform(X_test[[order_amount, order_count]])4.2 对偏态分布做对数变换“累计下单金额”这类字段典型特征是右偏绝大多数用户花得少极少数用户花得多。这种偏态对线性模型很不友好模型会把注意力集中在少数大额样本上。处理偏态分布最直接的手段是对数变换它能压缩大数值的间距、拉伸小数值的间距让分布更靠近正态。df[order_amount_log] np.log1p(df[order_amount])我一般用np.log1p而不是np.log区别在于log1p对0值友好log(x1)避免出现负无穷。如果你发现对数变换后分布还是偏可以试试Box-Cox变换但注意Box-Cox要求数据为正负值需要先平移。实操心得对数变换之后模型预测出的目标值如果要还原成真实金额记得用np.expm1把它变换回来。我有一次忘了做这一步预测结果看起来全都差一个数量级排查了半天才发现是忘了逆变换。4.3 类别变量编码策略独热编码与标签编码的适用场景类别编码我分三种场景来选有序类别比如城市等级一线、二线、三线用标签编码映射成1、2、3因为等级之间有顺序关系。无序类别比如渠道App、Web、微信用独热编码每个类别变成一个0/1列。高基数类别比如城市名字有成百上千个用目标编码用目标变量的均值来编码类别但这一步容易过拟合需要配合交叉验证。我的案例里城市等级用pd.Categorical转换成有序编码level_map {一线: 3, 新一线: 2, 二线: 1, 三线及以下: 0} df[city_level_code] df[city_level].map(level_map)渠道这种无序类别直接用pd.get_dummies或OneHotEncoderdf pd.get_dummies(df, columns[channel_clean], prefixchannel)4.4 特征构造从日期和业务关系中挖新特征特征工程是预处理里最有创造力的一环。很多时候模型效果上不去不是模型不够强而是特征不够多、不够好。真实数据本身信息密度不高需要通过业务理解去拆解和组合。从日期字段里我们可以挖出很多新特征df[signup_year] df[signup_date].dt.year df[signup_month] df[signup_date].dt.month df[signup_weekday] df[signup_date].dt.weekday df[active_days] (reference_date - df[signup_date]).dt.days这个案例里我构造了“注册至今活跃天数”active_days和“客单价”avg_order_value order_amount / order_count两个新特征。客单价比单纯的下单金额更能反映用户的质量对后续建模很有帮助。这一步充分解释了为什么预处理不仅仅是“清洗”它同时是特征工程的第一现场。5. 数据集成、数据集划分与流程固化5.1 表格合并时最容易踩的连接坑数据预处理到了中段经常要面对多张表的整合。比如用户基本信息一张表、订单明细一张表、渠道信息一张表你需要把它们拼在一起。pandas里的merge函数看着简单坑全在参数上。核心参数是how有left、right、inner、outer四种连接方式含义跟SQL里的join一致。我在这个案例里需要把用户表和渠道活跃度表按customer_id对齐df pd.merge(user_df, activity_df, oncustomer_id, howleft)选left是因为我以用户表为主体活动表里能匹配上的留下匹配不上的就是缺失值。但这里有个隐蔽的问题如果activity_df里存在重复的customer_idmerge之后用户表会被膨胀成多行。print(f合并前用户数: {len(user_df)}) print(f合并后行数: {len(df)})一旦发现行数变多八成就是连接键不唯一。解决办法是先对activity_df按customer_id去重或者用validateone_to_one参数让pandas在重复时直接报错避免问题悄悄出现df pd.merge(user_df, activity_df, oncustomer_id, howleft, validateone_to_one)5.2 训练集与测试集划分的两个陷阱数据切分看起来就一行train_test_split但有两个陷阱我几乎每次都要强调。第一个是分层抽样。如果你的目标变量存在明显的不均衡比如下单客户很少、未下单客户很多随机切分很可能让测试集里下单客户比例失真。加个stratifyy就能保住类别比例from sklearn.model_selection import train_test_split X df.drop(is_ordered, axis1) y df[is_ordered] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy )第二个是时间序列不能随机切分。如果你的数据有明显的时间属性比如预测未来一个月销量一定不能随机打乱否则就是用未来数据预测过去评估结果毫无意义。时间序列数据要按时间顺序切分split_date 2024-01-01 train df[df[signup_date] split_date] test df[df[signup_date] split_date]这两个陷阱前者丢的是评估的可靠性后者直接让模型在线上失效。我见过某项目用随机切分训练时间序列模型线下指标逼近90%上线后连一半都不到原因就在这里。5.3 用Pipeline防止代码失控当你把预处理的一堆步骤写满几十行代码之后下一步就该考虑怎么固化流程了。如果训练和预测时用了不同的处理逻辑模型上线必然出问题。最佳实践是把你所有的预处理步骤包进sklearn的Pipeline里让训练和预测共用同一套处理管线。from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.ensemble import RandomForestClassifier num_features [order_amount_log, order_count, active_days] cat_features [channel_clean] preprocessor ColumnTransformer([ (num, StandardScaler(), num_features), (cat, OneHotEncoder(handle_unknownignore), cat_features) ]) pipe Pipeline([ (prep, preprocessor), (clf, RandomForestClassifier(n_estimators200, random_state42)) ]) pipe.fit(X_train, y_train) score pipe.score(X_test, y_test)Pipeline最大的好处是你fit之后不需要再单独维护那一堆scaler、encoder的状态预测时直接pipe.predict就行所有变换自动同步。配合网格搜索调参会非常顺手from sklearn.model_selection import GridSearchCV param_grid {clf__n_estimators: [100, 200, 300]} gs GridSearchCV(pipe, param_grid, cv5, scoringroc_auc) gs.fit(X_train, y_train) print(gs.best_params_)注意参数名要用双下划线clf__n_estimators来指定子模块的参数这是Pipeline语法的关键细节新手经常写错。建议把data cleaning这类不适合放进sklearn的pandas操作单独抽成函数在pipe.fit之前调用。要让“清洗逻辑”和“模型变换逻辑”分层清晰以后维护起来才不头痛。6. 常见问题与排查技巧实录6.1 数据泄漏预处理里最隐蔽的暗坑数据泄漏是指在训练阶段使用了测试集或未来信息导致模型评估虚高、上线崩塌。预处理环节里最常见的泄漏点有三个缩放器在全量数据上fit、填充缺失值时用了全量数据的统计量、特征构造时用了未来的目标值。以填充缺失值为例如果你在切分训练集和测试集之前就用全量数据的中位数去填空缺值表面上没问题其实测试集的分布信息已经通过中位数流进了训练集。正确做法是在训练集上算出填充值应用到训练集和测试集fill_val df_train[order_amount].median() df_train[order_amount].fillna(fill_val, inplaceTrue) df_test[order_amount].fillna(fill_val, inplaceTrue)再举一个频繁踩坑的例子做时间序列时用当天的均值去填充当天的缺失值相当于用结果填原因模型会在线上学不到这个规律的。6.2 处理后数据分布异常反向排查法有一次我在跑完预处理后发现某个特征的取值范围完全不对数量级差了好几个倍。排查的时候我没有从头看代码而是从结果倒推先打印各特征的describe()找到数值异常的字段再反查是哪个操作导致的。结果发现是np.log1p放在fillna之前执行了——缺失值被transform后变成了NaNlog1p对NaN会原样保留导致后续模型训练时报“输入包含NaN”。这个问题的解法是先fillna再变换顺序不能反。这类“操作顺序”造成的bug用肉眼一遍扫过去很难发现最好在每个大步骤之后都输出df.info()或df.isnull().sum()做一次中间检查。6.3 性能优化处理大数据集的几个技巧当数据量上升到几千万行时预处理的性能问题就会凸显。我在处理亿级数据时有几条通用优化经验只用需要的列不要全表加载pd.read_csv(large.csv, usecols[col1, col2])可以省掉大量内存。批量操作代替遍历尽可能用向量化操作代替for i in range(len(df))pandas的apply也比逐行循环快但能不用就不用直接用内置方法最稳。分块处理用pd.read_csv(..., chunksize500000)分块读取和清洗最后拼起来。数据类型压缩把int64改成int32、把object转成category内存能直接降一半以上。这些技巧在数据量大的时候是救命级别的处理得当能把耗时从小时级压缩到分钟级。6.4 数据预处理速查表我把最常见的问题和对应处理方式整理成一张表你可以直接当工具表用问题类型典型表现处理方案注意事项缺失值字段空白或NaN密度高分类型填充、按组填充、模型预测先判断缺失机制不要无脑填均值异常值极值、负金额、日期错乱IQR或3σ识别后删除/截尾/标记结合业务判断真假异常重复数据id重复、整行重复按规则保留最新或聚合先查重复原因再处理类型错误日期读成字符串、ID带浮点to_datetime、astype转换类型转换前先解决空值量纲差异金额上万、次数几十StandardScaler或MinMaxScaler先fit训练集再transform测试集偏态分布金额右偏log1p或Box-Cox变换预测时记得逆变换类别混乱“app”“App”混用去空格、统一大小写、映射合并编码之前完成文本标准化数据泄漏全表fit缩放器/填充值按训练集fit再transform全集泄漏比不处理更有害时间顺序未来信息混入训练集按时间切分禁止随机shuffle时间序列必须顺序切分性能瓶颈处理几分钟跑不完选列、分块、压缩类型、向量化先定位瓶颈再优化这张表是多年实践中“血的教训”浓缩出来的每次项目开始前我都会过一眼。写在最后的一点体会数据预处理做久了我最大的感受是它是体力活但更是脑力活。一个看似简单的缺失值填充背后可能牵扯到数据采集逻辑、业务规则、下游模型的敏感性你在网上找到的每一条处理建议都有适用的前提条件直接搬过来不一定对得上你自己的数据。我自己现在做项目会刻意把“探查”这一步的时间拉长不要急着清洗。多花半小时去看分布、看缺失模式、看字段之间的关联后面就能省好几个小时。你还可以做一张处理记录表把每一步的依据、参数、处理前后数据量都记下来这不仅是自查的依据后面写数据报告、或者过几个月再回头看这个模型时都是非常有价值的资料。如果你想在这个方向上继续深入我建议接下来试着把整套流程写成一个函数库输入原始表格、输出处理后的数据每种数据场景对应一个配置。总有一天你会发现数据预处理这门“杂活”里其实藏着模型效果的上限。
返回列表