机器学习从入门到精通 - 数据预处理实战秘籍:清洗、转换与特征工程入门
开场:别急着喂数据,先看看这碗"米"好不好!
老话说"巧妇难为无米之炊",搞机器学习的朋友们,咱们手里这堆数据就是"米"。但你想过没有?直接从田里收上来的稻谷,能直接下锅煮饭吗?得晒干、脱壳、筛杂质... 预处理就是磨这把米!很多新手吭哧吭哧调模型,效果却惨不忍睹,八成是"米"没淘干净。这篇长文,我就跟你掏心窝子聊聊数据预处理里的那些门道、暗坑和救命技巧。从怎么洗掉脏数据、怎么给数据"变形",再到怎么"无中生有"造出好特征,咱们一步一个坑(不是,一步一个脚印)地走通它。读完这篇,我保证你再看原始数据,眼神都不一样了------那都是闪闪发光的金矿胚子啊!
第一章:数据清洗 - 给数据搓个澡,别让"泥点子"毁了模型
为啥要洗?脏数据有多可怕?
想象一下,你训练一个预测房价的模型。数据里有个豪宅面积写着"五百平米",另一个老破小面积写成了"500"。模型一看,老破小面积是500,豪宅是"五百",它很可能觉得"五百"是某种神秘代码或者干脆忽略掉------结果就是学习偏差。更别提那些缺失值、异常值了... 这些脏东西不清理,模型学到的就是失真的世界。
实战第一坑:缺失值处理 - 删?补?这是个哲学问题!
新手最容易掉进去的坑就是无脑删!看见空值(NaN, Null)就 df.dropna()
梭哈一把删。等等,这里有个矛盾点!数据量本身就少得可怜,你再删掉几行关键样本,模型直接饿死了怎么办?
为什么不能乱删?
- 代价高昂:特别是某些稀有事件(比如欺诈交易)的数据,删一个少一个。
- 引入偏差:如果缺失不是随机的(比如:收入高的人更不愿意填写收入栏),删除后样本就偏离了真实分布。
更聪明的"补"法:
-
统计量填充(适合数值型): 用均值、中位数、众数。为啥用中位数?因为它对异常值不敏感!比如小区房价,一个天价别墅会拉高均值,中位数就更稳健。
python# 计算中位数填充 'price' 列的缺失值 median_price = df['price'].median() df['price'].fillna(median_price, inplace=True) # 新手常犯的错:忘记 inplace=True 或者忘记赋值回去!
-
模型预测填充(更高级): 用其他没缺失的特征,训练一个回归/分类模型来预测缺失值。这招复杂点,但更精准。
pythonfrom sklearn.experimental import enable_iterative_imputer from sklearn.impute import IterativeImputer from sklearn.ensemble import RandomForestRegressor # 使用随机森林预测缺失的 'age' imputer = IterativeImputer(estimator=RandomForestRegressor(), max_iter=10, random_state=42) df[['age']] = imputer.fit_transform(df[['age']]) # 注意:确保输入是二维数组 [[]]
-
特殊值标记(分类/时序常用): 对于实在不好补的,干脆加一列标志
is_missing
告诉模型"这个值缺了",让模型自己判断缺了代表啥意思。
流程图:缺失值处理决策树
Yes, 很高 >30% No 随机 非随机 列缺失多 行缺失多且样本珍贵 发现缺失值 缺失比例高吗? 考虑删除整个特征或行? 慎重! 缺失有模式吗 随机缺失? 统计量填充 均值/中位数/众数 模型预测填充 或 特殊值标记 删除行还是列? 删特征 尝试填充或标记
实战第二坑:异常值 - 是金子还是老鼠屎?
数据里混进个身高3米8的大哥,或者年龄200岁的老寿星... 这些是宝贵的特殊样本,还是该清理的噪声?
为什么处理异常值?
异常值会像磁铁一样,把回归模型的线"吸"偏;也会让基于距离的算法(如KNN)直接懵圈;更会让需要计算均值和方差的算法(如PCA、标准化)结果失真。
怎么揪出它们?
-
可视化大法好! 箱线图(Boxplot)、散点图(Scatter Plot)一眼看出离群点。
pythonimport matplotlib.pyplot as plt import seaborn as sns # 绘制 'income' 的箱线图 plt.figure(figsize=(10, 6)) sns.boxplot(y=df['income']) plt.title('收入分布箱线图 - 揪出异常值') plt.show() # 看到图上方的"小星星"了吗?就是它们!
-
统计方法:
-
Z-Score(标准分数): 假设数据近似正态分布。计算每个数据点的Z分数
(x - mean) / std
。通常 |Z| > 3 的点就很可疑了。注意:异常值本身会影响mean和std! 这个标准化处理吧------其实对树模型影响有限。pythonfrom scipy import stats z_scores = stats.zscore(df['income']) outliers = df[np.abs(z_scores) > 3] # 找出Z分数绝对值大于3的异常点
-
IQR(四分位距): 更稳健,不受极端值影响。
IQR = Q3 - Q1
(Q3是75%分位数,Q1是25%分位数)。通常认为小于Q1 - 1.5 * IQR
或大于Q3 + 1.5 * IQR
的是温和异常值(Mild Outlier),超过Q1 - 3 * IQR
或Q3 + 3 * IQR
的是极端异常值。pythonQ1 = df['income'].quantile(0.25) Q3 = df['income'].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR outliers = df[(df['income'] < lower_bound) | (df['income'] > upper_bound)]
-
怎么处理?坑来了!
-
直接删除: 最简单粗暴,但要确认它真的是错误/无关噪音! 万一是特殊模式(比如超高消费的VIP用户)呢?删了就亏大了。
-
盖帽法(Capping/Winsorizing): 温和异常值用边界值替换。比如把超过
upper_bound
的值都设为upper_bound
。这招能保留样本数量。pythondf['income_capped'] = df['income'].clip(lower=lower_bound, upper=upper_bound) # Pandas 的 clip 函数超方便
-
分箱(Binning): 把连续值分成几个区间(如"低","中","高"),异常值会被归到最高/最低的箱子里稀释影响。
-
变量转换: 取对数(
np.log1p
)、开方等压缩数值范围,也能减弱大值影响。特别适合右偏分布的收入、房价等数据。对了,我强烈推荐在对数转换前加个1,避免对0取对数出错log(1+x)
。
第二章:数据转换 - 给数据"整容",让模型看得顺眼
为啥要转换?模型都是"颜控"!
不同的模型对数据的"长相"要求不同:
- 基于距离/梯度的模型(KNN, K-Means, 线性回归, SVM, 神经网络): 要求特征尺度统一!不然"身高"单位是米(数值小),"收入"单位是元(数值大),算距离时收入就完全主导了结果,身高被忽略。
- 树模型(决策树, 随机森林, XGBoost): 天生抗量纲,对单调变换(只改变大小顺序不改变相对关系)不敏感。所以标准化对它们效果有限。但! 特征缩放有时也能加速收敛(比如XGBoost的近似分桶算法)。
- 带正则化的模型(Lasso, Ridge): 要求特征尺度相近,否则惩罚项对不同特征的影响不均。
核心转换术1:特征缩放(Feature Scaling)
-
标准化(Standardization / Z-Score Normalization):
-
为什么用? 将数据变换为均值为0,标准差为1的正态分布(或接近)。公式直观:
z = (x - μ) / σ
x
:原始数据点μ
:特征列的均值σ
:特征列的标准差
-
推导(其实很简单): 中心化
(x - μ)
让均值变0,除以标准差σ
让数据的离散度统一。结果就是数据在新的坐标系下,中心是0,单位是"1个标准差"。 -
代码:
sklearn
的StandardScaler
是神器。pythonfrom sklearn.preprocessing import StandardScaler scaler = StandardScaler() # 假设 df_num 是包含数值型特征的DataFrame scaled_features = scaler.fit_transform(df_num) # 注意:这会返回 numpy array df_scaled = pd.DataFrame(scaled_features, columns=df_num.columns)
-
适用场景: 最常用! 适用于数据大致符合正态分布,或分布未知但需要计算协方差(如PCA)的情况。
-
-
归一化(Min-Max Scaling / Normalization):
-
为什么用? 将数据压缩到[0, 1]或[-1, 1]的固定区间。公式:
x_scaled = (x - min) / (max - min)
min
:特征列的最小值max
:特征列的最大值
-
推导: 分子
(x - min)
将最小值拉到0点,分母(max-min)
是原数据的全距。相除后,所有值被线性映射到[0,1]。 -
代码:
sklearn
的MinMaxScaler
。pythonfrom sklearn.preprocessing import MinMaxScaler minmax_scaler = MinMaxScaler(feature_range=(0, 1)) # 默认[0,1], 可设[-1,1] minmax_features = minmax_scaler.fit_transform(df_num) df_minmax = pd.DataFrame(minmax_features, columns=df_num.columns)
-
适用场景: 对输出范围有要求(如神经网络的激活函数输出常为[0,1]或[-1,1]),或者数据边界相对明确(如图像像素值0-255)。
-
标准化 vs 归一化 选哪个?
- 数据有明显边界 (如像素值、百分比分数)→ 归一化。
- 数据有极端值 或分布未知/非均匀 → 标准化(对异常值没那么敏感)。
- 大多数情况 → 标准化是更安全、更通用的选择。我个人的偏好也是标准化为主。
核心转换术2:编码(Encoding)- 让计算机看懂"文字"
模型只能吃数字!性别"男/女",城市"北京/上海/深圳"这些类别型数据(Categorical Data)必须变成数字。但绝不能简单赋值 男=1, 女=2
!为啥?模型会误会数字大小有含义(比如女 > 男?距离女-男=1?)。
-
独热编码(One-Hot Encoding, OHE):
-
为什么用? 彻底消除类别间的虚假序关系。原理是为每个类别创建一个新的二进制特征(0/1)。
-
坑:维度灾难! 如果一个类别特征有1000个不同的城市,OHE后就多了1000列!稀疏又占内存。
-
代码:
pd.get_dummies()
或sklearn.preprocessing.OneHotEncoder
。强烈建议用后者的drop='first'
参数 避免共线性(Dummy Variable Trap)。python# 使用 Pandas 的 get_dummies (注意避免虚拟变量陷阱,drop_first=True) df_encoded = pd.get_dummies(df, columns=['city', 'gender'], drop_first=True) # drop_first=True 是避免陷阱的关键!新手常漏掉这个导致共线性问题 # 或者用 sklearn (更规范,适合管道) from sklearn.preprocessing import OneHotEncoder ohe = OneHotEncoder(drop='first', sparse=False) # drop='first', sparse=False 返回密集数组 city_encoded = ohe.fit_transform(df[['city']]) # 输入必须是二维 # 将编码后的数组转成DataFrame并合并回原数据... (此处略)
-
适用场景: 低基数(类别数量少)的类别特征。
-
-
标签编码(Label Encoding):
-
为什么用? 简单,只把类别映射成0,1,2,...N-1的数字。
-
巨大坑点: 仅适用于有序类别(Ordinal)! 比如学历"小学<初中<高中<大学",数字大小有意义。对无序类别(Nominal)如城市用,就是埋雷!
-
代码:
sklearn.preprocessing.LabelEncoder
。pythonfrom sklearn.preprocessing import LabelEncoder le = LabelEncoder() df['education_encoded'] = le.fit_transform(df['education']) # 假设 'education' 是有序的
-
适用场景: 明确的有序类别特征。对无序类别,慎用!
-
-
目标编码(Target Encoding / Mean Encoding):
-
为什么用? 利用目标变量信息!用每个类别下的目标变量(如平均房价)的统计量(均值、中位数)来编码该类别。这能捕捉到类别与目标的关系。
-
巨大优势: 能处理高基数特征,且编码值有实际意义(反映目标期望)。
-
巨大坑点: 极易过拟合(Overfitting)!特别是数据少或某些类别样本少时。必须配合交叉验证或平滑(Smoothing)技术。
-
代码: 常用
category_encoders
库的TargetEncoder
。python# 安装 category_encoders: pip install category_encoders from category_encoders import TargetEncoder # 假设我们预测 'price', 对 'neighborhood' 进行目标编码 te = TargetEncoder(smoothing=2) # smoothing 参数很重要,防止过拟合 df['neighborhood_encoded'] = te.fit_transform(df['neighborhood'], df['price'])
-
适用场景: 高基数类别特征,且能谨慎处理过拟合风险。
-
第三章:特征工程 - 点石成金,释放数据的洪荒之力
数据清洗和转换只是"打扫房间"和"摆放家具",特征工程才是真正的"装修设计"!它的目标是:利用领域知识,从原始数据中提取或构造出对预测目标更有信息量的特征。
为什么要做特征工程?模型性能的天花板!
数据和特征决定了模型性能的上限,模型和算法只是不断逼近这个上限。再牛的模型,喂给它一堆烂特征也无力回天。好的特征能:
- 提升模型性能: AUC、准确率、RMSE等指标蹭蹭涨。
- 简化模型: 用更简单的模型(如线性模型)就能达到好效果。
- 增强可解释性: 构造的特征往往有更明确的业务含义。
特征创造:当个"数据炼金术士"
- 领域知识驱动:
-
日期时间特征: 从日期中提取年、月、日、周几、季度、是否周末、是否节假日、距离某个重要日期的天数等。电商中的用户行为、交通流量预测特别需要这个。
python# 假设有 'timestamp' 列 df['hour'] = df['timestamp'].dt.hour df['day_of_week'] = df['timestamp'].dt.dayofweek # Monday=0, Sunday=6 df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int) df['time_since_event'] = (datetime.now() - df['timestamp']).dt.days
-
组合特征: 把几个相关特征加、减、乘、除。比如电商中"商品单价 * 购买数量 = 总金额";风控中"负债 / 收入 = 负债收入比"。
pythondf['bmi'] = df['weight_kg'] / (df['height_m'] ** 2) # BMI指数 = 体重(kg) / 身高(m)^2 df['price_per_sqft'] = df['price'] / df['area_sqft'] # 每平方英尺单价
-
分箱(Binning / Discretization): 把连续变量分段。比如年龄分成"儿童(0-12)"、"青少年(13-18)"、"成年(19-65)"、"老年(>65)"。为什么?让模型学习非线性关系更容易,也能处理异常值。
-