在机器学习领域,有一个广为流传的说法:“数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限。” 特征工程就是将原始数据转化为最能代表问题的特征的过程。它往往是机器学习项目中最耗时但也是最有价值的部分。

为什么特征工程如此重要?

优秀特征带来的提升往往比换一个复杂模型更为显著。特征工程能:

  • 提升模型精度:好的特征让模式更容易被模型捕捉
  • 降低数据需求:好的特征可以用更简单的模型达到同等的效果
  • 提升可解释性:有意义的特征让模型决策更容易理解
  • 减少过拟合:去除噪音特征,让模型聚焦于真正的信号

特征编码

机器学习模型通常只能处理数值数据。原始数据中的类别、文本等信息需要转换为数值形式。

One-Hot 编码

最常用的类别编码方式。将 K 个类别的特征转换为 K 个二进制特征。

import pandas as pd
import numpy as np
from sklearn.preprocessing import OneHotEncoder, LabelEncoder, OrdinalEncoder
from sklearn.feature_extraction import FeatureHasher

# 示例数据
data = pd.DataFrame({
    'color': ['red', 'blue', 'green', 'blue', 'red'],
    'size': ['S', 'M', 'L', 'XL', 'M'],
    'price': [100, 150, 200, 250, 180]
})

# One-Hot 编码
onehot = OneHotEncoder(sparse_output=False, drop='first')  # drop='first' 避免多重共线性
encoded = onehot.fit_transform(data[['color']])
encoded_df = pd.DataFrame(encoded, columns=onehot.get_feature_names_out(['color']))
print("One-Hot 编码结果:")
print(pd.concat([data, encoded_df], axis=1))

注意事项

  • 当类别数量很多时,One-Hot 编码会产生大量特征(维度灾难)
  • drop='first' 可以去除冗余维度,避免多重共线性

标签编码与序数编码

标签编码(Label Encoding):将每个类别映射为一个整数。但问题在于,它引入了数值大小关系,模型可能以为 3 > 2 > 1。

序数编码(Ordinal Encoding):适用于类别本身有顺序关系的情况(如 S < M < L < XL)。

# 标签编码(不保留顺序信息)
label_encoder = LabelEncoder()
labels = label_encoder.fit_transform(data['color'])
print(f"标签编码: {labels}")

# 序数编码(保留顺序信息)
ordinal_encoder = OrdinalEncoder(categories=[['S', 'M', 'L', 'XL']])
ordinal = ordinal_encoder.fit_transform(data[['size']])
print(f"序数编码: {ordinal.flatten()}")

目标编码(Target Encoding)

用目标变量的均值来编码类别变量。常用于高基数类别特征。

# 目标编码实现
def target_encode(series, target, alpha=5):
    """alpha 是平滑参数,防止过拟合"""
    global_mean = target.mean()
    group_stats = target.groupby(series).agg(['count', 'mean'])
    # 贝叶斯平滑
    smooth = (group_stats['count'] * group_stats['mean'] + alpha * global_mean) / (group_stats['count'] + alpha)
    return series.map(smooth)

# 模拟数据
np.random.seed(42)
data['city'] = np.random.choice(['北京', '上海', '广州', '深圳', '杭州'], 5)
data['target'] = np.random.randn(5) * 50 + 100

data['city_encoded'] = target_encode(data['city'], data['target'])
print("\n目标编码结果:")
print(data[['city', 'target', 'city_encoded']])

特征哈希(Feature Hashing)

将高基数类别特征通过哈希函数映射到固定维度的向量中。空间效率高,但不可逆。

# 特征哈希
hasher = FeatureHasher(n_features=4, input_type='string')
features = [['red'], ['blue'], ['green'], ['blue'], ['red']]
hashed = hasher.transform(features).toarray()
print("特征哈希结果 (n_features=4):")
print(hashed)

特征缩放

不同特征的取值范围可能差异很大(如年龄 0-100 vs 年收入 0-1000000)。很多模型对特征尺度敏感。

