Machine Learning: 特征工程 — ML项目核心竞争力的系统方法指南

数据决定了上限,模型只是逼近上限——特征工程就是提升上限的关键。

1. 你将学到


2. 一个ML工程师的真实故事

(1) 痛点:花3天调参只提升1%,换个特征提升10%

Bob花了3天时间调XGBoost超参数,R²从0.78提升到0.79——仅1%的改善。但当他加入一个新特征"最近7天购买频次"时,R²直接跳到0.86——8%的提升。特征工程的效果远超调参,但缺乏系统方法论。

(2) 特征工程的系统解法

特征工程不是随机尝试,而是有系统的流程:理解业务 → 构造特征 → 选择特征 → 验证效果。

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

# Systematic feature engineering pipeline
num_features = ["ad_spend", "traffic", "log_monetary", "purchase_freq_7d"]
cat_features = ["category", "tier"]

preprocessor = ColumnTransformer([
    ("num", StandardScaler(), num_features),
    ("cat", OneHotEncoder(drop="first", handle_unknown="ignore"), cat_features),
])

pipe = Pipeline([("prep", preprocessor), ("model", XGBRegressor())])

(3) 收益:系统特征工程让R²从0.78提升到0.88

Bob建立特征工厂后,每次新特征的添加都有据可循,R²从0.78系统提升到0.88,远超调参效果。


3. 数值特征变换

(1) 标准化 vs 归一化

▶ 示例:StandardScaler vs MinMaxScaler对比

PYTHON
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
import numpy as np

data = np.array([[1], [5], [10], [50], [100], [500], [1000]])

scalers = {
    "Original": data,
    "StandardScaler": StandardScaler().fit_transform(data),
    "MinMaxScaler": MinMaxScaler().fit_transform(data),
    "RobustScaler": RobustScaler().fit_transform(data),
}

for name, scaled in scalers.items():
    print(f"{name:16s}: min={scaled.min():8.3f}, max={scaled.max():8.3f}, "
          f"mean={scaled.mean():8.3f}, std={scaled.std():8.3f}")

输出:

TEXT 📖 仅展示
# 执行成功
方法 公式 输出范围 抗异常值 适用场景
StandardScaler (x-μ)/σ 无固定 SVM/逻辑回归/神经网络
MinMaxScaler (x-min)/(max-min) [0,1] 神经网络输入
RobustScaler (x-median)/IQR 无固定 有异常值的数据
Log Transform log(1+x) 无固定 右偏分布

(2) 对数变换与分箱

▶ 示例:处理右偏分布与分箱

PYTHON
import numpy as np
from sklearn.preprocessing import KBinsDiscretizer
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
income = rng.exponential(50000, 1000)  # Right-skewed income data

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# (1) Original distribution
axes[0].hist(income, bins=30, color="#2196F3", edgecolor="white")
axes[0].set_title("Original (Right-Skewed)")

# (2) Log-transformed
log_income = np.log1p(income)
axes[1].hist(log_income, bins=30, color="#4CAF50", edgecolor="white")
axes[1].set_title("Log-Transformed (More Symmetric)")

# (3) Binned
binner = KBinsDiscretizer(n_bins=5, encode="ordinal", strategy="quantile")
binned = binner.fit_transform(income.reshape(-1, 1))
axes[2].hist(binned, bins=5, color="#FF9800", edgecolor="white")
axes[2].set_title("Quantile Binned (5 Bins)")

plt.tight_layout()
plt.savefig("feature_transform.png", dpi=150)

输出:

TEXT 📖 仅展示
# 执行成功

(3) 多项式特征

▶ 示例:捕捉非线性关系

PYTHON
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score
import numpy as np

rng = np.random.default_rng(42)
X = rng.uniform(-3, 3, (200, 1))
y = 2 * X.squeeze() ** 2 - 3 * X.squeeze() + 1 + rng.normal(0, 1, 200)

for degree in [1, 2, 3, 5]:
    pipe = Pipeline([
        ("poly", PolynomialFeatures(degree=degree, include_bias=False)),
        ("model", LinearRegression()),
    ])
    scores = cross_val_score(pipe, X, y, cv=5, scoring="r2")
    print(f"Degree={degree}: R²={scores.mean():.3f} +/- {scores.std():.3f}")

输出:

TEXT 📖 仅展示
# 执行成功

4. 类别特征编码

(1) 编码方法对比

▶ 示例:不同编码方式

PYTHON
import pandas as pd
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
import numpy as np

df = pd.DataFrame({
    "category": ["Electronics", "Clothing", "Food", "Electronics", "Food"],
    "tier": ["Low", "Medium", "High", "Medium", "Low"],
    "city": ["New York", "London", "Paris", "Tokyo", "Berlin"],
})

# One-Hot Encoding (no ordinal assumption)
ohe = OneHotEncoder(drop="first", sparse_output=False)
ohe_result = ohe.fit_transform(df[["category"]])
print(f"One-Hot category:\n{ohe_result}")
print(f"Feature names: {ohe.get_feature_names_out()}")

