☰
数据预处理实战:缺失值、异常值与标准化编码全解析
2026/10/14 13:35:40 网站建设 项目流程

1. 数据预处理到底在解决什么问题

很多人刚接触数据清洗时,会把“清洗”和“预处理”混为一谈。我刚开始做数据分析那会儿也是这样,拿到一份CSV就直接上pandas的dropna(),结果把一半有用的样本给干掉了。后来踩了几次坑才明白,数据预处理的核心不是“删”,而是“转”——把原始数据转换成模型或分析工具能直接消化的形态。

打个生活化的比方:你从菜市场买回来一条鱼,数据清洗相当于去鳞、去内脏,而数据预处理相当于根据你要做的菜——清蒸、红烧还是做鱼丸——来决定切片还是剁块。鱼还是那条鱼,但形态决定了它能不能下锅。

这一篇主要聊的是数据预处理阶段最常打交道的几件事:缺失值处理、异常值识别、数据类型转换、标准化与归一化、以及类别特征的编码。这些操作看起来零散,但实际项目中它们往往是一条流水线上的不同工位,顺序错了或者方法选错了,后面模型跑出来的结果就会莫名其妙地差。

适合谁看?如果你已经会用pandas读写文件、会做基本的筛选和聚合,但一到“这份数据到底该怎么处理才能喂给模型”就犯怵,那这篇内容就是写给你的。我会把每个环节的选型逻辑、参数计算、实操代码和踩坑经验都摊开讲,尽量让你看完就能直接套用到自己的项目里。

2. 缺失值处理:先搞清楚“为什么缺”,再决定“怎么补”

2.1 缺失值的三种类型与判断逻辑

缺失值不是铁板一块。统计学上通常把它分成三类:完全随机缺失(MCAR)、随机缺失(MAR)和非随机缺失(MNAR)。这个分类听起来学术,但判断起来其实有很实用的土办法。

完全随机缺失,指的是缺失跟任何变量都没关系,纯粹是记录时漏了、传感器抽风了。比如一份问卷里某几行年龄没填,跟性别、收入都不相关。这种缺失直接删掉或者用均值补,偏差最小。

随机缺失,指的是缺失跟其他观测到的变量有关。比如收入字段的缺失可能跟学历有关——学历低的人更不愿意填收入。这种情况下,如果你直接删掉缺失行,样本就会偏向高学历群体,结论就歪了。

非随机缺失最麻烦,缺失本身就跟缺失的值有关。比如高收入人群更倾向于不填收入,你看到的缺失背后藏着一个“高收入”的信号。这种缺失用常规方法补出来的值都是有偏的,需要建模专门处理。

实操中怎么判断?我一般会先跑一个缺失值的相关性热力图,看看缺失指示变量跟其他字段的相关性。如果相关性都很低,基本可以按MCAR处理;如果某些字段相关性明显,就要警惕MAR或MNAR。

import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt # 构造缺失指示矩阵 df = pd.read_csv('your_data.csv') missing_indicator = df.isnull().astype(int) # 看缺失字段之间的相关性 plt.figure(figsize=(10, 8)) sns.heatmap(missing_indicator.corr(), annot=True, cmap='coolwarm', center=0) plt.title('缺失指示变量相关性热力图') plt.show()

注意:缺失指示矩阵的相关性只能反映缺失字段之间的关联,要判断跟观测值的关系,还需要把缺失指示变量跟原始字段做相关性分析。

2.2 删除法的适用边界与风险

删除法是最省事的,但也是最容易出事的。我见过一个项目,原始数据十万行,做完dropna()只剩两万行,模型效果怎么调都上不去,最后发现是删掉了大量关键样本。

删除法分两种:行删除和列删除。行删除适用于缺失比例很低(一般低于5%)且属于MCAR的情况;列删除适用于某个字段缺失比例极高(比如超过80%)且该字段对分析目标不重要的情况。

这里有个经验公式可以参考:如果某字段缺失比例超过70%,且该字段不是核心目标变量,优先考虑整列删除;如果缺失比例在5%到70%之间,建议用填充法;如果低于5%,行删除的代价通常可以接受。

但要注意,行删除会改变样本的分布。删之前最好对比一下删除前后关键字段的均值、方差有没有明显漂移。如果漂移明显,说明删除引入了选择偏差,得换方法。