标准化(Standardization)

将特征转换为均值为 0、标准差为 1 的分布:

$$z = \frac{x - \mu}{\sigma}$$
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

data_scale = pd.DataFrame({
    'age': [25, 30, 35, 40, 45],
    'income': [30000, 50000, 80000, 120000, 200000],
    'score': [1, 2, 3, 4, 5]
})

# 标准化
scaler = StandardScaler()
standardized = scaler.fit_transform(data_scale)
std_df = pd.DataFrame(standardized, columns=data_scale.columns)
print("标准化后:\n", std_df.round(3))
print(f"均值: {std_df.mean().round(3).tolist()}")
print(f"标准差: {std_df.std().round(3).tolist()}")

归一化(Min-Max Scaling)

将特征缩放到 [0, 1] 区间:

$$x_{norm} = \frac{x - x_{min}}{x_{max} - x_{min}}$$
# 归一化
minmax = MinMaxScaler()
normalized = minmax.fit_transform(data_scale)
norm_df = pd.DataFrame(normalized, columns=data_scale.columns)
print("归一化后:\n", norm_df.round(3))

鲁棒缩放(Robust Scaling)

使用中位数和四分位数范围(IQR)进行缩放,对异常值不敏感:

# 鲁棒缩放(不受异常值影响)
robust = RobustScaler()
robust_scaled = robust.fit_transform(data_scale)
robust_df = pd.DataFrame(robust_scaled, columns=data_scale.columns)
print("鲁棒缩放后:\n", robust_df.round(3))

何时使用哪种缩放?

缩放方法 适用范围 对异常值敏感性
StandardScaler 数据近似正态分布 敏感
MinMaxScaler 需要固定范围(如神经网络) 敏感
RobustScaler 数据包含异常值 不敏感
无需缩放 基于树的模型(随机森林、XGBoost)

特征构建

特征构建是从现有数据中创造新特征的过程。好的特征往往来源于对业务的深刻理解。

多项式特征

from sklearn.preprocessing import PolynomialFeatures

# 创建多项式交互特征
poly = PolynomialFeatures(degree=2, include_bias=False, interaction_only=False)
X_simple = np.array([[2, 3], [4, 5], [6, 7]])
X_poly = poly.fit_transform(X_simple)

feature_names = poly.get_feature_names_out(['x1', 'x2'])
print("原始特征:", X_simple)
print("多项式特征 (degree=2):")
print(pd.DataFrame(X_poly, columns=feature_names))
# 输出: x1, x2, x1^2, x1*x2, x2^2

交互特征

# 手动创建交互特征
df = pd.DataFrame({
    'price_per_unit': [10, 20, 15, 25],
    'quantity': [100, 50, 80, 40]
})

# 交互特征
df['revenue'] = df['price_per_unit'] * df['quantity']
df['price_per_unit_sq'] = df['price_per_unit'] ** 2
df['log_quantity'] = np.log(df['quantity'] + 1)

print("包含构建特征的 DataFrame:")
print(df)

分箱(Binning)

将连续特征离散化,可以减少噪音的影响,捕捉非线性关系。

# 分箱
ages = np.array([18, 22, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70])

# 等宽分箱
bins = [0, 25, 40, 60, 100]
labels = ['青年', '中青年', '中年', '老年']
age_groups = pd.cut(ages, bins=bins, labels=labels)
print("分箱结果:")
for age, group in zip(ages, age_groups):
    print(f"  {age}岁 -> {group}")

# 等频分箱(每个箱有相同数量的样本)
age_groups_qcut = pd.qcut(ages, q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])
print("\n等频分箱:")
for age, group in zip(ages, age_groups_qcut):
    print(f"  {age}岁 -> {group}")

文本特征(TF-IDF)

对于文本数据,TF-IDF(词频-逆文档频率)是最经典的特征提取方法。

from sklearn.feature_extraction.text import TfidfVectorizer

