数据预处理:缺失值处理、标准化、数据类型转换
数据预处理,说白了就是给数据“洗澡”。你拿到的原始数据,往住缺胳膊少腿、单位不统一、类型还乱七八糟。不做预处理就直接跑异常值检测?结果一定是灾难。
我个人习惯把预处理分成三步走:缺的补上、拉的缩回来、乱的捋顺。每一步都有坑,我在项目里都踩过,咱们一个一个说。
1. 缺失值处理
老手看数据,第一眼都是查缺失。我入行时犯过一个低级错误:用均值填充了所有缺失值,结果把异常值检测的阈值整体拉偏了——后来复盘发现,那批缺失根本不是随机的,而是传感器超量程后自动置空的。教训深刻。
1.1 检测缺失
import pandas as pd
import numpy as np
# 快速概览
print(df.info())
# 每列缺失数量
print(df.isnull().sum())
# 缺失占比
print(df.isnull().mean())
为什么先查缺失占比?如果一个特征缺失超过70%,我通常会直接丢掉。但如果它是业务强相关的字段(比如用户ID),那就得想办法补。
1.2 处理方法
| 方法 | 做法 | 我什么时候用 |
|---|---|---|
| 直接删除 | dropna() 删行或删列 | 缺失量少(<5%),且随机分布 |
| 固定值填充 | fillna(0) 或 fillna('未知') | 分类特征,缺失本身可代表一种类别 |
| 统计量填充 | 均值、中位数、众数 | 数值型,且缺失机制为随机(MCAR) |
| 前向/后向填充 | ffill() / bfill() | 时间序列,丢失的是中间几个点 |
| 插值填充 | interpolate() | 序列数据,趋势明显,线性插值效果好 |
| 模型预测填充 | KNN / 回归模型 | 缺失量不大,但特征间相关性较强 |
说到代码实现,有个小细节容易翻车。看例子:
# 错误示范——原地修改,没有视图反馈
df.fillna(df.mean(), inplace=True)
# 我的习惯——先复制再填充
df_clean = df.copy()
df_clean['age'] = df_clean['age'].fillna(df_clean['age'].median())
# 如果是时间序列
df_clean['temperature'] = df_clean['temperature'].interpolate(method='linear')
嗯,这里要注意:inplace=True 虽然省事,但会丢失原始数据。异常值检测经常要回溯对比,我一般保留原始列,新建一列放处理后的值。
2. 标准化(归一化)
标准化不是数学游戏,是真有用。你想想看,“年龄 0~100”和“收入 3000~50000”两个特征,量级差了500倍。异常值检测算法大多依赖距离或密度,大数会把小数吃掉。
2.1 主流方法对比
| 方法 | 公式核心 | 输出范围 | 抗离群值 | 我的首选场景 |
|---|---|---|---|---|
| Z-score | (x - μ) / σ | 无固定边界 | 弱 | 数据近似正态分布 |
| Min-Max | (x - min) / (max - min) | [0, 1] | 极弱 | 已知上下界,无量纲均一化 |
| Robust | (x - median) / IQR | 无固定边界 | 强 | 数据含离群值,且我不打算删 |
| MaxAbs | x / |max| | [-1, 1] | 弱 | 稀疏数据或中心在0附近 |
我在做金融交易异常检测时踩过一个坑:有一批佣金数据包含极端大额交易(正常值的几百倍),用 Min-Max 归一化后,90%的正常数据被压到了 0~0.05 之间,几乎失去区分度。后来改用 Robust 标准化,问题解决。
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
# Z-score 标准化
scaler_z = StandardScaler()
df['age_z'] = scaler_z.fit_transform(df[['age']])
# Robust 标准化(推荐含离群值的数据)
scaler_r = RobustScaler()
df['income_robust'] = scaler_r.fit_transform(df[['income']])
# Min-Max 归一化
scaler_mm = MinMaxScaler()
df['score_norm'] = scaler_mm.fit_transform(df[['score']])
还有个常见问题:分训练集和测试集时,标准化参数该用谁的?记住一条铁律——只用训练集的统计量去 fit,然后 transform 训练集和测试集。我曾经见过有人把测试集和训练集拼在一起 fit,结果测试集信息泄露,上线后准确率直接崩了。
from sklearn.model_selection import train_test_split
X_train, X_test = train_test_split(df, test_size=0.2, random_state=42)
scaler = RobustScaler()
X_train_scaled = scaler.fit_transform(X_train) # 只 fit 训练集
X_test_scaled = scaler.transform(X_test) # 用训练集的参数转测试集
3. 数据类型转换
这个环节最容易被忽略,但它往往是 bug 的温床。我记得有次跑 Isolaction Forest,死活不出结果,查了半天——有个“用户等级”列存的是字符串“1”、“2”、“3”,模型把它当成类别特征直接扔掉。白白浪费半天时间。
3.1 数值类型统一
# 查看每列类型
print(df.dtypes)
# 强制转数值(错误值变 NaN)
df['price'] = pd.to_numeric(df['price'], errors='coerce')
# 浮点转整型(先确认无小数)
df['year'] = df['year'].astype('int32')
# 省内存:int64 转 int32 或 float32
df['age'] = df['age'].astype('float32')
这里有个陷阱:errors='coerce' 会把无法转换的值变成 NaN,但如果你事先没处理这些 NaN,它们会跟着进模型。我一般会先做一次缺失值处理,再转类型。
3.2 类别编码
| 编码方式 | 适用场景 | 举例 | 我的推荐 |
|---|---|---|---|
| Label Encoding | 有序类别 | 学历:小学→0,中学→1,大学→2 | 必修 |
| One-Hot Encoding | 无序类别,基数小 | 国别:中国→[1,0,0] | 最常用 |
| Frequency Encoding | 类别很多,且有频次信息 | 用该类别出现次数替代原值 | 树模型专用 |
| Target Encoding | 类别与目标变量强相关 | 用该类别的目标均值替代 | 小心过拟合 |
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
# Label Encoding
le = LabelEncoder()
df['education_code'] = le.fit_transform(df['education'])
# One-Hot Encoding
df_onehot = pd.get_dummies(df['country'], prefix='country')
# 合并回主表
df = pd.concat([df, df_onehot], axis=1)
3.3 时间特征转换
时间戳是个宝贝,但机器不认“2024-03-15 14:30:00”这种格式。必须拆成数值特征:
# 字符串转时间对象
df['timestamp'] = pd.to_datetime(df['timestamp'])
# 拆出周期特征
df['hour'] = df['timestamp'].dt.hour
df['day_of_week'] = df['timestamp'].dt.dayofweek
df['month'] = df['timestamp'].dt.month
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
# 计算时间差
df['time_since_epoch'] = (df['timestamp'] - pd.Timestamp('1970-01-01')).dt.total_seconds()
为什么拆小时和星期?我做过一个电商异常检测项目,周末深夜的大额订单异常概率明显高于工作日上午。如果不把这些周期拆出来,异常检测模型只会把深夜订单全判为异常——但它可能只是“周五晚上促销活动”的正常峰值。