☰
数据预处理全流程指南:从数据清洗到降维的实战方法
2026/9/30 15:23:09 网站建设 项目流程

搞了半天模型效果上不去,最后发现是喂给模型的数据本身就有问题——脏数据、缺失值、文本特征没转换、维度爆炸,这大概是做机器学习项目最让人头大的经历。数据预处理这件事,看着不起眼,实际却是整个项目里最决定下限的环节。这篇文章我想把自己在这些年反复踩坑、整理出来的数据预处理方法完整讲一遍,从数据清洗、数据转换到数据降维,再结合一份可复用的思维导图把整个流程串起来,帮大家少走弯路。

我默认来看这篇文章的读者,是正在学习机器学习、数据分析,或者准备参加竞赛、做毕业设计的人。实操部分我会用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]], axis=0, ignore_index=True) # 重复50行 df.loc[::17, 'order_amount'] = -5 # 负金额异常 df.loc[::23, 'food_type'] = ' pizza ' # 脏文本

这份数据里什么毛病都有了,下面一步步处理。

3.2 第一步:数据清洗

清洗阶段分三步走:先去重,再处理缺失值,最后处理异常值。

去重最简单,drop_duplicates()一行搞定。注意要用inplace=True或者重新赋值,不然不会生效:

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方法判断异常值的逻辑是:计算数据的Q1(25%分位)和Q3(75%分位),把小于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列,避免模型错误地认为“pizza=1,burger=2”之间存在大小关系。

# 先去掉文本中的多余空格 df['food_type'] = df['food_type'].str.strip() # 独热编码 df = pd.get_dummies(df, columns=['food_type'], prefix='food')

这里有个实际经验:当类别取值很多,比如城市名有几百个取值,独热编码会让特征维度爆炸,这时候可以考虑先用频次编码或者目标编码压缩维度。

接下来是数值特征的无量纲化。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_components=0.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_size=0.2, random_state=42, stratify=y )

这时候最好的做法是把清洗、转换、降维封装在一个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_split
    • Pipeline封装
    • 数据版本管理
    • 数据字典更新

强烈建议你在实际项目里按这个结构做一份自己的思维导图,然后在分支上补充每个项目特有的处理细节,变成自己的工具箱。思维导图的意义不是“画一张好看的图”,而是帮你形成处理数据的条件反射,该检查什么、该处理什么,完全不用再临时去想。

另外,数据降维和特征选择不是必须每次都用。如果你的特征本来就只有几个,而且业务含义很清楚,硬要做PCA反而会让特征失去解释性。我个人的判断标准是:特征数量超过50,或者明显存在高相关特征组,才考虑降维;否则优先特征选择。

做数据预处理这些年,我最大的感受是:算法模型再高级,也只是放大镜;数据预处理才是决定放大镜能不能看清东西的那块镜片。你在清洗、转换、降维上花的心思,最后都会变成模型性能里实实在在的优势。希望这套方法和这张思维导图能帮你把预处理这个环节彻底搞定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询