# 删除前的分布检查 before = df['key_feature'].describe() # 执行行删除 df_dropped = df.dropna(subset=['key_feature']) # 删除后的分布检查 after = df_dropped['key_feature'].describe() print(pd.DataFrame({'删除前': before, '删除后': after}))

2.3 填充策略的选型与参数计算

填充法才是预处理阶段的重头戏。常见的填充策略有均值、中位数、众数、前向填充、后向填充、插值、以及基于模型的填充。选哪种,取决于字段的分布特性和业务含义。

均值填充适合近似正态分布且缺失比例不高的数值字段。但均值的缺点是会压缩方差,让数据看起来比实际更“集中”。如果字段有明显的偏态,均值会被极端值拉偏,这时候用中位数更稳。

众数填充适合类别字段,但要注意如果某个类别占比极高,众数填充会让这个类别的比例进一步膨胀,可能掩盖真实的类别分布。

前向填充和后向填充适合时间序列数据,比如传感器读数、股价。但前向填充有个隐患:如果缺失是连续一大段,填充出来的值会是一条直线,模型会误以为这段时间没有变化。

插值法(线性插值、多项式插值、样条插值)也适合时间序列,比前向填充更平滑。线性插值计算简单,适合变化平缓的序列;样条插值适合波动较大的序列,但计算量大,且可能在边界处产生震荡。

基于模型的填充(如KNN填充、迭代填充)适合字段之间相关性强的场景。KNN填充的思路是:找到跟缺失样本最相似的K个邻居,用邻居的值来估计。K的取值一般从5开始试,太小容易受噪声影响,太大则失去局部性。

from sklearn.impute import KNNImputer from sklearn.experimental import enable_iterative_imputer from sklearn.impute import IterativeImputer # KNN填充 knn_imputer = KNNImputer(n_neighbors=5) df_knn = pd.DataFrame(knn_imputer.fit_transform(df), columns=df.columns) # 迭代填充(基于回归) iter_imputer = IterativeImputer(max_iter=10, random_state=42) df_iter = pd.DataFrame(iter_imputer.fit_transform(df), columns=df.columns)

实操心得:KNN填充和迭代填充都会改变字段的原始分布,填充后一定要重新做一次分布对比。我一般会画填充前后的核密度图,如果两条曲线形状差异很大,说明填充引入了过多人工痕迹,得换更保守的方法。

2.4 缺失值处理的顺序陷阱

很多人不知道,缺失值处理的顺序会影响最终结果。比如你先做了归一化再填充,那填充的均值是归一化后的均值,跟原始尺度的均值不是一回事。正确的顺序应该是:先处理缺失值,再做标准化/归一化。

另外,如果同时存在缺失值和异常值,建议先处理异常值再填充缺失值。因为异常值会拉偏均值和中位数,用被污染的中心值去填充,等于把异常的影响扩散到了缺失位置。

3. 异常值识别:别急着删,先搞清楚它是“错误”还是“信号”

3.1 异常值的两种来源与处理原则

异常值分两种:一种是数据错误,比如年龄填了200岁、温度传感器故障读出了-999;另一种是真实极端值,比如某天销售额突然暴涨,背后可能是促销活动或突发事件。

数据错误必须处理,真实极端值则要谨慎。我见过一个风控项目,把大额交易当成异常值删掉了,结果模型对高风险交易的识别率直接掉了一半——因为大额交易本身就是风险信号。

判断原则很简单:如果这个极端值在业务上说得通,就保留;如果说不通,就处理。说不通的情况包括:超出物理可能范围(如负身高)、超出业务规则范围(如订单金额为负)、以及明显的记录错误(如日期格式错乱)。

3.2 基于统计的异常检测方法

最常用的是Z-score法和IQR法。

Z-score法假设数据近似正态分布,计算每个点偏离均值多少个标准差。通常把|Z| > 3的点视为异常。但Z-score对极端值本身很敏感——极端值会拉高均值和标准差,导致自己反而不容易被检测出来。

IQR法(四分位距法)更稳健。计算Q1(25%分位)和Q3(75%分位),IQR = Q3 - Q1,把小于Q1 - 1.5×IQR或大于Q3 + 1.5×IQR的点视为异常。1.5这个系数是经验值,如果数据本身波动大,可以放宽到3。

# Z-score法 from scipy import stats z_scores = np.abs(stats.zscore(df['value'])) outliers_z = df[z_scores > 3] # IQR法 Q1 = df['value'].quantile(0.25) Q3 = df['value'].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR outliers_iqr = df[(df['value'] < lower_bound) | (df['value'] > upper_bound)]

