搞了半天模型效果上不去最后发现是喂给模型的数据本身就有问题——脏数据、缺失值、文本特征没转换、维度爆炸这大概是做机器学习项目最让人头大的经历。数据预处理这件事看着不起眼实际却是整个项目里最决定下限的环节。这篇文章我想把自己在这些年反复踩坑、整理出来的数据预处理方法完整讲一遍从数据清洗、数据转换到数据降维再结合一份可复用的思维导图把整个流程串起来帮大家少走弯路。我默认来看这篇文章的读者是正在学习机器学习、数据分析或者准备参加竞赛、做毕业设计的人。实操部分我会用Python和pandas、scikit-learn来演示代码给你写到可以直接跑的程度其中用到的知识点会讲清楚“为什么这么做”而不是只告诉你“怎么做”。1. 数据预处理到底在解决什么问题1.1 模型眼里容不下“脏数据”先说个最直观的比喻。做菜之前你得先摘菜、洗菜、切菜没人会把带着泥的菜直接丢进锅里。数据预处理就是机器学习项目里的“洗菜切菜”环节这一步不做或者做不好后面再厉害的算法、再耐心的调参都白搭。模型的本质是“从历史数据里学规律”。如果你喂给它的数据里有大量空值、明显错误的极端值、语义混乱的文本、数值范围天差地别的特征模型根本没法稳定学到真正的规律。比如一份销售订单数据里面有1000行记录其中“单价”列有80行是空的还有十几行单价写成了负数如果你直接把这份数据丢给模型训练模型会以为“负单价”也是一种正常模式预测出来的结果可想而知。所以业内一直有句话叫做“Garbage In, Garbage Out”——垃圾进垃圾出。数据预处理就是要把“垃圾”变成“干净、规整、可学习”的内容。1.2 几个最典型的业务场景我平时在实际项目中遇到最多的问题基本都是这几类。第一类是缺失值。系统上报的数据经常缺胳膊少腿要么是用户没填要么是接口传输丢了。第二类是重复数据。比如同一笔订单因为重试机制被记录了两次如果不做去重模型会被重复样本带偏还可能造成评估指标的虚高。第三类是数据格式和单位不统一。有人用“厘米”有人用“米”有人填“北京”有人填“北京市”算法是没法自动理解这些差异的。第四类是类别型特征。像“红色”“蓝色”“绿色”这种字符串很多算法根本不能直接处理必须转成数值或者进行独热编码。第五类是尺度差异过大。比如年龄是0到100收入是0到100万两者放到同一个模型里距离计算会被收入主导。第六类是维度爆炸。特征列比样本行还多模型很容易过拟合训练速度也很慢。你对照一下自己手头的数据大概率能命中好几条。这也说明数据预处理不是一个可选项而是必选项。1.3 不同角色对预处理的诉求不一样算法工程师关心的是“模型效果能不能提升”所以他们在预处理阶段更看重特征工程的潜力比如把时间戳拆成星期几、是否节假日等强特征。数据分析师关心的是“报表和结论是否可信”所以他们更看重去重、去异常值、统一口径。参加竞赛的选手则更狠会花大量时间在特征构造和清洗上因为竞赛里差0.001可能就是几百个名次的差距。不过不管你是哪种角色底层的预处理方法是一套通用的方法论。搞清楚了这套方法论再根据自己的场景做取舍就行。2. 动手之前先搭框架数据预处理的通用流程2.1 从原始数据到模型就绪数据的四个步骤我自己习惯把数据预处理分成四个阶段数据清洗、数据转换、数据降维、数据集成与规约。这也是很多资料里常见的划分方式网上搜“数据清洗和预处理”“数据预处理之数据转换”这些热词看到的基本都是这套框架。数据清洗负责处理缺失值、重复值、异常值目标是让数据“正确”。数据转换负责把数据变成算法能理解的形式包括文本转数值、无量纲化、特征编码等目标是让数据“可用”。数据降维负责减少特征数量、消除冗余目标是让数据“精简”。数据集成与规约负责把多张表合并、统一粒度、压缩数据量目标是让数据“好算”。四步不一定每次全做但每一步的方法你都应该掌握。真正动手的时候顺序一般是从“探索”开始的。拿到数据先不急着预处理先跑一下df.info()、df.describe()、df.head()看看数据长什么样心里有数再动手。2.2 工具链选型表格类数据我几乎都在用pandas做主处理配合NumPy做数值计算再用scikit-learn中的preprocessing、decomposition、feature_selection模块做转换和降维。pandas的优势在于API直观清洗操作写起来很快。scikit-learn的优势在于它提供了一套统一的fit/transform接口可以把预处理流程串成Pipeline训练和预测的时候不会出现“预处理不一致”的问题。图像类数据则是另一套工具链常用OpenCV和Pillow做缩放、裁剪、归一化训练深度学习模型时还会用PyTorch或TensorFlow自带的数据增强模块比如随机翻转、随机亮度调整等。2.3 环境准备下面我会用Python写演示代码你本地装好Python 3.8以上版本即可建议用Jupyter Notebook或VS Code的交互式窗口跑。需要安装的库是pandas、numpy、scikit-learn、matplotlib、seaborn一句话装完pip install pandas numpy scikit-learn matplotlib seaborn如果你用的是Anaconda这些库基本已经自带了可以直接开始。3. 核心实操用一份销售订单数据走完预处理全流程3.1 先造一份带“毛病”的原始数据理论讲多了容易飘直接上手才有感觉。我构造一份餐饮外卖订单数据故意往里面塞了各种问题缺失值、重复值、异常值、类别文本、量纲差异、高相关特征基本覆盖了预处理要处理的全部情况。import pandas as pd import numpy as np np.random.seed(42) n 1000 df pd.DataFrame({ order_id: range(1, n 1), user_id: np.random.randint(10000, 99999, n), order_amount: np.round(np.random.uniform(10, 200, n), 2), delivery_time_min: np.round(np.random.uniform(15, 90, n), 1), rating: np.random.choice([1, 2, 3, 4, 5], n, p[0.05, 0.1, 0.15, 0.3, 0.4]), food_type: np.random.choice([pizza, burger, sushi, salad, noodle], n), is_vip: np.random.choice([0, 1], n, p[0.6, 0.4]), distance_km: np.round(np.random.uniform(0.5, 20, n), 2), }) # 人为制造问题 df.loc[::37, order_amount] np.nan # 缺失值 df.loc[::53, delivery_time_min] np.nan # 缺失值 df.loc[::101, rating] np.nan # 缺失值 df.loc[::89, distance_km] np.random.uniform(100, 200) # 异常值 df pd.concat([df, df.iloc[:50]], axis0, ignore_indexTrue) # 重复50行 df.loc[::17, order_amount] -5 # 负金额异常 df.loc[::23, food_type] pizza # 脏文本这份数据里什么毛病都有了下面一步步处理。3.2 第一步数据清洗清洗阶段分三步走先去重再处理缺失值最后处理异常值。去重最简单drop_duplicates()一行搞定。注意要用inplaceTrue或者重新赋值不然不会生效df df.drop_duplicates() print(f去重后剩余: {len(df)} 行)缺失值处理要分情况讨论。如果某一列缺失比例超过50%这列基本没什么信息量了建议直接删掉强行填充只会制造大量人为噪声。如果缺失比例较小可以考虑删除缺失行或者用填充策略补上。填充策略有几种常见选择均值填充、中位数填充、众数填充、前向/后向填充、插值填充。数值型字段受异常值影响较大时用中位数更稳健类别型字段用众数填充时间序列数据用前向填充比较合理。订单金额的均值容易受异常值影响所以这里我用中位数填充配送时间同理# 先用中位数填充数值类缺失 df[order_amount] df[order_amount].fillna(df[order_amount].median()) df[delivery_time_min] df[delivery_time_min].fillna(df[delivery_time_min].median()) # 评分用众数填充 df[rating] df[rating].fillna(df[rating].mode()[0])异常值的检测方式很多最简单的有两种基于业务规则和基于统计分布。比如订单金额不可能为负数这是业务规则层面的异常直接过滤或替换。距离超过100公里的外卖也不太现实这种属于统计分布上的异常值可以用IQR四分位距或者Z-Score来判断。IQR方法判断异常值的逻辑是计算数据的Q125%分位和Q375%分位把小于Q1 - 1.5*IQR或大于Q3 1.5*IQR的点视为异常。比如订单金额正常分布大概在10到200之间异常点会落在很远的位置对于业务上明显不合理的负金额直接处理掉即可。# 负金额替换为正常区间中位数 df.loc[df[order_amount] 0, order_amount] df[order_amount].median() # 基于IQR处理距离异常 Q1 df[distance_km].quantile(0.25) Q3 df[distance_km].quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR df df[(df[distance_km] lower_bound) (df[distance_km] upper_bound)]注意异常值处理不是“见异常就删”有时候异常值本身代表着有价值的业务信号比如欺诈订单、设备故障记录。你需要在理解业务的前提下去判断是删除、替换还是单独拎出来作为一个类别。3.3 第二步数据转换清洗完的数据还不能直接训练因为算法只认数值不认字符串。food_type列是类别型文本 pizza 还带着多余空格先清洗掉再进行编码。常见的编码方式有两种标签编码和独热编码。标签编码适合有序类别比如“低中高”可以编码为0、1、2独热编码适合无序类别比如食物类型编码后每一类变成一个0/1列避免模型错误地认为“pizza1burger2”之间存在大小关系。# 先去掉文本中的多余空格 df[food_type] df[food_type].str.strip() # 独热编码 df pd.get_dummies(df, columns[food_type], prefixfood)这里有个实际经验当类别取值很多比如城市名有几百个取值独热编码会让特征维度爆炸这时候可以考虑先用频次编码或者目标编码压缩维度。接下来是数值特征的无量纲化。order_amount、delivery_time_min、distance_km这三个字段的取值范围完全不同金额0到200配送时间15到90距离0.5到20。如果不做处理基于距离计算的模型比如KMeans、KNN、SVM会被数值范围大的字段主导数值范围小的字段基本不起作用。标准化和归一化是两种最常用的方法。MinMax归一化把所有值压缩到[0,1]区间计算公式是(x - min) / (max - min)适合数据本身有明确边界的场景。Z-Score标准化让数据变成均值为0、标准差为1的分布计算公式是(x - mean) / std适合数据分布近似高斯分布、存在离群点的场景。from sklearn.preprocessing import StandardScaler, MinMaxScaler # 方案一Z-Score标准化 scaler StandardScaler() df[[amount_scaled, time_scaled, distance_scaled]] scaler.fit_transform( df[[order_amount, delivery_time_min, distance_km]] ) # 方案二MinMax归一化 mms MinMaxScaler() df[[amount_minmax, time_minmax, distance_minmax]] mms.fit_transform( df[[order_amount, delivery_time_min, distance_km]] )用哪个更好我的习惯是如果后续用树模型随机森林、XGBoost、LightGBM不做标准化问题也不大因为树模型只关心特征分裂点不关心距离如果用神经网络、KMeans、KNN、SVM这类对尺度敏感的方法标准化是必须的。另外如果数据里有明显离群点Z-Score比MinMax更稳一点因为MinMax会被离群点拉到压缩到很小的区间。时间特征转换也很重要。如果你有订单时间戳直接拿“2024-06-01 12:30:45”这种字符串给模型模型啥也学不到。需要把它拆成年、月、日、星期、小时等周期性特征。比如外卖订单的配送时长和是否是周末关系很大把星期几提取出来就是有用的特征。# 假设有create_time列 df[create_time] pd.to_datetime(df.get(create_time, pd.Timestamp(2024-06-01 12:30:45))) df[hour] df[create_time].dt.hour df[weekday] df[create_time].dt.dayofweek df[is_weekend] df[weekday].apply(lambda x: 1 if x 5 else 0)3.4 第三步数据降维清洗和转换做完数据已经能用了但有时候特征太多会造成问题。数据降维就是在这时候出手的手段主要有两类特征选择和特征提取。特征选择是直接在原有特征里挑有用的丢掉没用的。最简单的方式是看相关系数如果两个特征高度相关比如相关系数大于0.8通常保留一个就行因为它们携带的信息高度重合。# 相关性矩阵 corr df[[order_amount, delivery_time_min, distance_km]].corr() print(corr)如果发现amount_minmax和amount_scaled这类同一字段的不同处理结果之间的相关系数是1那就不用想只留一个就够了。在实际业务中“商品价格”和“订单总价”往往高度相关类似情况都可以直接合并或删除。特征提取则是把原始特征通过数学变换压缩成少数几个新特征最常用的是PCA主成分分析。它的原理是把高维数据投影到方差最大的几个方向上用更少的维度保留尽可能多的信息。PCA做之前建议先做标准化否则方差大的特征会主导主成分方向。之后看“解释方差比”来决定保留几个主成分一般累计贡献率达到85%到95%就够用了。from sklearn.decomposition import PCA features [order_amount, delivery_time_min, distance_km, is_vip, rating] X df[features] from sklearn.preprocessing import StandardScaler X_scaled StandardScaler().fit_transform(X) pca PCA(n_components0.95) X_pca pca.fit_transform(X_scaled) print(f原始特征数: {X.shape[1]}, 保留主成分数: {X_pca.shape[1]}) print(f各主成分解释方差比: {pca.explained_variance_ratio_})我实际用的过程中的体会是PCA适合高维稠密数据降维但降维后的特征可解释性会变差每一个主成分都是原始特征的线性组合业务上很难讲清楚“这个主成分到底代表什么”。这也是为什么很多工业实践宁可做特征选择而不是PCA就是为了保留业务解释性。3.5 第四步数据集划分与输出预处理最后一步是把数据切成训练集和测试集然后输出成模型可以直接用的格式。这里特别强调一点所有基于训练集的统计量比如均值、中位数、标准化里的mean和std、PCA的投影矩阵必须先在训练集上计算好再应用到测试集上。不这么做就会造成数据泄露测试集不再可信。数据泄露是新手最容易犯的错误而且特别隐蔽。比如你全量数据标准化后再切分测试集的信息已经通过均值和标准差“泄露”给了模型模型的评估结果会虚高上线后立刻打回原形。from sklearn.model_selection import train_test_split X df[[amount_scaled, time_scaled, distance_scaled, is_vip, rating]] y df[is_vip] # 假设要预测的目标 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy )这时候最好的做法是把清洗、转换、降维封装在一个Pipeline里先对训练集fit再对训练集和测试集一起transform。scikit-learn的Pipeline就是干这个的以后有机会可以单独写一篇。4. 不同模态数据的预处理差异表格之外还要会这些4.1 图片数据的预处理很多人一开始接触的就是“图片训练数据预处理”尤其是类似ISIC 2017皮肤影像分割数据集这样的医学图像场景。图片数据和表格数据完全不同它的预处理核心是几何变换、颜色归一化、数据增强。第一步是统一尺寸。模型输入一般要求固定尺寸比如224x224你需要把所有图片裁剪或缩放到统一大小。第二步是像素值归一化。图片读进来是0到255的整数通常要先除以255变成0到1之间的浮点数用预训练模型时还要按模型的均值和标准差做标准化。第三步是数据增强包括随机翻转、旋转、裁剪、色彩抖动等相当于用现有数据“造”出更多变体提升模型的泛化能力。医学图像尤其讲究灰度归一化和对比度归一化因为不同设备拍摄的图片亮度差异很大直接训练会导致模型只记住亮度差异而不是病症特征。4.2 文本数据的预处理文本数据的预处理更偏NLP。中文需要分词英文按空格切分就行。分词后还要去除停用词比如“的”“了”“and”“the”这类高频但没实际含义的词。再往下是文本向量化常见的有TF-IDF和词嵌入。TF-IDF的核心思想是一个词在当前文本中出现越多同时在其他文本中出现越少它就越能代表当前文本的特征。入门阶段用TF-IDF足够深度学习阶段再考虑Word2Vec、BERT之类的预训练向量。4.3 表格数据预处理的“傻瓜式”清单如果你只想记住一份最常用的流程我建议按这个顺序操作先df.info()和df.describe()快速发现缺失值分布和数据范围问题。去掉完全重复的行检查有没有一列多值的情况。缺失值按比例决定删除或填充数值型优先中位数类别型优先众数。处理明显违背业务逻辑的异常值比如负数、超范围值。文本列去空格、统一大小写、处理同义表达。类别型做独热编码或标签编码。需要做距离计算的模型先做标准化或归一化。用相关性分析和PCA检查冗余特征。先切分数据再做任何基于全局统计量的处理。保存一份处理日志记录每一步做了什么方便复查和复现。5. 数据预处理常见问题与排查经验实录关于这部分的坑我整理了一张表全部来自我实际踩过的经验和带新人时看到的高频问题。常见问题可能原因排查思路处理方法模型训练时直接报错无法识别字符串类别特征没有编码df.dtypes查看类型做标签编码或独热编码特征数值差异过大模型效果差量纲不一致df.describe()查看std标准化或归一化训练集效果很好测试集崩溃预处理时用了全量数据统计量检查是否先切分再fit用Pipeline训练集fit后再transform测试集同一模型效果忽高忽低切分没有固定随机种子检查random_state是否设置固定random_state必要时设置多个种子取平均缺失值填充后模型没有提升填充策略不合理观察缺失值分布换中位数/分组填充/模型预测填充类别特征独热编码后维度爆炸类别基数太高统计unique数量改用频次编码/目标编码或先合并稀有类别图片训练时loss不下降像素没有归一化打印图像矩阵范围除以255或按均值标准差标准化避坑技巧一先看数据分布再决定填充策略很多人一拿到数据就fillna(0)或者fillna(df.mean())这是最偷懒但最危险的做法。某列缺失值如果是因为“不满足某个条件的人没有填写”那缺失本身就有信息量直接填充会掩盖这种模式。比如问卷里“是否有孩子”这列没结婚的人没填如果一律填充为“无”等于把未婚人士强行归入已婚无孩后续分析全部出错。更好的做法是先看缺失值的分布是否与某个特征相关如果真的相关可以把“是否缺失”单独作为一个特征参与建模或者按分组填充。避坑技巧二标准化和归一化别选错如果你用的是深度学习、SVM、KMeans这类对尺度敏感的模型标准化是标配。MinMax归一化适合数据范围明确的场景比如像素值0到255、评分1到5分Z-Score标准化适合存在离群点、数据范围不确定的场景。我遇到过有人对所有列都做StandardScaler结果离散的0/1特征也被标准化反而引入了负数取值模型解释起来很别扭。对于0/1这种二值特征通常不建议无量纲化保持原样就好。避坑技巧三时间特征的价值经常被忽略时间列在表格数据里是最常见的“沉睡特征”。直接在模型里丢一个2024-06-01 12:30:45没有意义但拆成“星期几”“小时”“是否节假日”之后预测价值往往非常高。做外卖订单预测的时候我试过只是新增一个“是否饭点”的特征模型AUC直接提升了2个百分点。避坑技巧四类别特征别无脑独热编码独热编码不是万能药。当类别取值超过几十个甚至几百个时独热编码会制造大量稀疏列训练慢、内存大模型还可能过拟合。这种情况下我一般先用分组统计替代比如用“该类别出现次数”“该类别对应的目标均值”来编码效果通常更好也更省空间。避坑技巧五多做预处理日志最后这点可能算不上技术但非常值得养成习惯。每处理一步记下来“我做了什么、为什么这么做”。因为数据预处理过程中有很多临时判断比如“订单金额的负值我替换成中位数了”如果不记录三个月后再看代码你根本不记得为什么这么干更不敢随便改。一份好的预处理日志比注释和文档都管用。6. 附赠一张可复用的数据预处理思维导图答应大家的思维导图我把结构完整写在这里。你自己用XMind、MindNode或者在线工具画都可以按这个结构展开里面标注“重点”的就是需要写代码注意的环节。数据获取与探索数据字典文档样本量与特征量统计缺失值分布图数值型分布直方图类别型频次表数据清洗重复值处理去重缺失值处理删除、均值/中位数/众数填充、KNN填充、插值异常值处理IQR、Z-Score、业务规则一致性检查单位、时区、编码数据转换类别编码标签编码、独热编码、目标编码数值转换MinMax归一化、Z-Score标准化、Log变换时间特征年、月、日、星期、小时、是否节假日文本特征分词、去停用词、TF-IDF、词向量数据降维特征选择过滤式、包裹式、嵌入式特征提取PCA、LDA相关性分析Pearson、Spearman数据集成与规约多表合并inner/left/right join数据聚合groupby、pivot_table采样随机采样、分层采样数据集划分与交付train_test_splitPipeline封装数据版本管理数据字典更新强烈建议你在实际项目里按这个结构做一份自己的思维导图然后在分支上补充每个项目特有的处理细节变成自己的工具箱。思维导图的意义不是“画一张好看的图”而是帮你形成处理数据的条件反射该检查什么、该处理什么完全不用再临时去想。另外数据降维和特征选择不是必须每次都用。如果你的特征本来就只有几个而且业务含义很清楚硬要做PCA反而会让特征失去解释性。我个人的判断标准是特征数量超过50或者明显存在高相关特征组才考虑降维否则优先特征选择。做数据预处理这些年我最大的感受是算法模型再高级也只是放大镜数据预处理才是决定放大镜能不能看清东西的那块镜片。你在清洗、转换、降维上花的心思最后都会变成模型性能里实实在在的优势。希望这套方法和这张思维导图能帮你把预处理这个环节彻底搞定。