做AI项目这些年几乎每次开工前都会收到一句“数据我已经整理好了你直接建模吧”。但真的把数据接过来跑一遍十有八九是字段缺失、单位混乱、类别串味甚至连基础类型都是错的。时间一长我就明白了在真实落地场景里Python 数据预处理根本不是模型训练前一个可有可无的小步骤而是决定项目能不能跑通的大半个工程。这篇文章我就把 AI 数据预处理里最常用的三件套——数据清洗、数据增强、数据标准化从业务拆解到代码实现完整走一遍。适合正在做数据分析、机器学习的 Python 开发者也适合刚入门的朋友提前搞懂这个环节到底在解决什么问题。1. AI项目为什么必须过预处理这一关1.1 我见过的三类“不能直接用的数据”第一类叫脏数据。字段缺失率百分之三四十同一列里混着“男”“m”“M”三种写法金额列里有人填的是元有人填的是万元还有人填的是“1.2万”这种字符串日期格式有 2024-01-01也有 20240101还有从 Excel 导出来变成 44000 这种序列号的。这类数据不处理就送到模型里模型学到的全是错误关联训练时看着还行上线后效果立刻原形毕露。第二类叫样本太少。尤其是做异常检测、风控、故障诊断这种项目正样本往往只有几百条负样本几万条。拿这么少的数据去训深度模型基本是找死即使是轻量模型也容易过拟合到那几条稀疏的正样本上。这类问题靠调参解决不了只能靠数据层面的增补和重采样来缓解。第三类叫量纲差距离谱。用户年龄在 0 到 100消费金额在 0 到 10 万两个特征放在一起绝大多数模型会被大数值特征带走。如果你做的是 KNN、SVM、线性回归、神经网络这类对尺度敏感的模型不标准化几乎等于白做。这三类问题分别对应数据清洗、数据增强、数据标准化。很多人误以为它们是三个独立步骤实际上它们互相牵连顺序错了效果打折甚至还会引入数据泄漏。1.2 全流程顺序为什么是“清洗→增强→标准化”先说结论先清洗再增强最后再做标准化。这个顺序不是拍脑袋定的里面有因果关系。数据增强是在原始数据的基础上生成新样本。如果原始数据里还有缺失值、异常值、重复值增强环节会把错误一并放大。比如你用一个金额字段做 SMOTE 插值原始数据里恰好有一条金额因为单位混乱多填了一万倍生成的人工样本就会把这个错误扩散成一小片错误区域后期清洗成本更高。标准化依赖的统计量均值、标准差、最小值、最大值必须在增强后的完整样本上计算才有代表性。如果先把标准化做好再用 SMOTE 生成样本新样本是基于旧统计量插值出来的相当于整套特征的分布已经被旧参数扭曲过了后面再怎么调整都补不回来。除了这个顺序还要盯住数据划分的顺序。标准做法是先把原始数据切分成训练集、验证集、测试集再在训练集内部做清洗和增强标准化参数也只在训练集上拟合验证集和测试集只做 transform。否则测试集的信息会通过统计量回流到模型训练里评估结果虚高上线即翻车这就是数据泄漏。2. 数据清洗把脏数据挡在模型门外2.1 缺失值处理三种策略和选择逻辑缺失值是所有清洗工作里最常遇到的。处理方式就三类删除、填充、模型插补。删除优先用于缺失比例非常高的列。我的习惯是缺失率超过 60% 的列默认删除。但有例外如果这个字段对目标变量有很强的解释力缺失本身可能代表“用户没有这个行为”这时与其删除不如用专门的标记去填充比如“无”或者 -1让模型自己学习缺失含义。类似的做法在金融风控里很常见。填充要分字段类型。连续数值字段分布比较对称用均值有偏态用中位数类别字段用众数时间序列字段用前后值填充pandas 里的 ffill 和 bfill 是最自然的。填充值也可以根据业务造“哨兵值”比如温度缺失填 -999但这个值不能参与模型训练只能在预处理中间态使用否则会污染统计量。模型插补适合数据量大、字段之间关联明显的场景。用 KNNImputer 或简单回归模型补缺失效果通常好于均值填充。但有个坑KNN 插补会把原本离散的 0/1 特征填充出连续值比如 0.34 这种后续需要额外做一次阈值取整否则特征语义就变了。先看一下数据缺失的整体情况用代码说话import pandas as pd import numpy as np df pd.read_csv(raw_data.csv) print(数据形状:, df.shape) print(缺失统计:\n, df.isnull().sum()) miss_rate df.isnull().mean().sort_values(ascendingFalse) print(缺失率TOP5:\n, miss_rate.head())实操时我一般先跑这段把缺失率排序打印出来再决定每一列走删除还是填充。缺失率 5% 以内中位数填充就够了30% 到 60%必须结合业务判断这个字段要不要保留超过 60%默认删除特殊业务单独处理。2.2 重复值、异常值和“野值”的定位与修复重复值大家都会用 drop_duplicates()但很多人在这一步会犯一个错全字段去重。实际业务里全字段一模一样的记录反而是少数更多是同一用户、同一事件在不同时间被重复采集其他字段因为时间变化而不同。这时候应该按业务键去重比如“用户ID 事件类型 时间戳”三个字段组合判断相同就算重复。异常值的处理比重复值复杂得多。用箱线图 IQR 方法、Z-Score 方法圈出来的“异常”只能说明它在数值上离群不代表它是错误数据。在金融风控里一笔 100 万的异常消费可能恰恰是欺诈信号在物联网故障诊断里一个温度突变可能是设备故障前兆。所以我的原则是先分箱看分布再结合业务判断是噪声还是信号不要一上来就删。对于确实确认是脏数据的野值比如年龄出现 300、身高出现 5 米直接做边界截断。pandas 里的 clip 方法可以把超出边界的值统一压到边界比删除更温和保留“这个值确实存在但异常大”的信息。单位混乱是比异常值更隐蔽的问题常见于爬虫或手工录入的数据。比如金额字段里混着“元”“千”“万”“万元”或者把“1.2万”写成“12000”。处理思路是做一个单位映射函数把带单位字符串统一转成数字def clean_number(x): if isinstance(x, str): if 万 in x: return float(x.replace(万, )) * 10000 if 千 in x: return float(x.replace(千, )) * 1000 try: return float(x) except ValueError: return np.nan return x df[amount] df[amount].apply(clean_number)这类函数建议单独存成一个 utils.py因为单位换算规则一旦确认后续每次导入数据都要复用。2.3 类型转换、日期和文本字段统一字段类型是最容易被忽略的。pandas 读进来全是 object你以为那是字符串其实是混入了空格或者注释导致整列被判定为 object。比如“123 ”和“abc”混在一起的列用 pd.to_numeric(errorscoerce) 可以把能转的转成数字不能转的变成 NaN再进入缺失值流程。日期字段的坑更多。不同来源的日期可能有“2024-01-01”“20240101”“01/01/2024”“44000”四种形态。统一用 pd.to_datetime(df[date], format%Y%m%d) 指定格式解析最可靠解析失败就返回 NaT再手动排查。如果确实存在 Excel 序列日期要单独写转换逻辑。文本字段比如城市名、产品名、渠道名清洗重点在于统一字符集。中文数据里最容易出现全角数字、全角标点比如“”和“”。写一个函数def normalize_text(s): if isinstance(s, str): # 全角转半角 result [] for ch in s: code ord(ch) if code 0x3000: code 0x20 elif 0xFF01 code 0xFF5E: code - 0xFEE0 result.append(chr(code)) return .join(result).strip().lower() return s df[city] df[city].apply(normalize_text)把全角统一成半角去掉首尾空格再统一成小写。这一套下来很多分组匹配失败的问题会明显减少。手机号、邮箱、身份证这类字段不要自己写穷举规则直接用预编译的正则库处理一步到位。数据清洗的任务类型很多整理成一张速查表放在项目文档里方便团队对齐清洗任务推荐手段注意点缺失值均值/中位数/众数填充分类变量优先用众数连续变量看偏态重复值drop_duplicates(subset[业务键])全字段去重会漏掉业务重复异常值箱线图/Z-Score辅助定位先结合业务判断别直接删单位混乱自定义函数 字典映射单独存为工具函数复用日期混合pd.to_datetime(format)统一成 datetime64避免字符串日期文本脏字符正则 全角半角统一先统一字符集再做分组匹配3. 数据增强样本不够时的第二增长曲线3.1 表格数据最常用的一招SMOTE 重采样很多人一听到数据增强想到的是图像领域的旋转、翻转、裁剪。但 AI 项目里大量场景是结构化表格数据比如风控、金融、医疗、广告点击预测这类数据里的“样本不足”往往表现为类别不平衡正样本少得可怜。SMOTE 是解决这个问题的经典方法。核心逻辑是在特征空间中寻找少数类样本的近邻用线性插值的方式生成人工样本。比如少数类样本 A 和它最近邻样本 BSMOTE 会在 A、B 连线上随机取点合成一个新的少数类样本。这样做的好处是生成样本不是无中生有而是沿着真实样本的局部分布延伸能有效缓解过拟合。代码实现很简单from imblearn.over_sampling import SMOTE from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy ) smote SMOTE(random_state42, k_neighbors5) X_train_res, y_train_res smote.fit_resample(X_train, y_train) print(增强前:, X_train.shape, 增强后:, X_train_res.shape)这里 k_neighbors 默认 5我建议除非有明确理由否则不要随便调大。k 太大生成的样本容易侵入多数类区域制造边界噪声k 太小又容易把个别离群点放大。类别极度不平衡时可以下调到 3 或 4让插值更保守。SMOTE 还有几个变体值得关注。BorderlineSMOTE 只对靠近边界的少数类样本做插值生成的新样本更有判别价值适合边界模糊的分类问题。ADASYN 则针对难学习的样本加权生成越难分类的少数类样本生成数量越多。这几个方法的取舍核心是看样本分布是均匀边界还是交错边界建议做实验对比而不是凭直觉选。3.2 图像、文本、时序数据的增强各有各的招增强思路不止适用于表格数据。在图像领域随机裁剪、水平翻转、旋转、亮度对比度调整是常规操作PyTorch 和 TensorFlow 里都内置了 transform 模块。在文本领域常见做法是同义词替换、随机插入、回译把中文翻译成英文再翻译回中文能生成语义相近但表达不同的新句子。在时序数据里可以加入高斯噪声、时间缩放、幅值偏移用于增强传感器信号。这些方法的共同点都是“在保真和多样性之间找平衡”。生成样本不能离原始分布太远否则模型学到的不是数据的真实结构而是人为制造的伪模式。我用过一个比较土但有效的办法增强后把生成样本单独拎出来画分布和原始样本分布重叠度高说明增强合理如果生成样本分布漂移明显说明增强参数过猛需要回调强度。3.3 增强的纪律先验证再上线增强处理有一个纪律必须守住只对训练集做增强验证集和测试集必须保持原始分布。原因很简单如果你在验证集里放入了人工合成样本那评估就不客观了模型在“开卷考试”里拿高分真实场景里却可能跟不上。另一个经验不要追求把少数类补齐到和多数类完全一样多。重采样比例太高分类器会把概率推向一个过度膨胀的状态误报率飙升。我的做法是分别尝试 0.5、0.75、1.0 三档把少数类比例调到多数类的一半到四分之三之间用交叉验证选最优档位。这样既补充了样本又没有破坏类别本来的信息量。4. 数据标准化给所有特征一个公平的起跑线4.1 为什么要做标准化梯度下降和距离计算很多算法依赖特征之间的距离或者梯度方向。KNN 要算欧氏距离SVM 要优化超平面间隔线性回归和神经网络要走梯度下降聚类要算样本间相似度。这些方法都有一个共同前提特征应该是可比的。举个实际例子。假设特征 A 是年龄范围 20 到 40特征 B 是年收入范围 5 万到 100 万。KNN 计算两个样本的距离时年龄的差异在收入差异面前小到可以忽略模型几乎只看收入年龄维度直接被“压死”了。神经网络训练时也一样损失函数在尺度不均匀的特征空间里会变成一个狭长的椭圆梯度方向忽上忽下收敛速度极慢学习率也要调得小心翼翼。标准化之后的特征均值趋近于 0方差趋近于 1所有维度在同一个尺度上参与计算模型才能公平地利用每一个特征。4.2 StandardScaler、MinMaxScaler、RobustScaler 横向对比常用方法有三个适合不同场景。StandardScaler 用 (x - mean) / std 转换默认首选适合大多数模型尤其是线性类模型和神经网络MinMaxScaler 用 (x - min) / (max - min) 转换把数据压到 [0,1] 区间适合业务上要求数值非负或者需要限定取值范围的场景比如图像像素值、评分预测RobustScaler 用 (x - 中位数) / IQR 转换抗异常值能力强数据里噪声多时优先选它。方法转换方式适用场景StandardScaler(x - mean) / std最通用线性模型、神经网络首选MinMaxScaler(x - min) / (max - min)需要限定取值范围时对异常值敏感RobustScaler(x - median) / IQR存在较多异常值时更稳定MinMaxScaler 的坑在于它完全依赖 min 和 max只要数据里有一个巨大的离群点其他样本会被压到非常接近 0 的区域信息挤成一团。所以看到数据里有明显异常点时优先选 RobustScaler或者先做异常截断再上 MinMaxScaler。标准化代码实现如下from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_val_scaled scaler.transform(X_val)这里最关键的细节是验证集和测试集只调用 transform不调用 fit。fit 只能发生在训练集上。一旦在验证集上重新 fit就相当于让验证集自己参与了统计参数的计算评估结果就不可信了。4.3 顺序错误带来的数据泄漏我见过不少初学者把整个数据集丢给 scaler.fit_transform再去做 train_test_split。这种做法在数学上没错但逻辑上有严重问题测试集的均值和标准差会通过 scaler 的统计量“写进”训练时的数据分布里模型在训练阶段就已经偷看到了测试集的信息。这属于典型的数据泄漏。正确的顺序是切分 → fit on train → transform on both。如果用的是 sklearn Pipeline算法会自动保证这个顺序。除了标准化任何需要在全量数据上计算统计量再切分的操作都有同样风险比如缺失值均值填充如果用全量数据的均值填训练集也算轻微泄漏。规范做法是先 fit 一个 imputer 到训练集再用同一个 imputer 去 transform 验证集和测试集。5. 全流程工程化把清洗、增强、标准化串成一条 Pipeline5.1 先固化流程再谈调参项目进行到一定阶段预处理逻辑就不要再靠手写脚本一个个跑了。sklearn 提供 Pipeline 机制可以把“填充缺失值 → 标准化 → 编码 → 建模”串成一个对象。好处有两点第一流程可复用换数据集时不用重写整套代码第二防止训练集和测试集处理方式不一致Pipeline 会自动保证 transform 逻辑统一。先定义数值列和类别列的处理路径from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.impute import SimpleImputer from sklearn.ensemble import RandomForestClassifier from imblearn.pipeline import Pipeline as ImbPipeline num_cols [age, amount, score] cat_cols [city, channel] num_pipe Pipeline([ (imputer, SimpleImputer(strategymedian)), (scaler, StandardScaler()) ]) cat_pipe Pipeline([ (imputer, SimpleImputer(strategymost_frequent)), (encoder, OneHotEncoder(handle_unknownignore)) ]) preprocessor ColumnTransformer([ (num, num_pipe, num_cols), (cat, cat_pipe, cat_cols) ]) model ImbPipeline([ (preprocess, preprocessor), (smote, SMOTE(random_state42)), (clf, RandomForestClassifier(n_estimators200, random_state42)) ]) model.fit(X_train, y_train) val_score model.score(X_val, y_val) print(验证集准确率:, val_score)这里我用的是 imblearn 的 Pipeline因为它原生支持 SMOTE 这类重采样中间步骤。注意 SMOTE 在 Pipeline 中的位置它必须位于 preprocessor 之后因为要先处理缺失值和编码才能对特征空间做插值。如果原始数据里还有 NaN 就直接跑 SMOTE会直接报错。5.2 训练集、验证集、测试集的拆分顺序和特殊场景前面反复提到切分顺序这里再强调一下时间序列场景。普通分类可以做随机切分但时序数据不能随机打乱必须按时间顺序切分否则模型会“看未来”。时序场景建议用 TimeSeriesSplit 或者手动按时间边界切比如前 80% 做训练后 20% 做验证。农业价格预测、设备趋势预警都适用这一条规则。切分还涉及一个容易被忽略的问题数据增强和特征工程如果在切分前做训练集的信息会通过生成样本泄漏到验证集。尤其是 SMOTE 这种基于局部分布生成样本的方法一旦生成样本同时出现在训练集和验证集模型的验证表现就没意义了。所以必须先切分再增强。5.3 端到端流程的六步走整理我自己的项目流程基本是六步数据接入 → 数据清洗 → 切分 → 增强 → 标准化与编码 → 建模评估。shapes 和数据字典也会在前两步里同步维护。清洗阶段花 60% 的预处理时间都不奇怪后面增强和标准化反而很快。这个顺序如果反了比如先标准化再增强后面就会发现各种统计量对不上模型效果一塌糊涂。还有一个团队协作的细节所有预处理脚本都放进项目仓库不要只留在 Jupyter Notebook 里。我在实际项目里见过太多次“我 Notebook 里跑得好好的怎么部署就崩了”——往往就是因为预处理逻辑散落各处没有固化成可复用的 Pipeline。6. 常见问题与排查技巧实录6.1 高频报错速查表下面这些报错基本是我在各个项目里反复遇到过、并且在社区问答里高频出现的。整理成一张速查表能省下不少排查时间报错信息常见原因解决方法ValueError: Input contains NaN缺失值未处理就送进模型先跑 df.isna().sum()填充或删除ValueError: Unknown label type标签列是字符串分类器不支持用 LabelEncoder 或 pd.Categorical 转码TypeError: not supported数值列里混入字符串用 pd.to_numeric(errorscoerce) 处理ValueError: could not convert string to float含单位或逗号的数字字符串自定义清洗函数去掉单位和千分位MemoryError数据一次性载入内存过大分块读取chunksize或转用 Polars/Spark模型训练快速报错列数不一致ColumnTransformer 列名配置错误打印 preprocessor.get_feature_names_out() 看输出验证集分数虚高线上崩标准化或增强泄漏到验证集检查是否只对训练集做 fit6.2 我在实际项目中反复验证过的经验第一数据预处理的投入产出比极高。大部分模型效果不佳的问题根源都在数据侧而不在算法侧。我做过一个网约车聚合数据的项目业务方催着上模型结果前期没有认真清洗异常的里程和时长记录第一版模型评估指标好看但线上坏单率迟迟降不下来。后来重新梳理清洗规则把异常里程、异常时长单独标记成特征模型才真正发挥作用。第二增强比例不要拍脑袋。SMOTE 之后一定要画图看分布生成样本和真实样本重叠太少说明插值参数不合适。我会在 Notebook 里把增强前后的 TSNE 降维图并排放着看直观判断增强是否合理。第三标准化参数要版本化。每次修改预处理脚本都要记录变更内容。项目时间拉长之后特别容易出现“这个模型之前跑的是 87现在还是 87但换了数据”这种不可复现的混乱版本记录能省下大量排查时间。第四不要迷信花哨的增强方法。我见过有人把文本回译、SMOTE、GAN 生成全堆上去结果模型效果没有明显提升反而训练时间翻倍。增强的本意是补充样本分布不是制造数据奇观。先用最简单的 SMOTE 跑一遍基线确认有收益再逐步尝试更复杂的方法。做数据预处理这几年我最大的体会是模型效果提升最快的往往不是换了更高级的算法而是把数据质量扎扎实实提上去。数据预处理这件事短期内看不到惊艳的成果但它决定了后续所有建模工作的上限。我现在拿到任何新项目第一周都只做三件事摸清字段含义、画出分布、写一份数据字典。这比急着训练模型有用得多。希望这篇全流程拆解能让你在开始建模之前多花点时间善待数据。