注意:IQR法对偏态分布的数据也会误判。比如收入数据右偏严重,高收入人群很容易被IQR判为异常。这时候可以先做对数变换,再跑IQR。

3.3 基于业务规则的异常检测

统计方法只能告诉你“这个点跟大多数点不一样”,但没法告诉你“这个点是不是错的”。业务规则才能补上这一环。

比如电商订单数据,业务规则可能包括:订单金额不能为负、下单时间不能晚于发货时间、同一用户短时间内不能有大量重复订单。这些规则用简单的条件筛选就能实现,但效果往往比统计方法更精准。

# 业务规则异常检测示例 df['is_abnormal'] = False # 规则1:金额为负 df.loc[df['amount'] < 0, 'is_abnormal'] = True # 规则2:发货时间早于下单时间 df.loc[df['ship_time'] < df['order_time'], 'is_abnormal'] = True # 规则3:同一用户一小时内下单超过10次 df['order_hour'] = pd.to_datetime(df['order_time']).dt.floor('H') user_hour_count = df.groupby(['user_id', 'order_hour']).size().reset_index(name='count') abnormal_users = user_hour_count[user_hour_count['count'] > 10][['user_id', 'order_hour']] df = df.merge(abnormal_users, on=['user_id', 'order_hour'], how='left', indicator=True) df.loc[df['_merge'] == 'both', 'is_abnormal'] = True df.drop(columns=['_merge'], inplace=True)

3.4 异常值的处理方式与影响评估

识别出异常值之后,处理方式主要有四种:删除、截断、替换、保留并标记。

删除适合数据错误且数量极少的情况。截断(Winsorization)是把超出边界的值拉回到边界上,适合不想删样本但又想削弱极端值影响的情况。替换是用均值、中位数或模型预测值替代异常值,适合异常值明显是记录错误的情况。保留并标记是新增一个布尔字段标记异常,让模型自己决定怎么用,适合不确定异常值是否真实的情况。

处理完之后,一定要对比处理前后的统计指标和模型效果。我一般会跑一个A/B对比:用处理前的数据训练一版模型,用处理后的数据训练一版,看验证集上的指标变化。如果处理后指标反而下降,说明可能删掉了有用的信号。

4. 数据类型转换与标准化:让数据“说同一种语言”

4.1 数据类型转换的常见场景

原始数据里的类型往往不是我们想要的。比如日期可能是字符串、数值可能是带逗号的文本、布尔值可能是“是/否”或“Y/N”。这些都需要在预处理阶段统一。

日期转换是最常见的。pd.to_datetime()能处理大部分格式,但遇到混合格式(比如有的行是“2024-01-01”,有的是“01/01/2024”)就会报错。这时候可以用format='mixed'参数,或者先统一格式再转换。

数值转换的坑更多。比如“1,234.56”这种带千分位的字符串,直接astype(float)会报错,得先str.replace(',', '')。还有“10%”这种带百分号的,得先去掉百分号再除以100。

# 日期转换 df['date'] = pd.to_datetime(df['date'], format='mixed', errors='coerce') # 带千分位的数值转换 df['amount'] = df['amount'].str.replace(',', '').astype(float) # 百分比转换 df['rate'] = df['rate'].str.replace('%', '').astype(float) / 100

实操心得:errors='coerce'是个好东西,它会把转换失败的值变成NaT或NaN,而不是直接报错中断。转换完之后记得检查一下有多少变成了缺失值,如果比例很高,说明原始格式比你想的复杂,得回去看看原始数据长什么样。

4.2 标准化与归一化的选型逻辑

标准化(Standardization)和归一化(Normalization)经常被混用,但它们解决的是不同问题。

标准化是把数据转换成均值为0、标准差为1的分布,公式是(x - mean) / std。它不改变数据的分布形状,只改变尺度。适合大多数基于距离的算法(如KNN、SVM)和基于梯度的算法(如线性回归、神经网络)。

归一化是把数据缩放到[0, 1]区间,公式是(x - min) / (max - min)。它会把所有值压缩到固定范围,适合需要固定输入范围的算法(如图像处理中的像素值)和某些对输入范围敏感的神经网络。

选哪个?我的经验是:如果数据近似正态分布,用标准化;如果数据有明确边界且分布不均匀,用归一化。另外,如果数据中存在极端值,归一化会被极端值主导——因为min和max都被极端值决定了,中间的值会被压缩到很窄的区间。这时候标准化更稳。