# Ordinal Encoding (has order)
ord_enc = OrdinalEncoder(categories=[["Low", "Medium", "High"]])
ord_result = ord_enc.fit_transform(df[["tier"]])
print(f"\nOrdinal tier: {ord_result.flatten()}")

# Target Encoding (encode by target mean per category)
target_means = df.groupby("city")["some_target"].transform("mean")

输出:

TEXT 📖 仅展示
# 执行成功
编码方式 适用场景 维度增加 有序性 高基数风险
One-Hot 无序类别(<10类) K-1 维度爆炸
Ordinal 有序类别(等级) 0
Target 高基数(城市/商品) 0 过拟合(需CV)
Frequency 高基数 0 信息损失
Embedding 超高基数(深度学习) 可控 学习 需训练

(2) 高基数处理

▶ 示例:Target Encoding处理城市编码

PYTHON
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
import pandas as pd
import numpy as np

rng = np.random.default_rng(42)
n = 1000
cities = rng.choice([f"City_{i}" for i in range(50)], n)
df = pd.DataFrame({
    "city": cities,
    "income_k": rng.exponential(50, n),
    "age": rng.integers(18, 70, n),
    "purchased": rng.choice([0, 1], n, p=[0.7, 0.3]),
})

# Target encoding with CV to avoid leakage
from category_encoders import TargetEncoder

pipe = Pipeline([
    ("target_enc", TargetEncoder(cols=["city"])),
    ("scaler", StandardScaler()),
    ("model", LogisticRegression(max_iter=500)),
])

X = df[["city", "income_k", "age"]]
y = df["purchased"]
scores = cross_val_score(pipe, X, y, cv=5, scoring="accuracy")
print(f"Target Encoding CV Accuracy: {scores.mean():.3f}")

输出:

TEXT 📖 仅展示
# 执行成功

5. 时间特征提取

▶ 示例:电商时间特征工程

PYTHON
import pandas as pd
import numpy as np

rng = np.random.default_rng(42)
n = 1000
df = pd.DataFrame({
    "order_date": pd.date_range("2023-01-01", periods=n, freq="6H"),
    "user_register_date": pd.to_datetime(rng.choice(pd.date_range("2020-01-01", "2023-01-01"), n)),
})

# Extract time-based features
df["order_year"] = df["order_date"].dt.year
df["order_month"] = df["order_date"].dt.month
df["order_day"] = df["order_date"].dt.day
df["order_hour"] = df["order_date"].dt.hour
df["day_of_week"] = df["order_date"].dt.dayofweek  # 0=Mon, 6=Sun
df["is_weekend"] = (df["day_of_week"] >= 5).astype(int)
df["is_night"] = ((df["order_hour"] >= 22) | (df["order_hour"] <= 6)).astype(int)

# Quarter and season
df["quarter"] = df["order_date"].dt.quarter

# Time since registration
df["days_since_register"] = (df["order_date"] - df["user_register_date"]).dt.days

# Is holiday (simplified - mark Dec 20-31 as holiday season)
df["is_holiday_season"] = ((df["order_month"] == 12) & (df["order_day"] >= 20)).astype(int)

# Days to next month end (urgency feature)
df["days_to_month_end"] = (df["order_date"] + pd.offsets.MonthEnd(0) - df["order_date"]).dt.days

print(f"Time features: {[c for c in df.columns if c not in ['order_date', 'user_register_date']]}")
print(df.head(3))

输出:

TEXT 📖 仅展示
# 执行成功
特征类型 示例 业务含义
周期性 hour, day_of_week, month 购买高峰期
标志性 is_weekend, is_holiday 特殊时段消费模式
距离性 days_since_register 用户生命周期
紧迫性 days_to_month_end 冲动消费信号

6. 特征选择

(1) 三大方法

100%
mindmap
  root((Feature Selection))
    Filter
      Correlation Coefficient
      Mutual Information
      Variance Threshold
      Chi-Square Test
    Wrapper
      Recursive Feature Elimination RFE
      Sequential Feature Selection
      Cross-Validation Based
    Embedded
      L1 Regularization Lasso
      Tree Feature Importance
      XGBoost Importance

▶ 示例:三种特征选择方法对比

PYTHON
from sklearn.feature_selection import SelectKBest, f_regression, mutual_info_regression, RFE
from sklearn.linear_model import Lasso, LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
import numpy as np

rng = np.random.default_rng(42)
n, p = 500, 20
X = rng.standard_normal((n, p))
true_coefs = np.zeros(p)
true_coefs[:5] = [3, -2, 1.5, 0.8, -0.5]
y = X @ true_coefs + rng.normal(0, 1, n)

feature_names = [f"feat_{i}" for i in range(p)]

# Method 1: Filter (F-regression)
selector_filter = SelectKBest(f_regression, k=5)
selector_filter.fit(X, y)
filter_selected = np.array(feature_names)[selector_filter.get_support()]
print(f"Filter top 5: {filter_selected}")

# Method 2: Wrapper (RFE)
rfe = RFE(LinearRegression(), n_features_to_select=5)
rfe.fit(X, y)
rfe_selected = np.array(feature_names)[rfe.get_support()]
print(f"RFE top 5:    {rfe_selected}")

