Skip to content

特征工程全指南:从原始数据到建模特征 ​

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()

missing_heatmap.png

预期输出: 一张热力图中,Cabin列几乎全黄(缺失),Age列有零星黄色条纹,Embarked列仅2个黄色点。缺失模式一目了然——Cabin的缺失不是随机分布,可能与客舱等级有关,这本身就是一种信号。


二、缺失值填充策略

三类变量分别处理:

变量缺失率策略原因
Age20%中位数填充连续变量,分布右偏
Cabin77%标记为"缺失"特征缺失太多,填均值会引入噪声
Embarked0.2%众数填充分类变量,极少缺失
python
# Age:中位数填充(对右偏分布更稳健)
df['Age'] = df['Age'].fillna(df['Age'].median())
print(f'Age填充后缺失: {df["Age"].isnull().sum()}')  # 应为0
输出: Age填充后缺失: 0
python
# 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    204
python
# Embarked:众数填充(仅2条缺失)
df['Embarked'] = df['Embarked'].fillna(df['Embarked'].mode()[0])
print(f'填充后总缺失: {df.isnull().sum().sum()}')  # 应为0
输出: 填充后总缺失: 0

为什么Age用中位数而非均值? Age分布右偏(少数高龄乘客拉高均值),中位数更稳健。Cabin填充中位数是常见错误——77%数据是人工值,模型学到的是"噪声"而非信号。缺失本身就是特征,这是特征工程的第一条经验。


三、类别编码:把文字变成数字

变量编码方式原因
SexLabel(0/1)二分类,有序隐含
EmbarkedOne-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()

encoding_comparison.png

选择原则: 二分类→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()

feature_distributions.png

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      7
python
# 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       27
python
# 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    222
python
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      8
python
# 用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.0000

TF-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    491
python
# 构造聚合特征:每个乘客所在客舱的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()

correlation_heatmap.png

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.0103
python
# 梯度提升特征重要性(对照验证)
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()

feature_importance_top10.png

结论: 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       179

Pipeline流程可视化 ​

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_flow.png

Pipeline三大优势:

  1. 防数据泄露:fit()只在训练集上学习参数(均值、缩放参数),transform()应用到测试集
  2. 一键部署:joblib.dump(clf, 'model.pkl') 保存整个流程,部署时只需加载和调用predict()
  3. 易于调优:可直接传给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),其核心思想之一就是特征随机子空间——在每次分裂时只考虑随机选择的特征子集。这种"特征采样"大幅增强了模型的泛化能力,也启发了特征工程中的很多实践。


关注公众号:QIAN数据