4、数据预处理(下):数据规约与特征工程
好,咱们接着聊数据预处理的下半场。上一章我们把脏数据洗得干干净净,但数据量可能还是很大,特征也可能不够“聪明”。这一章,我们就来解决这两个问题:数据规约和特征工程。
说白了,数据规约就是给数据“减负”,特征工程就是给数据“增值”。这两个活儿干好了,模型训练又快又准。
4.1 数据规约:给数据做减法
数据规约,就是在大数据集上“瘦身”。我见过不少新手,拿到数据一股脑全扔进模型,结果跑了一宿没出结果。嗯,这里要注意,数据量大不等于信息多。
4.1.1 维度规约
维度规约,就是减少特征的个数。你想想看,如果一张表有1000列,很多列之间其实是相关的,留着就是浪费算力。
主成分分析(PCA)是我最常用的方法。它能把高维数据投影到低维空间,同时尽量保留数据的“差异”。
核心思想:找到数据方差最大的方向,把数据投影过去。
from sklearn.decomposition import PCA
# 假设 X 是原始特征矩阵
pca = PCA(n_components=2) # 降到2维
X_reduced = pca.fit_transform(X)
print(f"原始维度: {X.shape[1]}")
print(f"降维后维度: {X_reduced.shape[1]}")
print(f"解释方差比: {pca.explained_variance_ratio_}")
我在项目中遇到过这样一个场景:一个电商用户画像数据,原始特征有300多个。用PCA降到50维后,模型准确率只掉了1%,但训练时间从2小时缩短到了10分钟。值不值?你自己算算。
我的习惯:降维前先做标准化。PCA对尺度敏感,不标准化的话,量级大的特征会主导结果。
4.1.2 数量规约
数量规约,就是减少样本的个数。数据量太大,跑不动怎么办?抽一部分呗。
抽样是最直接的方法。但要注意,抽样不能破坏原始数据的分布。
import pandas as pd
# 随机抽样
sample_df = df.sample(frac=0.1, random_state=42)
# 分层抽样(按类别)
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
我曾经犯过一个错:做随机抽样时没设 random_state,结果每次跑出来的结果都不一样,排查了半天才发现是抽样不一致导致的。避坑指南:抽样一定要固定随机种子。
4.2 特征工程:给数据做加法
特征工程,说白了就是“造特征”。数据和算法是固定的,但特征可以千变万化。我常说,特征工程决定了模型的上限,算法只是去逼近这个上限。
4.2.1 特征构建
特征构建,就是从原始数据中创造新特征。比如,从日期中提取“星期几”、“是否节假日”;从文本中提取“长度”、“关键词数量”。
import pandas as pd
import numpy as np
# 从日期列构建特征
df['timestamp'] = pd.to_datetime(df['timestamp'])
df['hour'] = df['timestamp'].dt.hour
df['day_of_week'] = df['timestamp'].dt.dayofweek
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
# 从数值列构建交互特征
df['income_per_capita'] = df['total_income'] / df['family_size']
df['income_log'] = np.log1p(df['total_income']) # 处理偏态分布
我记得有一次做房价预测,原始特征只有面积和房间数。我构建了一个“房间密度”(房间数/面积)的特征,结果模型性能提升了5%。有时候,一个简单的比值就能带来惊喜。
4.2.2 特征选择
特征选择,就是从一堆特征里挑出最有用的。不是所有特征都对模型有帮助,有些甚至是噪音。
常用的方法有三种:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 过滤法 | 计算每个特征与目标变量的相关性 | 特征数量多,快速筛选 |
| 包裹法 | 用模型训练,逐步添加或删除特征 | 特征数量适中,追求最优组合 |
| 嵌入法 | 在模型训练过程中自动选择特征 | 使用L1正则化或树模型 |
from sklearn.feature_selection import SelectKBest, chi2
from sklearn.ensemble import RandomForestClassifier
# 过滤法:卡方检验
selector = SelectKBest(chi2, k=10)
X_selected = selector.fit_transform(X, y)
# 嵌入法:随机森林特征重要性
rf = RandomForestClassifier()
rf.fit(X, y)
importances = rf.feature_importances_
top_features = np.argsort(importances)[-10:] # 取最重要的10个
注意:特征选择要在训练集上做,不要在全集上做。否则会引入数据泄露,导致模型评估结果虚高。我曾经吃过这个亏,模型上线后效果直接腰斩。
4.2.3 特征提取
特征提取,和维度规约有点像,但目的不同。维度规约是为了降维,特征提取是为了从原始数据中“提炼”出更有代表性的特征。
比如,从图片中提取边缘特征(SIFT、HOG),从文本中提取主题特征(LDA)。
from sklearn.feature_extraction.text import TfidfVectorizer
# 文本特征提取:TF-IDF
corpus = ["数据挖掘很有趣", "特征工程很重要", "模型训练需要数据"]
vectorizer = TfidfVectorizer(max_features=100)
X_tfidf = vectorizer.fit_transform(corpus)
print(f"词汇表大小: {len(vectorizer.get_feature_names_out())}")
print(f"特征矩阵形状: {X_tfidf.shape}")
4.3 处理类别型数据
类别型数据,就是那些不能直接做数学运算的字段,比如“颜色”、“城市”、“职业”。模型不认识“红色”、“北京”,它只认数字。所以,我们需要把类别转成数字。
4.3.1 独热编码
独热编码,就是把每个类别变成一个独立的0/1列。比如“颜色”有红、黄、蓝三种,就变成三列:is_red、is_yellow、is_blue。
import pandas as pd
df = pd.DataFrame({'color': ['red', 'blue', 'green', 'red']})
df_encoded = pd.get_dummies(df, columns=['color'])
print(df_encoded)
# color_blue color_green color_red
# 0 0 0 1
# 1 1 0 0
# 2 0 1 0
# 3 0 0 1
独热编码适合类别数量不多的情况。如果类别有几百个,比如“城市”,那就会产生几百列,反而把数据搞稀疏了。
我的建议:类别数超过10个,可以考虑先用业务知识合并,或者用目标编码(Target Encoding)替代。
4.3.2 标签编码
标签编码,就是把每个类别映射成一个整数。比如“红”=0,“黄”=1,“蓝”=2。
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df['color_encoded'] = le.fit_transform(df['color'])
print(df)
# color color_encoded
# 0 red 2
# 1 blue 0
# 2 green 1
# 3 red 2
但要注意,标签编码会给类别强加一个“顺序”。比如“红”=0,“蓝”=1,模型可能会认为“蓝”比“红”大。这在线性模型中是个大问题,但在树模型中影响不大。
我个人习惯是:无序类别用独热编码,有序类别用标签编码。比如“学历”(小学、中学、大学)这种天然有序的,用标签编码就很合适。
4.4 本章小结
这一章我们聊了数据规约和特征工程的核心内容。数据规约帮我们“减负”,特征工程帮我们“增值”。处理类别型数据则是让模型能“看懂”非数值信息。
记住一句话:好的特征,胜过复杂的模型。下一章,我们会进入模型训练环节,到时候你就知道,前期这些预处理工作有多重要了。