Machine Learning: 特征工程 — ML项目核心竞争力的系统方法指南
数据决定了上限,模型只是逼近上限——特征工程就是提升上限的关键。
1. 你将学到
- 数值特征:标准化/归一化、对数变换、分箱(Binning)、多项式特征
- 类别特征:One-Hot/Ordinal/Target Encoding,高基数处理
- 时间特征提取:年/月/周/小时、是否节假日、距离某事件天数
- 特征选择:Filter(相关系数/互信息)、Wrapper(RFE)、Embedded(L1正则/树重要性)
- Bob的电商特征工厂:构建"最近7天购买频次""品类偏好熵"等业务特征
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) 三大方法
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评估该特征被随机打乱后性能下降多少。
📖 小节
- 数值特征三件套:标准化/归一化消除量纲、对数变换处理偏态、分箱捕获非线性
- 类别编码根据有序性选择:无序→One-Hot,有序→Ordinal,高基数→Target Encoding
- 时间特征是电商ML的金矿:周期性+标志性+距离性+紧迫性四类特征
- 特征选择三种路径:Filter(快速初筛)、Wrapper(精细但慢)、Embedded(实用首选)
- 特征工厂:从原始交易数据聚合出RFM、品类偏好熵、价格敏感度等业务特征
- 特征工程决定模型上限——投入特征工程的时间,回报远超调参
📝 作业
- 基础题(难度⭐):对一个含偏态分布的数据集,分别用StandardScaler和Log+StandardScaler处理,对比处理后的分布统计量。提示:
np.log1p()+StandardScaler()。 - 进阶题(难度⭐⭐):实现完整的特征工程Pipeline——数值列用StandardScaler,类别列用OneHotEncoder,时间列提取月/周/是否周末,然后用RFE选5个最重要特征。提示:ColumnTransformer + RFE。
- 挑战题(难度⭐⭐⭐):构建Bob的电商特征工厂——从原始交易数据出发,至少构造10个业务特征(含RFM、品类偏好熵、价格敏感度、购买频率等),用Lasso选特征,对比选前选后的模型性能。提示:参考第6节特征工厂示例。