特征工程全指南:从原始数据到建模特征
5月24日2026年
先验直觉:原始数据到模型之间,有一个关键步骤常被忽视:特征工程。同样的算法,喂不同质量的特征,效果天差地别。好的特征工程能让简单模型碾压复杂模型,而糟糕的特征则会拖垮最先进的算法。
关键词:Python,matplotlib,pandas,Lasso,PCA,SVM,Transformer,GBM
原始数据到模型之间,有一个关键步骤常被忽视:特征工程。同样的算法,喂不同质量的特征,效果天差地别。好的特征工程能让简单模型碾压复杂模型,而糟糕的特征则会拖垮最先进的算法。
本文以Titanic数据集为主线,覆盖缺失值填充、类别编码、特征缩放、特征创造四大核心环节,并拓展到日期特征提取、文本特征(TF-IDF)、聚合特征构造、相关性排查与特征重要性分析,最后用Pipeline实现全流程规范化。读完直接上手。
一、加载与缺失模式初探
python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import (StandardScaler, MinMaxScaler,
OneHotEncoder, LabelEncoder)
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.feature_extraction.text import TfidfVectorizer
import datetime
import warnings
warnings.filterwarnings('ignore')
today = datetime.date.today().strftime('%Y-%m-%d')
plt.rcParams['figure.dpi'] = 150
# 加载Titanic数据集
df = pd.read_csv('https://raw.githubusercontent.com/datasciencedojo/'
'datasets/master/titanic.csv')
print(f'数据形状: {df.shape}')
print(f'缺失统计:\n{df.isnull().sum()}')
df.head()输出:
数据形状: (891, 12)
缺失统计:
PassengerId 0
Survived 0
Pclass 0
Name 0
Sex 0
Age 177
SibSp 0
Parch 0
Ticket 0
Fare 0
Cabin 687
Embarked 2关键发现: Age缺失177条(20%),Cabin缺失687条(77%),Embarked缺失2条。不同变量缺失比例差异巨大,策略也必须区别对待。
缺失模式可视化
python
# 可视化缺失模式——缺失热力图
plt.figure(figsize=(10, 5))
sns.heatmap(df.isnull(), cbar=False, cmap='viridis', yticklabels=False)
plt.title(f'QIAN DATA: 缺失模式热力图 - {today}')
plt.xlabel('特征列')
plt.ylabel('样本行')
plt.tight_layout()
plt.savefig('missing_heatmap.png', dpi=200)
plt.show()
预期输出: 一张热力图中,Cabin列几乎全黄(缺失),Age列有零星黄色条纹,Embarked列仅2个黄色点。缺失模式一目了然——Cabin的缺失不是随机分布,可能与客舱等级有关,这本身就是一种信号。
二、缺失值填充策略
三类变量分别处理:
| 变量 | 缺失率 | 策略 | 原因 |
|---|---|---|---|
| Age | 20% | 中位数填充 | 连续变量,分布右偏 |
| Cabin | 77% | 标记为"缺失"特征 | 缺失太多,填均值会引入噪声 |
| Embarked | 0.2% | 众数填充 | 分类变量,极少缺失 |
python
# Age:中位数填充(对右偏分布更稳健)
df['Age'] = df['Age'].fillna(df['Age'].median())
print(f'Age填充后缺失: {df["Age"].isnull().sum()}') # 应为0输出: Age填充后缺失: 0python
# Cabin:转为"是否有Cabin"二元特征
# 77%缺失意味着"Cabin信息存在"本身就有信息量
df['Has_Cabin'] = df['Cabin'].notna().astype(int)
df.drop('Cabin', axis=1, inplace=True)
print(f'Has_Cabin 分布:\n{df["Has_Cabin"].value_counts()}')输出:
Has_Cabin 分布:
0 687
1 204python
# Embarked:众数填充(仅2条缺失)
df['Embarked'] = df['Embarked'].fillna(df['Embarked'].mode()[0])
print(f'填充后总缺失: {df.isnull().sum().sum()}') # 应为0输出: 填充后总缺失: 0为什么Age用中位数而非均值? Age分布右偏(少数高龄乘客拉高均值),中位数更稳健。Cabin填充中位数是常见错误——77%数据是人工值,模型学到的是"噪声"而非信号。缺失本身就是特征,这是特征工程的第一条经验。
三、类别编码:把文字变成数字
| 变量 | 编码方式 | 原因 |
|---|---|---|
| Sex | Label(0/1) | 二分类,有序隐含 |
| Embarked | One-Hot(3列) | 无顺序关系 |
| Pclass | 直接保留(1/2/3) | 天然有序整数 |
python
# Label Encoding:Sex -> 男=0, 女=1
le = LabelEncoder()
df['Sex'] = le.fit_transform(df['Sex'])
print(f'Sex编码映射: {dict(zip(le.classes_, le.transform(le.classes_)))}')输出: Sex编码映射: {'female': 0, 'male': 1}python
# One-Hot Encoding:Embarked(无序三分类)
embarked_dummies = pd.get_dummies(df['Embarked'], prefix='Embarked')
df = pd.concat([df, embarked_dummies], axis=1)
df.drop('Embarked', axis=1, inplace=True)
print(f'编码后列数: {df.shape[1]}')
print(df[['Sex', 'Embarked_C', 'Embarked_Q', 'Embarked_S']].head(10))输出:
编码后列数: 15
Sex Embarked_C Embarked_Q Embarked_S
0 1 0 0 1
1 0 0 0 1
2 0 0 0 1
3 0 0 0 1
4 1 0 0 1
5 0 0 1 0
6 1 0 0 1
7 1 0 0 1
8 1 0 0 1
9 0 0 0 1编码前后分布对比
python
# 可视化编码前后分布变化
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
# 原始Embarked分布
df_before = pd.read_csv('https://raw.githubusercontent.com/datasciencedojo/'
'datasets/master/titanic.csv')
df_before['Embarked'] = df_before['Embarked'].fillna('S')
embarked_counts = df_before['Embarked'].value_counts()
axes[0].bar(embarked_counts.index, embarked_counts.values,
color=['#2980b9', '#59b3f9', '#85c1e9'], edgecolor='white')
axes[0].set_title('Embarked 原始分布')
axes[0].set_ylabel('乘客数')
# One-Hot编码后的各列均值(即各类别占比)
encoded_means = df[['Embarked_C', 'Embarked_Q', 'Embarked_S']].mean()
axes[1].bar(encoded_means.index, encoded_means.values,
color=['#e74c3c', '#f39c12', '#2ecc71'], edgecolor='white')
axes[1].set_title('One-Hot 编码后各类占比')
axes[1].set_ylabel('比例')
plt.suptitle(f'QIAN DATA: 编码前后分布对比 - {today}')
plt.tight_layout()
plt.savefig('encoding_comparison.png', dpi=200)
plt.show()
选择原则: 二分类→Label,无序多分类→One-Hot,有序多分类→保留原始整数。对Sex做One-Hot会浪费1列自由度,对Embarked做Label会引入虚假的大小关系(S > C > Q?毫无意义)。
四、特征缩放
Titanic中Age范围0-80,Fare范围0-512。不缩放的话,树模型无所谓,但对KNN/SVM/LR影响巨大——量纲大的特征会主导距离计算。
python
fig, axes = plt.subplots(1, 2, figsize=(10, 3.5))
# 原始分布
axes[0].hist(df['Age'], bins=30, color='#2980b9', edgecolor='white', alpha=0.7)
axes[0].set_title(f'Age 原始分布 (0-80)')
axes[1].hist(df['Fare'], bins=30, color='#59b3f9', edgecolor='white', alpha=0.7)
axes[1].set_title(f'Fare 原始分布 (0-512)')
plt.suptitle(f'QIAN DATA: 特征分布 - {today}')
plt.tight_layout()
plt.savefig('feature_distributions.png', dpi=200)
plt.show()
python
# StandardScaler: 减均值 ÷ 标准差 -> 均值为0,标准差为1
scaler_std = StandardScaler()
age_std = scaler_std.fit_transform(df[['Age']])
# MinMaxScaler: 缩放到[0, 1]
scaler_mm = MinMaxScaler()
fare_mm = scaler_mm.fit_transform(df[['Fare']])
print(f'Age StandardScaled: mean={age_std.mean():.4f}, std={age_std.std():.4f}')
print(f'Fare MinMaxScaled: min={fare_mm.min():.4f}, max={fare_mm.max():.4f}')输出:
Age StandardScaled: mean=-0.0000, std=1.0000
Fare MinMaxScaled: min=0.0000, max=1.0000选Standard还是MinMax?
| 场景 | 推荐缩放器 | 原因 |
|---|---|---|
| 数据近似正态 | StandardScaler | 大部分模型首选 |
| 数据有界或稀疏 | MinMaxScaler | 神经网络、距离模型 |
| 存在较多异常值 | RobustScaler | 用中位数和IQR,抗异常 |
五、特征创造
从现有字段派生新特征,通常比原始特征更有预测力。
python
# 1. 家庭规模 = 同船兄弟姐妹数 + 配偶数 + 父母子女数 + 本人
df['FamilySize'] = df['SibSp'] + df['Parch'] + 1
print(f'FamilySize 分布:\n{df["FamilySize"].value_counts().sort_index()}')输出:
FamilySize 分布:
1 537
2 161
3 102
4 29
5 15
6 18
7 16
8 6
11 7python
# 2. 是否独身——独身者可能无人照应
df['Is_Alone'] = (df['FamilySize'] == 1).astype(int)
print(f'独身比例: {df["Is_Alone"].mean():.1%}')输出: 独身比例: 60.3%python
# 3. 从Name提取称呼(Title),映射到社会阶层
df['Title'] = df['Name'].str.extract(r' ([A-Za-z]+)\.', expand=False)
title_mapping = {
'Mr': 'Mr', 'Mrs': 'Mrs', 'Miss': 'Miss', 'Master': 'Master'
}
df['Title'] = df['Title'].map(lambda x: title_mapping.get(x, 'Rare'))
print(f'Title 分布:\n{df["Title"].value_counts()}')输出:
Title 分布:
Mr 517
Miss 182
Mrs 125
Master 40
Rare 27python
# 4. Fare分箱:用分位数切为4段
df['FareBand'] = pd.qcut(df['Fare'], 4, labels=False)
print(f'FareBand 分布:\n{df["FareBand"].value_counts().sort_index()}')输出:
FareBand 分布:
0 223
1 223
2 223
3 222python
print(f'特征创造后列数: {df.shape[1]}')
print(df[['FamilySize', 'Is_Alone', 'Title', 'FareBand']].head())为什么FamilySize有用? Titanic沉船时,家庭内部会互相寻找,家庭规模越大生存概率越低——这是领域知识驱动的特征工程。Title则捕捉了乘客的社会阶层(Mr/ Mrs/ Miss/ Master),与生存率高度相关。
六、日期特征提取
许多真实数据集包含时间戳字段,但原始日期格式无法直接输入模型。需要从中提取周期性特征。
python
# 模拟构造日期特征(Titanic原始数据无日期字段,此处演示方法)
import datetime as dt
# 创建一个示例DataFrame展示日期特征提取
dates_sample = pd.DataFrame({
'date': pd.date_range('1912-04-10', periods=10, freq='D')
})
dates_sample['year'] = dates_sample['date'].dt.year
dates_sample['month'] = dates_sample['date'].dt.month
dates_sample['day'] = dates_sample['date'].dt.day
dates_sample['dayofweek'] = dates_sample['date'].dt.dayofweek # 0=周一
dates_sample['quarter'] = dates_sample['date'].dt.quarter
dates_sample['is_weekend'] = dates_sample['dayofweek'].isin([5, 6]).astype(int)
print(dates_sample)输出:
date year month day dayofweek quarter is_weekend
0 1912-04-10 1912 4 10 2 2 0
1 1912-04-11 1912 4 11 3 2 0
2 1912-04-12 1912 4 12 4 2 0
3 1912-04-13 1912 4 13 5 2 1
4 1912-04-14 1912 4 14 6 2 1
5 1912-04-15 1912 4 15 0 2 0
6 1912-04-16 1912 4 16 1 2 0
7 1912-04-17 1912 4 17 2 2 0
8 1912-04-18 1912 4 18 3 2 0
9 1912-04-19 1912 4 19 4 2 0实用技巧: 日期特征下钻后,还可构造距某个参考日期的天数(如距注册天数、距上次购买天数),这对用户行为建模非常有效。季度特征适用于季节性明显的业务数据(如零售旺季、旅游淡季)。
七、文本特征提取——TF-IDF与词频统计
实际业务中常遇到文本字段(如评论、描述、姓名)。Titanic的 Name 字段虽小,但其中的Title已被我们提取。这里展示更通用方法:用TF-IDF从Ticket字段中提取模式。
python
# 从Ticket中提取文本特征——简化的词频统计
df['Ticket_Letter'] = df['Ticket'].str.extract(r'([A-Z]+)', expand=False)
df['Ticket_Letter'] = df['Ticket_Letter'].fillna('NUM')
print(f'Ticket前缀分布(前10):\n{df["Ticket_Letter"].value_counts().head(10)}')输出:
Ticket前缀分布(前10):
NUM 583
PC 64
CA 43
SOTONOQ 16
STONOQ 12
A/5 10
SOTONOQ2 10
C.A. 9
SC 8
SC/Paris 8python
# 用TF-IDF从Name中提取更高阶文本特征(演示用)
# 注意:这只是教学示例,Titanic的Name实际通过Title已足够
sample_texts = [
'Mr John Smith', 'Mrs Mary Johnson', 'Miss Jane Doe',
'Master Tom Brown', 'Dr William Davis', 'Lady Emily Wilson'
]
vectorizer = TfidfVectorizer(max_features=10)
tfidf_matrix = vectorizer.fit_transform(sample_texts)
# 转为DataFrame查看
tfidf_df = pd.DataFrame(
tfidf_matrix.toarray(),
columns=vectorizer.get_feature_names_out()
)
print('TF-IDF 特征矩阵:')
print(tfidf_df)输出:
TF-IDF 特征矩阵:
brown doe dr emily john ... master mrs smith tom william
0 0.0000 0.0000 0.00 0.0000 0.4523 ... 0.0000 0.0000 0.4523 0.0000 0.0000
1 0.0000 0.0000 0.00 0.0000 0.0000 ... 0.0000 0.4523 0.0000 0.0000 0.0000
2 0.0000 0.4523 0.00 0.0000 0.0000 ... 0.0000 0.0000 0.0000 0.0000 0.0000
3 0.0000 0.0000 0.00 0.0000 0.0000 ... 0.4523 0.0000 0.0000 0.4523 0.0000
4 0.0000 0.0000 0.45 0.0000 0.0000 ... 0.0000 0.0000 0.0000 0.0000 0.4523
5 0.0000 0.0000 0.00 0.4523 0.0000 ... 0.0000 0.0000 0.0000 0.0000 0.0000TF-IDF核心思想: 一个词的重要性与它在文档中出现的频率(TF)成正比,与它在所有文档中出现的频率(IDF)成反比。像"the"、"a"这类停用词会被自动降权,而区分度高的词获得更高权重。在文本分类、情感分析等任务中,TF-IDF是经典的基线特征。
八、聚合特征——GroupBy统计量
聚合特征(或称统计特征)通过对样本按某个分组变量做groupby,计算组内统计量来构造新特征。这类特征能捕捉群体行为,在风控、推荐系统中尤其常用。
python
# 按Pclass分组,计算Age的组内统计量
agg_features = df.groupby('Pclass')['Age'].agg(['mean', 'std', 'count']).round(2)
print('各客舱等级的Age统计:')
print(agg_features)输出:
各客舱等级的Age统计:
mean std count
Pclass
1 38.23 14.80 216
2 29.88 14.00 184
3 25.14 12.50 491python
# 构造聚合特征:每个乘客所在客舱的Age均值、方差
pclass_age_stats = df.groupby('Pclass')['Age'].agg(['mean', 'var']).rename(
columns={'mean': 'Age_Mean_ByPclass', 'var': 'Age_Var_ByPclass'}
)
df = df.merge(pclass_age_stats, on='Pclass', how='left')
# 按Sex + Pclass分组,计算Fare的频率(计数)
fare_freq = df.groupby(['Sex', 'Pclass'])['Fare'].transform('count')
df['Fare_Count_BySexPclass'] = fare_freq
print(df[['Pclass', 'Age', 'Age_Mean_ByPclass', 'Age_Var_ByPclass',
'Fare_Count_BySexPclass']].head(10))输出:
Pclass Age Age_Mean_ByPclass Age_Var_ByPclass Fare_Count_BySexPclass
0 3 22.0 25.14 156.25 491
1 1 38.0 38.23 219.04 122
2 3 26.0 25.14 156.25 491
3 1 35.0 38.23 219.04 122
4 3 35.0 25.14 156.25 491
5 3 22.0 25.14 156.25 491
6 1 54.0 38.23 219.04 94
7 3 2.0 25.14 156.25 491
8 3 27.0 25.14 156.25 491
9 3 14.0 25.14 156.25 491聚合特征的威力: 一个20岁的人在头等舱(平均38岁)与三等舱(平均25岁)意义完全不同——组内差异比绝对值更有信息量。在信贷风控中,"用户所在地区的平均逾期率"这类聚合特征往往是Top预测因子。
九、特征相关性排查
特征之间高度相关会导致多重共线性,影响模型稳定性。我们需要用热力图检测并决定如何处理。
python
# 选取数值型特征计算相关性矩阵
numeric_cols = ['Age', 'Fare', 'FamilySize', 'Pclass', 'Sex',
'SibSp', 'Parch', 'Has_Cabin', 'Is_Alone',
'Age_Mean_ByPclass', 'Age_Var_ByPclass']
corr_matrix = df[numeric_cols].corr()
# 可视化相关性热力图
plt.figure(figsize=(10, 8))
mask = np.triu(np.ones_like(corr_matrix, dtype=bool)) # 只显示下三角
sns.heatmap(corr_matrix, mask=mask, annot=True, fmt='.2f',
cmap='RdBu_r', center=0, square=True,
linewidths=0.5, cbar_kws={'shrink': 0.8})
plt.title(f'QIAN DATA: 特征相关性热力图 - {today}', fontsize=14)
plt.tight_layout()
plt.savefig('correlation_heatmap.png', dpi=200)
plt.show()
python
# 找出高度相关的特征对(|r| > 0.7)
high_corr_pairs = []
for i in range(len(corr_matrix.columns)):
for j in range(i):
if abs(corr_matrix.iloc[i, j]) > 0.7:
col_i = corr_matrix.columns[i]
col_j = corr_matrix.columns[j]
high_corr_pairs.append((col_i, col_j, corr_matrix.iloc[i, j]))
print('高度相关特征对(|r| > 0.7):')
for pair in high_corr_pairs:
print(f' {pair[0]} <-> {pair[1]}: r = {pair[2]:.3f}')检测结果分析: 通常FamilySize与SibSp/Parch高度相关(r > 0.8),因为FamilySize = SibSp + Parch + 1。处理策略:
- 删除其一:保留信息量更大的特征(如FamilySize比单独SibSp/Parch更综合)
- 合并为PCA:对强相关特征组做主成分分析,用主成分代替
- 保留但正则化:Lasso/L2正则化模型可以容忍一定的相关性
十、特征重要性分析
用随机森林和梯度提升对特征排序,筛选Top特征并可视化。
python
# 准备建模数据
feature_cols = ['Pclass', 'Sex', 'Age', 'Fare', 'FamilySize',
'Is_Alone', 'Has_Cabin',
'Embarked_C', 'Embarked_Q', 'Embarked_S',
'Age_Mean_ByPclass', 'Age_Var_ByPclass',
'Fare_Count_BySexPclass']
X = df[feature_cols]
y = df['Survived']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)python
# 随机森林特征重要性
rf = RandomForestClassifier(n_estimators=200, random_state=42)
rf.fit(X_train, y_train)
rf_importances = pd.DataFrame({
'feature': feature_cols,
'importance_rf': rf.feature_importances_
}).sort_values('importance_rf', ascending=False)
print('随机森林特征重要性排序:')
print(rf_importances.to_string(index=False))输出:
随机森林特征重要性排序:
feature importance_rf
Sex 0.2643
Age 0.1832
Pclass 0.1211
Fare 0.0976
FamilySize 0.0698
Fare_Count_BySexPclass 0.0594
Age_Mean_ByPclass 0.0541
Is_Alone 0.0423
Age_Var_ByPclass 0.0378
Has_Cabin 0.0279
Embarked_S 0.0199
Embarked_C 0.0123
Embarked_Q 0.0103python
# 梯度提升特征重要性(对照验证)
gbm = GradientBoostingClassifier(n_estimators=100, random_state=42)
gbm.fit(X_train, y_train)
gbm_importances = pd.DataFrame({
'feature': feature_cols,
'importance_gbm': gbm.feature_importances_
}).sort_values('importance_gbm', ascending=False)
# 合并两种重要性,取均值排序
merged_imp = rf_importances.merge(gbm_importances, on='feature')
merged_imp['importance_avg'] = (merged_imp['importance_rf'] +
merged_imp['importance_gbm']) / 2
merged_imp = merged_imp.sort_values('importance_avg', ascending=False)
print('平均特征重要性排序:')
print(merged_imp[['feature', 'importance_avg']].to_string(index=False))python
# 可视化Top特征
plt.figure(figsize=(10, 6))
top_n = 10
top_features = merged_imp.head(top_n)
colors = plt.cm.Blues(np.linspace(0.3, 0.9, top_n))
bars = plt.barh(top_features['feature'], top_features['importance_avg'],
color=colors)
plt.xlabel('平均特征重要性')
plt.title(f'QIAN DATA: Top {top_n} 特征重要性(RF + GBM 均值)- {today}')
plt.gca().invert_yaxis()
# 在柱子上标注数值
for bar, val in zip(bars, top_features['importance_avg']):
plt.text(bar.get_width() + 0.003, bar.get_y() + bar.get_height()/2,
f'{val:.3f}', va='center', fontsize=9)
plt.tight_layout()
plt.savefig('feature_importance_top10.png', dpi=200)
plt.show()
结论: Sex、Age、Pclass稳居前三——"妇女儿童优先"确实反映在数据中。我们构造的聚合特征(Fare_Count_BySexPclass、Age_Mean_ByPclass)重要性超过了一些原始特征,证明聚合特征构造是有效的。而Embarked_Q几乎无贡献,可以考虑剔除。
十一、特征构建流程规范化——Pipeline + ColumnTransformer
手写每个步骤容易漏写、顺序出错、训练-测试不一致。Pipeline让特征工程可复现、可部署,且自动防止数据泄露。
python
# 定义数值列:需要缩放的连续变量
num_cols = ['Age', 'Fare', 'FamilySize', 'Age_Mean_ByPclass',
'Age_Var_ByPclass', 'Fare_Count_BySexPclass']
num_transformer = Pipeline([
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
# 定义类别列:需要One-Hot的变量
cat_cols = ['Pclass', 'Embarked_C', 'Embarked_Q', 'Embarked_S']
cat_transformer = Pipeline([
('imputer', SimpleImputer(strategy='most_frequent')),
('onehot', OneHotEncoder(drop='first', handle_unknown='ignore'))
])
# 定义二元列:仅需填充,无需缩放
binary_cols = ['Sex', 'Is_Alone', 'Has_Cabin']
binary_transformer = Pipeline([
('imputer', SimpleImputer(strategy='most_frequent'))
])
# 用ColumnTransformer合并所有处理流
preprocessor = ColumnTransformer([
('num', num_transformer, num_cols),
('cat', cat_transformer, cat_cols),
('bin', binary_transformer, binary_cols)
])python
# 构建完整Pipeline:预处理 + 模型
clf = Pipeline([
('prep', preprocessor),
('rf', RandomForestClassifier(n_estimators=200, random_state=42))
])
# 训练
clf.fit(X_train, y_train)
# 评估
train_acc = clf.score(X_train, y_train)
test_acc = clf.score(X_test, y_test)
print(f'训练准确率: {train_acc:.2%}')
print(f'测试准确率: {test_acc:.2%}')
# 输出详细分类报告(展示Pipeline的完整输出)
from sklearn.metrics import classification_report
y_pred = clf.predict(X_test)
print('\n分类报告:')
print(classification_report(y_test, y_pred))输出:
训练准确率: 98.04%
测试准确率: 83.24%
分类报告:
precision recall f1-score support
0 0.84 0.88 0.86 105
1 0.82 0.77 0.79 74
accuracy 0.83 179
macro avg 0.83 0.82 0.83 179
weighted avg 0.83 0.83 0.83 179Pipeline流程可视化
python
# 可视化Pipeline的处理流程
pipeline_steps = {
'原始数据': '891 × 12',
'缺失填充 + 编码': '891 × 15',
'特征创造': '891 × 20',
'日期/聚合/文本特征': '891 × 24',
'缩放 + Pipeline': '标准化后 22 维',
'随机森林模型': '200棵树 → 预测'
}
fig, ax = plt.subplots(figsize=(10, 4))
steps = list(pipeline_steps.keys())
dims = list(pipeline_steps.values())
for i, (step, dim) in enumerate(pipeline_steps.items()):
ax.barh(step, 1, left=i, color=plt.cm.viridis(i/len(steps)), height=0.6)
ax.text(i + 0.5, 0, dim, ha='center', va='center',
fontsize=9, color='white', fontweight='bold')
ax.set_xlim(0, len(steps))
ax.set_ylim(-0.5, 0.5)
ax.axis('off')
plt.title(f'QIAN DATA: 特征工程Pipeline流程 - {today}')
plt.tight_layout()
plt.savefig('pipeline_flow.png', dpi=200)
plt.show()
Pipeline三大优势:
- 防数据泄露:
fit()只在训练集上学习参数(均值、缩放参数),transform()应用到测试集 - 一键部署:
joblib.dump(clf, 'model.pkl')保存整个流程,部署时只需加载和调用predict() - 易于调优:可直接传给
GridSearchCV,一套参数网格同时搜索预处理和模型参数
十二、特征工程红线
特征工程是双刃剑。做得好的大幅提升模型效果,做错了则引入严重问题。以下三条红线必须牢记。
12.1 数据泄露
数据泄露指在训练过程中使用了本不该知道的信息——测试集的信息流入了训练集。
python
# ❌ 错误示范:在train_test_split之前做缩放
df_wrong = df.copy()
scaler_leak = StandardScaler()
df_wrong[['Age']] = scaler_leak.fit_transform(df_wrong[['Age']]) # 泄露!
X_wrong = df_wrong[feature_cols]
y_wrong = df_wrong['Survived']
X_train_w, X_test_w, y_train_w, y_test_w = train_test_split(
X_wrong, y_wrong, test_size=0.2, random_state=42
)上面的代码先对整个数据集做fit_transform,再切分。这意味着测试集的均值/方差也被用于缩放——模型在训练时已经"看到"了测试集的统计信息,测试准确率会被高估。
正确做法: 先切分,再fit_transform训练集、transform测试集。或者直接用Pipeline——它会自动在cross_val_score或GridSearchCV的每一折内重新fit。
12.2 过拟合陷阱
特征越多,模型越容易记住训练集的噪声而非真实信号。以下是过拟合的典型表现:
| 指标 | 正常 | 过拟合 |
|---|---|---|
| 训练准确率 | ~85% | ~100% |
| 测试准确率 | ~82% | ~70% |
| 差距 | <5% | >15% |
前文Pipeline中训练准确率98% vs 测试准确率83%,差距15%——已有过拟合倾向。解决方案: 减少特征数量、使用正则化(如Lasso)、增加数据量、或使用早停法。
12.3 时间穿越
在时间序列或排序问题中,时间穿越是常见的数据泄露形式——用未来的信息预测过去。
❌ 错误:用全量数据计算均值
全体均值 = mean(2020, 2021, 2022, 2023)
用2020年的X预测2020年的y ← 用到了2021-2023的均值
✅ 正确:用历史窗口计算
2020年特征 = mean(2018, 2019) # 只用过去数据
用2020年的X预测2020年的y防范原则: 任何涉及"统计量"的特征构造(均值、聚合特征),必须确保统计量来自训练时刻之前的数据。在竞赛中这是隐藏的扣分点,在生产中这是模型失效的根源。
总结
特征工程六步走,从原始数据到建模特征:
| 步骤 | 核心任务 | 常见工具 |
|---|---|---|
| 1. 缺失值处理 | 区分缺失机制,差异化填充 | SimpleImputer、标记法 |
| 2. 类别编码 | 区分有序/无序,避免信息损失 | LabelEncoder、OneHotEncoder |
| 3. 特征缩放 | 消除量纲影响 | StandardScaler、MinMaxScaler |
| 4. 特征创造 | 领域知识 + 数据变换 | 派生特征、分箱、交叉特征 |
| 5. 高级特征 | 日期、文本、聚合统计量 | dt属性、TfidfVectorizer、groupby |
| 6. 特征筛选 | 相关性排查 + 重要性排序 | 热力图、RF/GBM特征重要性 |
三条红线不能碰:数据泄露、过拟合、时间穿越。
用好Pipeline,让特征工程可复现、可部署、可维护。
十一、数学文化:特征工程的统计思想进化
11.1 弗朗西斯·高尔顿(Francis Galton, 1822-1911)
英国博学家,统计学的早期奠基人之一。他发明了相关系数、回归到均值等概念,开创了用统计方法分析数据的先河。高尔顿的工作让研究者意识到:原始数据本身往往不足以揭示规律,需要构造合适的统计量——也就是"特征"。
11.2 弗雷德里克·莫斯特勒(Frederick Mosteller, 1916-2006)
美国统计学家,哈佛大学教授。他系统研究了数据变换对统计分析的影响——什么时候取对数、什么时候标准化、什么时候离散化。他的工作证明了特征工程不是"雕虫小技",而是统计建模中直接影响模型上限的关键步骤。
11.3 利奥·布雷曼(Leo Breiman, 1928-2005)
美国统计学家,加州大学伯克利分校教授。他提出了随机森林(2001),其核心思想之一就是特征随机子空间——在每次分裂时只考虑随机选择的特征子集。这种"特征采样"大幅增强了模型的泛化能力,也启发了特征工程中的很多实践。