机器学习实践项目(二)- 房价预测增强篇 - 特征工程四
好的,上一篇文章我们提到了独热编码(One-Hot)是一个非常妙的对于分类类型字段的处理方式,它把模型不能识别的字符型的类别字段,转换成了0/1为值的新列,既解决了模型不能识别分类值的问题,又不去“错误暗示”分类之间有大小关系。但是妙则妙矣,这么多0/1值,其实也挺难理解的,而且多了n个字段,其中只有一个字段是1,其余全部为0,有点傻傻的。
解决这个问题的方法之一就是分层目标编码。老规矩,解释知识让AI来:
「目标编码(Target Encoding)」的安全实现版本(带 OOF + 平滑)。
我们来一步步拆解,让你完全理解它的作用、原理👇
🌟 一句话解释:
它用目标值(SalePrice)的平均水平来数值化一个类别特征,并且通过 K 折分层来避免“信息泄漏”。
换句话说:
这个函数的任务就是把一个类别型变量(如 Neighborhood),
转换成一个与房价相关的数值特征(类别对应的平均房价),
并保证这个过程不会在训练中“偷看答案”。
🧠 一、目标编码(Target Encoding)是啥?
假设我们有一个类别特征:
| Neighborhood | SalePrice |
|---|---|
| CollgCr | 200000 |
| CollgCr | 220000 |
| OldTown | 120000 |
| NridgHt | 320000 |
传统的 One-Hot 会生成三列 0/1 矩阵,而目标编码更聪明:
用该类别的目标平均值代替它。
得到新特征:
| Neighborhood | SalePrice | Neighborhood_te |
|---|---|---|
| CollgCr | 200000 | 210000 |
| CollgCr | 220000 | 210000 |
| OldTown | 120000 | 120000 |
| NridgHt | 320000 | 320000 |
这就让模型直接“知道”:某个社区的平均房价水平。
对于高基数特征(比如上百个类别),比 One-Hot 更高效、解释性更强。
🧩 二、问题:这样会“偷看答案”
如果我们直接用全量 y 算均值(比如上面 210000),
那模型在训练时已经看到了目标 SalePrice ——
这就是数据泄漏(data leakage)。
会让交叉验证看起来分数高,但在真实预测中掉分。
🚧 三、解决方案:Out-Of-Fold(OOF)编码
这个函数做的就是 OOF Target Encoding。
原理是:
1️⃣ 把训练集分成 n_splits(默认 5 折)。
2️⃣ 每次只用“训练折”的数据去计算类别均值。
3️⃣ 再把均值应用到“验证折”。
这样每一行的编码都来自模型未见过的折,就不会泄漏目标信息。
🔢 四、代码逻辑逐行解释
y_log = np.log1p(y)
global_mean = y_log.mean()
👉 对房价取对数(Kaggle 房价比赛的标准做法),
在 log 空间计算类别平均价格,稳定性更高。
global_mean 是全局平均房价,用作平滑与缺失的兜底值。
kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)
te = pd.Series(index=X.index, dtype=float)
👉 初始化 5 折交叉验证 + 存放结果的空列。
for tr_idx, va_idx in kf.split(X):tr, va = X.iloc[tr_idx], X.iloc[va_idx]y_tr = y_log.iloc[tr_idx]
👉 进入每一折:
tr是“训练折”;va是“验证折”;- 只在训练折上计算编码,避免泄漏验证折的
y。
means = tr.groupby(col).apply(lambda d: y_tr[d.index].mean())
counts = tr[col].value_counts()
👉 在训练折中:
means是每个类别的平均 log(房价);counts是每个类别出现次数(用于平滑)。
enc = (means * counts + global_mean * smooth) / (counts + smooth)
👉 平滑公式:
当类别样本数小(如只出现一次),直接取均值会不稳定;
通过加上全局均值 global_mean 按比例混合,稳定效果。
smooth 越大,越偏向全局均值。
te.iloc[va_idx] = X.iloc[va_idx][col].map(enc).fillna(global_mean)
👉 把这一折的验证数据用训练折的编码映射;
未见过的类别用全局均值代替。
这样得到的 te 是一个与原训练集行数相同的新特征,
但它的每一行值都来自“别的折”的均值 → 完全避免泄漏。
🔮 五、测试集的处理
means_full = X.groupby(col).apply(lambda d: y_log[d.index].mean())
counts_full = X[col].value_counts()
enc_full = (means_full*counts_full + global_mean*smooth) / (counts_full + smooth)
te_test = X_test[col].map(enc_full).fillna(global_mean)
👉 对测试集可以安全地用全量训练数据来算编码,因为测试集没有目标泄漏风险。
🧩 六、最后一步:加新列
X[f"{col}_te"] = te.values
X_test[f"{col}_te"] = te_test.values
return X, X_test
这一步生成一个新的特征列,比如 Neighborhood_te,
表示该样本所在社区的“平均 log 房价水平”。
✅ 七、效果总结
| 特征 | 转换前 | 转换后 |
|---|---|---|
| Neighborhood | CollgCr / OldTown / … | 数值(代表类别平均房价) |
| 技术点 | 作用 |
|---|---|
log1p(y) | 稳定波动,贴合 RMSE 评测 |
| KFold OOF | 避免目标泄漏 |
| 平滑 | 防止稀有类别导致过拟合 |
| 训练/测试分别编码 | 保证一致性与鲁棒性 |
💡 举个直观例子
假设:
| Neighborhood | SalePrice |
|---|---|
| A | 100000 |
| A | 120000 |
| B | 250000 |
| C | 500000 |
全局均值(log1p 后)大约等于 log(242500)。
折分后:
- 第1折训练 A、B → 验证 C → C_te = log(平均B/A价);
- 第2折训练 A、C → 验证 B → B_te = log(平均A/C价)。
训练集每一行都没看自己的真实价格。
最终模型能利用这些 *_te 特征捕捉“地区价格等级”,提升预测力。
🏁 八、总结一句话
oof_target_encode()的作用是:✅ 将类别特征数值化为“类别对应的平均目标值”;
✅ 使用 K 折 OOF 机制防止信息泄漏;
✅ 用平滑控制稀有类别稳定性;
✅ 最终输出训练/测试都包含的安全编码特征。
其实看到这里,什么是“目标编码”就很好理解了,但是我第一次看这个解释时,并没有明白何为“分层”,即K-Fold,那我们继续请教爱老师。
KFold(K 折交叉验证)其实一点都不复杂,
我用生活比喻 + 图解的方式,帮你彻底搞懂它👇
🌰 一、直白解释:
KFold 就是“把训练集切成几块,轮流用一块来测试,其他几块来训练”的办法。
举个生活例子 🍰
你有一个蛋糕(数据集),要知道它的味道(模型好不好)。
如果你只尝一口,可能不准;
所以你把蛋糕切成 K 份(比如 5 份):
| 折数 | 用来训练的部分 | 用来验证的部分 |
|---|---|---|
| 第1折 | 第2~5份 | 第1份 |
| 第2折 | 第1,3,4,5份 | 第2份 |
| 第3折 | 第1,2,4,5份 | 第3份 |
| 第4折 | 第1,2,3,5份 | 第4份 |
| 第5折 | 第1~4份 | 第5份 |
然后你做 5 次实验,每次:
- 在“训练部分”训练模型;
- 用“验证部分”测试模型的表现。
最后,把这 5 次的表现(比如 RMSE)取平均值,
就得到了一个更稳定的结果。
📊 二、图解理解(假设 K=5)
数据集索引: [1 2 3 4 5 6 7 8 9 10]第1折: 训练=[3,4,5,6,7,8,9,10], 验证=[1,2]
第2折: 训练=[1,2,5,6,7,8,9,10], 验证=[3,4]
第3折: 训练=[1,2,3,4,7,8,9,10], 验证=[5,6]
第4折: 训练=[1,2,3,4,5,6,9,10], 验证=[7,8]
第5折: 训练=[1,2,3,4,5,6,7,8], 验证=[9,10]
你看,每一折都会轮流当一次“验证集”,
其他数据则都用来训练。
所以每个样本都被模型预测过一次,但不会参与预测自己。
这就能避免模型“作弊”看到答案。
💡 三、为什么要这么麻烦?
如果你只随机分一次训练/验证:
- 运气好:分得刚好平衡 → 分数看起来很好;
- 运气差:验证集偏、含噪声多 → 分数很差。
而 KFold 多次轮流验证,就像:
“我不看一次考试成绩,而看五次平均分。”
这样更稳定、更可靠。
它能告诉你模型整体表现,而不是“某一次偶然分割”的结果。
🧮 四、在 oof_target_encode 里的作用
回想我们在做“目标编码”时:
- 我们要用
y(房价)去算类别均值; - 但又不能“偷看自己”。
KFold 就帮我们做到这点👇
| 折 | 用来计算均值的行(训练折) | 被赋值的行(验证折) |
|---|---|---|
| 1 | 折2~5 的数据 | 折1 的数据 |
| 2 | 折1,3,4,5 的数据 | 折2 的数据 |
| … | … | … |
所以对于每个样本,它得到的编码值,
都来自其它样本,而不是自己那一折的目标值。
这就完全避免了“信息泄漏”。
简单说:KFold 让我们“用别人家的答案算平均”,
而不是“用自己的答案作弊”。
🧩 五、为什么叫 “Out-Of-Fold” (OOF)
- “Fold” = 折。
- “Out-Of-Fold” = 不在这一折里的数据。
即:
每个样本的预测/编码,来自于它不在的那几折的模型或均值。
所以叫 Out-of-Fold 编码。
🚀 六、类比理解
| 场景 | 类比 |
|---|---|
| 你考试 5 次,每次都换不同试卷(验证集)。 | → KFold |
| 老师不让你看自己卷子的答案,但可以看同学们的平均分来推测难度。 | → Out-of-Fold Target Encoding |
| 最后你拿 5 次考试的平均分作为期末成绩。 | → 平均 RMSE 作为模型得分 |
🧠 七、总结一句话
KFold 就是反复切分数据,让模型在“看不到自己”的情况下多次验证,从而得到稳健、无泄漏的结果。
好了,那到此为止,大家明白什么是分层目标编码了吧?