# Method 3: Embedded (Lasso)
lasso = Lasso(alpha=0.1)
lasso.fit(StandardScaler().fit_transform(X), y)
lasso_selected = np.array(feature_names)[np.abs(lasso.coef_) > 0.01]
print(f"Lasso top 5:  {lasso_selected}")
print(f"True features: {feature_names[:5]}")

输出:

TEXT 📖 仅展示
# 执行成功
方法 速度 特征交互 过拟合风险 适用场景
Filter 不考虑 初筛、高维数据
Wrapper 考虑 精细选择
Embedded 部分考虑 实用首选

▶ 示例:Bob的电商特征工厂

PYTHON
import pandas as pd
import numpy as np

rng = np.random.default_rng(42)
n = 5000

# Raw transaction data
transactions = pd.DataFrame({
    "user_id": rng.choice(range(1000), n),
    "order_date": pd.date_range("2023-01-01", periods=n, freq="3H"),
    "amount_usd": rng.exponential(100, n).clip(1, 5000),
    "category": rng.choice(["Elec", "Cloth", "Food", "Book", "Home"], n),
})

# Feature factory: aggregate transactions into user features
user_features = transactions.groupby("user_id").agg(
    total_orders=("amount_usd", "count"),
    total_spending_usd=("amount_usd", "sum"),
    avg_order_value=("amount_usd", "mean"),
    std_order_value=("amount_usd", "std"),
    last_order_date=("order_date", "max"),
    first_order_date=("order_date", "min"),
    favorite_category=("category", lambda x: x.mode().iloc[0] if len(x.mode()) > 0 else "Unknown"),
    category_diversity=("category", "nunique"),
).reset_index()

# Derived business features
user_features["recency_days"] = (pd.Timestamp("2024-01-01") - user_features["last_order_date"]).dt.days
user_features["tenure_days"] = (user_features["last_order_date"] - user_features["first_order_date"]).dt.days
user_features["purchase_frequency"] = user_features["total_orders"] / (user_features["tenure_days"] + 1) * 30

# Category preference entropy (how diverse is user's category preference)
from scipy.stats import entropy
def calc_entropy(series):
    probs = series.value_counts(normalize=True)
    return entropy(probs)

cat_entropy = transactions.groupby("user_id")["category"].apply(calc_entropy).reset_index()
cat_entropy.columns = ["user_id", "category_entropy"]
user_features = user_features.merge(cat_entropy, on="user_id")

# Price sensitivity (std/mean of order values)
user_features["price_sensitivity"] = user_features["std_order_value"] / (user_features["avg_order_value"] + 1)

print(f"User features shape: {user_features.shape}")
print(f"Feature columns: {list(user_features.columns)}")
print(user_features.head(3))

输出:

TEXT 📖 仅展示
# 函数定义成功

❓ 常见问题

Q 特征工程应该在train/test split之前还是之后做?
A 涉及目标信息的操作(如Target Encoding)必须在split后、且用训练集fit;不涉及目标信息的(如log变换、提取月份)可以在split前做。Pipeline自动处理这个顺序。
Q 多少特征算太多?
A 没有绝对标准。一般经验:样本数应该至少是特征数的10倍(n > 10p)。特征数超过样本数时(高维小样本),必须做特征选择或正则化。
Q log变换后怎么解释模型系数?
A log(y)的线性模型中,系数表示"x增加1单位,y变化约coef*100%"。这是弹性(elasticity)解释,在经济学中常用。
Q One-Hot编码后的虚拟变量陷阱是什么?
A K个类别创建K个哑变量会导致完美共线性(所有列和=1)。必须drop_first=True去掉一个,保留K-1个。
Q 特征选择应该优先选哪种方法?
A 先用Filter快速筛选(去掉明显无关的),再用Embedded(Lasso/树重要性)精细选择。Wrapper最准但最慢,用于最终优化。
Q 怎么判断一个新特征是否有用?
A 三个方法——1)看与目标的单变量相关性;2)加入模型后看验证集指标是否提升;3)用permutation importance评估该特征被随机打乱后性能下降多少。

📖 小节


📝 作业

  1. 基础题(难度⭐):对一个含偏态分布的数据集,分别用StandardScaler和Log+StandardScaler处理,对比处理后的分布统计量。提示:np.log1p() + StandardScaler()
  2. 进阶题(难度⭐⭐):实现完整的特征工程Pipeline——数值列用StandardScaler,类别列用OneHotEncoder,时间列提取月/周/是否周末,然后用RFE选5个最重要特征。提示:ColumnTransformer + RFE。
  3. 挑战题(难度⭐⭐⭐):构建Bob的电商特征工厂——从原始交易数据出发,至少构造10个业务特征(含RFM、品类偏好熵、价格敏感度、购买频率等),用Lasso选特征,对比选前选后的模型性能。提示:参考第6节特征工厂示例。

← 上一课:KNN与聚类 | 下一课:集成学习 — XGBoost与LightGBM →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