from sklearn.preprocessing import StandardScaler, MinMaxScaler # 标准化 scaler_std = StandardScaler() df_std = pd.DataFrame(scaler_std.fit_transform(df[numeric_cols]), columns=numeric_cols) # 归一化 scaler_minmax = MinMaxScaler() df_minmax = pd.DataFrame(scaler_minmax.fit_transform(df[numeric_cols]), columns=numeric_cols)

4.3 标准化与归一化的参数计算与陷阱

标准化的参数是均值和标准差,归一化的参数是最小值和最大值。这些参数必须只在训练集上计算,然后应用到验证集和测试集。如果在整个数据集上计算,就会造成数据泄露——测试集的信息通过参数泄露到了训练过程中。

这个坑我踩过不止一次。有一次做房价预测,我在全量数据上做了归一化,然后划分训练测试集,结果测试集上的R²高得离谱,后来才发现是泄露了。

from sklearn.model_selection import train_test_split # 先划分数据集 X_train, X_test = train_test_split(df[numeric_cols], test_size=0.2, random_state=42) # 只在训练集上fit scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 注意这里是transform,不是fit_transform

注意:fit_transform和transform的区别是新手最容易搞混的。fit_transform会计算参数并转换,transform只用已有参数转换。测试集必须用transform。

4.4 类别特征的编码方式选择

类别特征不能直接喂给模型,需要编码成数值。常见的编码方式有:标签编码、独热编码、目标编码、频率编码。

标签编码是把每个类别映射成一个整数,适合有序类别(如“低/中/高”)。但如果是无序类别,标签编码会引入虚假的大小关系,让模型误以为“类别3 > 类别1”。

独热编码是为每个类别创建一个二进制列,适合类别数较少(一般少于10个)的无序类别。缺点是如果类别数很多,会产生大量稀疏列,增加计算负担,还容易过拟合。

目标编码是用类别的目标均值来替换类别值,适合高基数类别(如用户ID、商品ID)。但目标编码容易造成目标泄露,必须用交叉验证的方式计算,或者加平滑项。

频率编码是用类别的出现频率来替换类别值,适合类别分布本身有信息量的场景。比如在欺诈检测中,某些类别的出现频率异常低,可能本身就是信号。

# 标签编码 from sklearn.preprocessing import LabelEncoder le = LabelEncoder() df['category_encoded'] = le.fit_transform(df['category']) # 独热编码 df_onehot = pd.get_dummies(df, columns=['category'], drop_first=True) # 目标编码(带平滑) def target_encode(df, col, target, smoothing=10): global_mean = df[target].mean() agg = df.groupby(col)[target].agg(['mean', 'count']) smooth = (agg['mean'] * agg['count'] + global_mean * smoothing) / (agg['count'] + smoothing) return df[col].map(smooth) df['category_target_encoded'] = target_encode(df, 'category', 'target')

实操心得:独热编码的drop_first=True参数可以避免多重共线性,在线性模型中很有用。但在树模型中,保留所有列也没关系,因为树模型对共线性不敏感。

5. 实操全流程:从原始数据到模型可用的特征矩阵

5.1 完整预处理流水线的搭建

把前面讲的环节串起来,一个完整的预处理流水线大概长这样:

  1. 加载数据,检查基本信息和缺失情况
  2. 处理数据类型转换(日期、数值、文本)
  3. 识别并处理异常值(统计方法+业务规则)
  4. 处理缺失值(根据缺失类型选填充策略)
  5. 类别特征编码
  6. 数值特征标准化/归一化
  7. 特征筛选与降维(可选)
  8. 保存预处理后的数据

这个顺序不是死的。比如如果异常值会影响缺失值的填充中心,就要先处理异常值;如果某些字段需要先编码才能计算相关性,就要先编码。