# 示例文本
documents = [
    "数据分析是数据科学的核心",
    "机器学习是人工智能的重要分支",
    "数据分析需要使用统计学知识",
    "深度学习和机器学习密切相关",
    "数据科学包含数据分析与机器学习"
]

# TF-IDF 特征提取
tfidf = TfidfVectorizer(max_features=10)
X_tfidf = tfidf.fit_transform(documents)

feature_names = tfidf.get_feature_names_out()
print("TF-IDF 特征矩阵:")
print(pd.DataFrame(X_tfidf.toarray(), columns=feature_names).round(3))

特征选择

特征选择旨在从原始特征中挑选出最有价值的特征子集,达到降维、减少过拟合、提升效率的目的。

过滤方法(Filter Methods)

独立于任何机器学习模型,基于统计指标对特征进行评估。

from sklearn.feature_selection import SelectKBest, f_classif, chi2, mutual_info_classif
from sklearn.datasets import load_iris

iris = load_iris()
X, y = iris.data, iris.target

# 方差分析(ANOVA F-test)
selector_f = SelectKBest(score_func=f_classif, k=2)
X_selected_f = selector_f.fit_transform(X, y)
print("F-test 特征得分:")
for name, score in zip(iris.feature_names, selector_f.scores_):
    print(f"  {name}: {score:.2f}")

# 互信息
selector_mi = SelectKBest(score_func=mutual_info_classif, k=2)
X_selected_mi = selector_mi.fit_transform(X, y)
print("\n互信息得分:")
for name, score in zip(iris.feature_names, selector_mi.scores_):
    print(f"  {name}: {score:.4f}")

包装方法(Wrapper Methods)

将特征选择看作一个搜索问题,用目标模型的性能作为评估标准。

from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestClassifier

# 递归特征消除 (RFE)
estimator = RandomForestClassifier(n_estimators=50, random_state=42)
selector_rfe = RFE(estimator, n_features_to_select=2, step=1)
selector_rfe.fit(X, y)

print("RFE 特征选择结果:")
for name, selected in zip(iris.feature_names, selector_rfe.support_):
    print(f"  {name}: {'✓ 选中' if selected else '✗ 未选中'}")
print(f"特征排序: {selector_rfe.ranking_}")

嵌入方法(Embedded Methods)

特征选择嵌入在模型训练过程中。

# Lasso 特征选择(L1 正则化)
from sklearn.linear_model import LogisticRegression

lasso_selector = LogisticRegression(penalty='l1', solver='liblinear', C=0.5, random_state=42)
lasso_selector.fit(X, y)

print("Lasso 系数(非零即选中):")
nonzero_count = 0
for name, coef in zip(iris.feature_names, lasso_selector.coef_[0]):
    selected = abs(coef) > 1e-5
    print(f"  {name}: {coef:.4f} {'✓' if selected else '✗'}")
    if selected:
        nonzero_count += 1
print(f"选中的特征数: {nonzero_count}")

# 树模型特征重要性
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)
print("\n随机森林特征重要性:")
for name, imp in sorted(zip(iris.feature_names, rf.feature_importances_), 
                        key=lambda x: x[1], reverse=True):
    print(f"  {name}: {imp:.4f}")

降维

降维是在保留数据关键结构的同时减少特征数量。

PCA(主成分分析)

PCA 通过线性变换将原始特征投影到方差最大的方向上。它是无监督的,不依赖标签。

from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

# 将鸢尾花数据降到 2 维
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)

print(f"原始维度: {X.shape[1]}")
print(f"降维后维度: {X_pca.shape[1]}")
print(f"解释方差比例: {pca.explained_variance_ratio_}")
print(f"累计解释方差: {pca.explained_variance_ratio_.sum():.4f}")

# 可视化
plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=y, cmap='viridis', s=50, alpha=0.7)
plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.2%})')
plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.2%})')
plt.title('PCA 降维可视化')
plt.colorbar(scatter, label='类别')
plt.show()

