元数据卡
- 前置知识:第4章(SQL for Analysis)、第6章(线性回归)
- 预计时间:50 分钟
- 核心难度:进阶
- 阅读模式:高度专注
- 完成标志:能对原始数据做完整的特征工程——编码、缩放、分箱、交叉、选择
你的进度
线性回归跑通了。你用一个公式把补给量和任务完成率联系了起来——每增加 10% 补给,完成率大约提升 2.1 个百分点。
但情报官把更多数据推到你面前:气温、湿度、哨兵经验值、装备等级、距离上次补给的天数……你意识到这些都不是现成的输入变量。它们需要处理——气温要分箱、装备等级要编码、时间要提取特征。
真正的建模工作不是调模型,而是造特征。
你的任务
第 6 章里你用 duration_minutes、team_size、resources_used 三个数值特征做回归。但真实数据中,特征往往不是直接可用的——有类别文本、有时间戳、有空值、有太稀疏的列。特征工程就是把原始数据变成模型能吃的形态。好的特征工程可以比模型选择对结果影响更大。
特征工程不是可选的
训练一个模型,用原始数据 vs. 经过良好工程处理的特征,差距可以达到数倍。这是因为模型(尤其是线性模型)对输入的形态有假设——数值应该是连续的,类别应该是编码的,尺度应该是接近的。
数值特征:缩放
当特征尺度差异大时(一个特征范围 0-1,另一个 0-1000000),梯度下降会震荡,正则化会对大尺度特征不公平。
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
import pandas as pd
df = pd.read_csv("missions_clean.csv")
# StandardScaler: 减去均值,除以标准差 → 均值为0,方差为1
scaler = StandardScaler()
df[["duration_scaled"]] = scaler.fit_transform(df[["duration_minutes"]])
# MinMaxScaler: 缩放到 [0, 1] 范围
mms = MinMaxScaler()
df[["resources_scaled"]] = mms.fit_transform(df[["resources_used"]])
# RobustScaler: 使用中位数和 IQR,对异常值不敏感
rs = RobustScaler()怎么选?如果你的数据没有异常值,StandardScaler 默认选择。如果有大量异常值,RobustScaler 更安全。如果模型对尺度敏感且你需要固定范围(如神经网络用 sigmoid 激活时),用 MinMaxScaler。
类别特征:编码
模型吃数值,不吃字符串。你需要把类别文本转成数值。
from sklearn.preprocessing import OneHotEncoder, LabelEncoder
# One-Hot Encoding —— 适用低基数类别
df_encoded = pd.get_dummies(df, columns=["mission_type"], prefix="type")
# 或者用 sklearn 的 OneHotEncoder
encoder = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
encoded = encoder.fit_transform(df[["mission_type"]])If-Else 决策:
- 类别数 < 10,互斥的 → One-Hot Encoding
- 类别数 = 2 → 直接编码为 0/1
- 类别数很多(几百个城市名)→ 用目标编码或频率编码
- 类别有序 → 有序编码
# 标签编码 —— 适用于有序类别(低级 < 中级 < 高级)
from sklearn.preprocessing import OrdinalEncoder
ordinal = OrdinalEncoder(categories=[["low", "medium", "high"]])
df["skill_level_encoded"] = ordinal.fit_transform(df[["skill_level"]])时间特征:拆解
时间戳是最容易被低估的信息源。一条时间戳可以拆出十几个特征:
df["timestamp"] = pd.to_datetime(df["timestamp"])
df["hour"] = df["timestamp"].dt.hour
df["day_of_week"] = df["timestamp"].dt.dayofweek
df["month"] = df["timestamp"].dt.month
df["is_weekend"] = df["day_of_week"].isin([5, 6]).astype(int)
df["day_of_year"] = df["timestamp"].dt.dayofyear
df["elapsed_days"] = (df["timestamp"] - df["timestamp"].min()).dt.days有些时间模式是周期性的(小时、星期)。对线性模型,直接用 hour(0-23)会导致模型以为 23 和 0 差距很大。更好的做法是用 sin/cos 变换:
df["hour_sin"] = np.sin(2 * np.pi * df["hour"] / 24)
df["hour_cos"] = np.cos(2 * np.pi * df["hour"] / 24)缺失值处理
特征工程中的缺失值处理比数据清洗中的缺失处理更讲究——你不能简单地填充了事,因为模型训练时也需要相同的填充逻辑。
from sklearn.impute import SimpleImputer
# 数值列:用中位数填充
num_imputer = SimpleImputer(strategy="median")
df[["resources_used"]] = num_imputer.fit_transform(df[["resources_used"]])
# 类别列:用众数填充
cat_imputer = SimpleImputer(strategy="most_frequent")
df[["region"]] = cat_imputer.fit_transform(df[["region"]])更好的做法是加一个"是否缺失"标记列:
df["resources_missing"] = df["resources_used"].isna().astype(int)这告诉模型"这个值原本缺失"这个信息本身可能有预测力。
特征选择
你不一定需要用所有特征。特征选择的三个主流方法:
from sklearn.feature_selection import SelectKBest, f_regression, RFE
from sklearn.ensemble import RandomForestRegressor
# 方法1:单变量筛选
selector = SelectKBest(score_func=f_regression, k=10)
selected = selector.fit_transform(X, y)
# 方法2:递归特征消除
selector = RFE(RandomForestRegressor(), n_features_to_select=10)
selector.fit(X, y)
print("选中的特征:", X.columns[selector.support_])
# 方法3:正则化自动选择 — Lasso 会把不重要特征的系数压到 0常见陷阱
- 在分割数据之前做缩放和编码。这会导致数据泄露——验证集的信息混入了训练集的统计量中。一定要先分割,再在训练集上 fit,在测试集上 transform。
- 特征太多,样本太少。每增加一个特征就需要更多的样本来稳定估计。经验规则:样本数至少是特征数的 10 倍。
- 忽略特征交互。
duration * team_size的组合特征可能比两个单独特征更有预测力。
通关挑战
- 热身:在你的数据集中找出所有需要编码的类别列,选择正确的编码策略。
- 挑战:完成一个从原始数据到特征矩阵的管道——包括缩放、编码、时间拆分、缺失标记——并证明特征工程提升了模型表现。
- 观察:比较使用 StandardScaler 和 RobustScaler 后,线性回归系数的差异。
验收标准
- 能根据特征类型选择正确的编码和缩放策略
- 知道时间特征如何拆解
- 理解为什么要在数据分割后做缩放(避免数据泄露)
- 能用至少一种方法做特征选择
旅人笔记
好的特征让简单模型变得强大。特征工程花的时间,是建模阶段最高 ROI 的投入之一。
下一站预告
特征准备好了,模型也准备好了。但你确定你看到的"效果"是真实的吗?下一章,抽样与因果推断。