import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler, LabelEncoder from sklearn.impute import KNNImputer def preprocess_pipeline(df, target_col, numeric_cols, category_cols): df = df.copy() # 1. 数据类型转换 for col in df.columns: if 'date' in col.lower() or 'time' in col.lower(): df[col] = pd.to_datetime(df[col], format='mixed', errors='coerce') # 2. 异常值处理(IQR截断) for col in numeric_cols: Q1 = df[col].quantile(0.25) Q3 = df[col].quantile(0.75) IQR = Q3 - Q1 lower = Q1 - 1.5 * IQR upper = Q3 + 1.5 * IQR df[col] = df[col].clip(lower, upper) # 3. 缺失值填充 for col in numeric_cols: if df[col].isnull().sum() > 0: df[col] = df[col].fillna(df[col].median()) for col in category_cols: if df[col].isnull().sum() > 0: df[col] = df[col].fillna(df[col].mode()[0]) # 4. 类别编码 for col in category_cols: le = LabelEncoder() df[col] = le.fit_transform(df[col].astype(str)) # 5. 划分数据集 X = df.drop(columns=[target_col]) y = df[target_col] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 6. 标准化(只在训练集上fit) scaler = StandardScaler() X_train[numeric_cols] = scaler.fit_transform(X_train[numeric_cols]) X_test[numeric_cols] = scaler.transform(X_test[numeric_cols]) return X_train, X_test, y_train, y_test, scaler

5.2 预处理效果的验证方法

预处理做完不能直接扔给模型,得先验证一下效果。我一般会做三件事:

第一,分布对比。画预处理前后关键字段的直方图或核密度图,看分布形状有没有明显变化。如果标准化后分布形状变了,说明标准化过程有问题。

第二,缺失值复查。预处理后应该没有缺失值了,如果有,说明填充逻辑有漏洞。

第三,模型快速验证。用一个简单的模型(比如逻辑回归或决策树)跑一下,看验证集指标是否合理。如果指标异常低,可能是预处理引入了噪声;如果异常高,可能是数据泄露。

# 分布对比 fig, axes = plt.subplots(1, 2, figsize=(12, 4)) df['value'].hist(ax=axes[0], bins=50) axes[0].set_title('预处理前') X_train['value'].hist(ax=axes[1], bins=50) axes[1].set_title('预处理后') plt.show() # 缺失值复查 print(X_train.isnull().sum().sum()) print(X_test.isnull().sum().sum())

5.3 预处理流水线的保存与复用

实际项目中,预处理参数(均值、标准差、编码映射)需要保存下来,以便对新数据做同样的处理。sklearn的Pipeline和ColumnTransformer可以帮我们把这些步骤封装起来,保存成文件,下次直接加载。

from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.impute import SimpleImputer import joblib # 构建列转换器 numeric_transformer = Pipeline(steps=[ ('imputer', SimpleImputer(strategy='median')), ('scaler', StandardScaler()) ]) categorical_transformer = Pipeline(steps=[ ('imputer', SimpleImputer(strategy='most_frequent')), ('onehot', OneHotEncoder(handle_unknown='ignore')) ]) preprocessor = ColumnTransformer( transformers=[ ('num', numeric_transformer, numeric_cols), ('cat', categorical_transformer, category_cols) ]) # 保存 joblib.dump(preprocessor, 'preprocessor.pkl') # 加载并复用 preprocessor_loaded = joblib.load('preprocessor.pkl') X_new_processed = preprocessor_loaded.transform(X_new)

注意:OneHotEncoder的handle_unknown='ignore'参数很重要。如果新数据里出现了训练时没见过的类别,不加这个参数会直接报错,加了之后会把这个类别编码成全零向量。

6. 常见问题与排查技巧实录

6.1 缺失值填充后模型效果反而变差

这是最常见的问题之一。原因通常有三个:一是填充方法选错了,比如用均值填充了偏态分布的数据;二是填充引入了虚假信号,比如用目标编码填充缺失值时泄露了目标信息;三是缺失本身有信息量,填充反而抹掉了这个信号。

排查思路:先对比填充前后的分布,再看填充值跟目标变量的相关性。如果填充值跟目标高度相关,说明填充过程泄露了信息。解决办法是改用不依赖目标的填充方法,或者把缺失指示变量作为一个新特征保留下来。

6.2 标准化后数据出现极端值

标准化后理论上数据应该在[-3, 3]左右,如果出现绝对值很大的值,说明原始数据里有极端值没处理干净。标准化的分母是标准差,如果标准差很小,稍微大一点的值标准化后就会变得很大。

解决办法:先处理异常值再标准化,或者改用RobustScaler。RobustScaler用中位数和四分位距代替均值和标准差,对极端值更稳健。

from sklearn.preprocessing import RobustScaler scaler = RobustScaler() X_scaled = scaler.fit_transform(X)

6.3 独热编码后特征维度爆炸

类别数多的时候,独热编码会产生大量列。比如一个用户ID字段有10万个不同值,独热编码后就是10万列,内存直接爆掉。