PCA 的注意事项

  • 使用前需要标准化数据
  • 主成分是可解释的(它们是原始特征的线性组合)
  • PCA 假设数据的重要信息包含在方差最大的方向上

t-SNE(t-Distributed Stochastic Neighbor Embedding)

t-SNE 擅长将高维数据可视化到 2D 或 3D 空间,特别擅长保持局部结构。

from sklearn.manifold import TSNE

# t-SNE 降维(主要用于可视化)
tsne = TSNE(n_components=2, perplexity=30, random_state=42)
X_tsne = tsne.fit_transform(X)

plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap='viridis', s=50, alpha=0.7)
plt.xlabel('t-SNE 维度 1')
plt.ylabel('t-SNE 维度 2')
plt.title('t-SNE 可视化')
plt.colorbar(scatter, label='类别')
plt.show()

t-SNE 与 PCA 对比

特性 PCA t-SNE
线性/非线性 线性 非线性
速度
保留全局结构
保留局部结构 一般
主要用途 降维、去噪 可视化
随机性 确定 每次结果不同

UMAP(Uniform Manifold Approximation and Projection)

UMAP 是比 t-SNE 更快、更能保留全局结构的流形学习方法。

# 需要安装: pip install umap-learn
import umap

reducer = umap.UMAP(n_components=2, random_state=42)
X_umap = reducer.fit_transform(X)

plt.figure(figsize=(10, 8))
scatter = plt.scatter(X_umap[:, 0], X_umap[:, 1], c=y, cmap='viridis', s=50, alpha=0.7)
plt.title('UMAP 可视化')
plt.colorbar(scatter, label='类别')
plt.show()

实战:完整的特征工程流程

让我们把今天学到的内容组合成一个完整的特征工程管线:

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer

# 创建混合类型数据
np.random.seed(42)
n = 200

df = pd.DataFrame({
    'age': np.random.normal(35, 12, n).clip(18, 80),
    'income': np.random.lognormal(10, 1, n),
    'gender': np.random.choice(['M', 'F'], n),
    'education': np.random.choice(['高中', '本科', '硕士', '博士'], n,
                                   p=[0.2, 0.4, 0.3, 0.1]),
    'tenure': np.random.randint(0, 15, n),
    'score': np.random.uniform(0, 100, n),
})

# 添加少量缺失值
df.loc[::10, 'income'] = np.nan
df.loc[::15, 'education'] = np.nan

# 定义特征转换方案
numeric_features = ['age', 'income', 'tenure', 'score']
categorical_features = ['gender', 'education']

# ColumnTransformer 组合不同处理方式
preprocessor = ColumnTransformer([
    ('num', Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', StandardScaler()),
    ]), numeric_features),
    ('cat', Pipeline([
        ('imputer', SimpleImputer(strategy='most_frequent')),
        ('onehot', OneHotEncoder(drop='first', sparse_output=False)),
    ]), categorical_features),
])

# 应用预处理
X_processed = preprocessor.fit_transform(df)
print(f"原始特征数: {len(numeric_features) + len(categorical_features)}")
print(f"处理后特征数: {X_processed.shape[1]}")
print(f"特征矩阵形状: {X_processed.shape}")

总结

特征工程是机器学习中最重要的技能之一。本文覆盖了以下核心内容:

  1. 特征编码:One-Hot、标签编码、序数编码、目标编码、特征哈希
  2. 特征缩放:标准化、归一化、鲁棒缩放
  3. 特征构建:多项式特征、交互特征、分箱、TF-IDF
  4. 特征选择:过滤法、包装法、嵌入法
  5. 降维:PCA、t-SNE、UMAP

特征工程是一场"数据炼金术"——将原始数据提炼为信息的黄金。好的特征工程能让简单模型发挥出远超预期的效果。

最后一篇文章,我们将讨论如何评估和优化你的模型,确保它们在实际场景中可靠稳定地工作。