解决办法:对于高基数类别,改用目标编码或频率编码;或者先做类别合并,把低频类别归为“其他”;或者用嵌入(Embedding)方法在模型内部学习类别表示。

6.4 预处理顺序错误导致数据泄露

数据泄露是预处理阶段最隐蔽的坑。常见的泄露场景包括:在全量数据上计算标准化参数、在全量数据上做缺失值填充、在全量数据上做特征选择。

排查方法:检查所有fit操作是否只在训练集上执行。可以用Pipeline来强制保证顺序,因为Pipeline的fit和transform是分开的,不容易搞错。

6.5 常见问题速查表

问题现象可能原因排查方法解决方案
填充后模型变差填充方法不匹配分布对比填充前后分布改用中位数或模型填充
标准化后极端值原始异常值未处理检查标准化后Z-score先处理异常值或改用RobustScaler
独热编码维度爆炸类别数过多统计类别数目标编码或频率编码
测试集指标异常高数据泄露检查fit操作范围只在训练集上fit
新数据编码报错出现未知类别检查编码器参数设置handle_unknown='ignore'
时间序列填充成直线前向填充连续缺失检查缺失连续性改用插值法

实操心得:预处理阶段最值得花时间的不是写代码,而是理解数据。我一般会花至少30%的时间在数据探索上——看分布、看缺失模式、看异常值的业务含义。代码写错了可以改,但对数据的理解错了,后面所有步骤都是白费。

7. 一些容易被忽略的细节

7.1 时间特征的预处理

时间字段不只是转成datetime就完事了。从时间字段里可以提取出很多有用的特征:年、月、日、星期几、是否周末、是否节假日、季度、小时、分钟。这些特征对很多模型都有帮助。

但要注意,时间特征不能直接做标准化。比如“星期几”是循环特征,周一和周日只差一天,但数值上差了6。正确的做法是做循环编码:用sin和cos把周期特征映射到二维平面上。

df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24) df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24) df['dayofweek_sin'] = np.sin(2 * np.pi * df['dayofweek'] / 7) df['dayofweek_cos'] = np.cos(2 * np.pi * df['dayofweek'] / 7)

7.2 文本字段的预处理

如果数据里有文本字段,预处理还包括:去除HTML标签、去除特殊字符、统一大小写、分词、去除停用词、词干提取或词形还原。这些操作在NLP项目里是标配,但在结构化数据项目里容易被忽略。

我见过一个项目,用户评论字段里混着大量HTML标签和表情符号,直接做词频统计,结果“div”“span”这些标签词排到了前面。后来加了清洗步骤,效果才正常。

7.3 预处理的可复现性

预处理过程必须可复现。这意味着:随机种子要固定、参数要保存、代码要版本控制。我一般会把预处理脚本和参数文件一起提交到代码仓库,确保任何时候都能复现出同样的结果。

另外,预处理后的数据最好也保存一份,并记录预处理的时间、参数、代码版本。这样如果后面模型效果出问题,可以回溯到具体是哪一步预处理导致的。

7.4 预处理与特征工程的边界

预处理和特征工程有时候界限模糊。我的划分是:预处理是“让数据可用”,特征工程是“让数据更好用”。缺失值填充、类型转换、标准化属于预处理;特征交叉、多项式特征、聚合统计属于特征工程。

但实际项目中不用太纠结这个边界。重要的是每一步操作都有明确的理由,并且能解释清楚它对模型的影响。

8. 个人经验总结

做了这么多数据清洗和预处理的项目,我最大的体会是:预处理没有标准答案,只有适合当前数据和当前目标的方案。同样一份数据,做分类任务和做回归任务,预处理方式可能完全不同;用线性模型和用树模型,预处理方式也可能不同。

另一个体会是:不要过度预处理。我早期做项目时,恨不得把所有能做的预处理都做一遍,结果反而把数据里的信号给洗没了。后来学乖了,每次只做必要的预处理,做完之后用简单模型验证一下,如果效果没提升甚至下降,就回退。

最后分享一个实用技巧:把预处理步骤写成函数,每个函数只做一件事,然后用Pipeline串起来。这样调试的时候可以单独跑某个步骤,看它的输入输出,比写成一坨代码好排查多了。而且Pipeline可以保存和复用,新数据来了直接transform就行,不用重新写一遍逻辑。

这个系列后面还会继续聊特征工程和模型调优的内容,感兴趣的话可以接着看。

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

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

立即咨询