机器学习实践项目(二)- 房价预测增强篇 - 特征工程一

机器学习的项目,跟着网上的教程和AI的代码,也学了2个了,但是总不能都不是自己的,所以必须要基于代码做一些总结和深化。

这种项目的第一步,一般都是观察数据。有哪些字段、各字段的业务含义是什么、哪些有缺失值、各字段的取值是什么样的,就是所谓的know your business,这个就像我们干IT的,不懂业务肯定不行,连基本常识都缺乏是干不好项目的,但是这个暂时不是本文的讨论范围。

本文将从处理数据的第一步开始,也就是补全缺失值。先看下列代码。

python 复制代码
# 2.1 LotFrontage 按 Neighborhood(社区)分组的中位数进行缺失填补
# 原因:不同社区的临街长度分布不同,分组填补往往比全局中位数更合理
def impute_lotfrontage_by_neighborhood(df):
    df = df.copy()  # copy 防止原表被就地修改
    if "LotFrontage" in df.columns and "Neighborhood" in df.columns:
        # 对每条记录,取其所在 Neighborhood 的 LotFrontage 中位数
        med = df.groupby("Neighborhood")["LotFrontage"].transform("median")
        # 用该中位数填补缺失值;若个别社区全缺失,依然会留下 NaN(后续流程再兜底)
        df["LotFrontage"] = df["LotFrontage"].fillna(med)
    return df

对于数值类型的缺失来讲,我们不会直接给个0了事,常见的处理方式是给平均值。本文是房价预测,粗暴点的方式就是把所有房价取个平均赋给缺失字段。但是有更精细一点的做法。作为房价来讲,不同的街区房价一般都有差异,所以可以按街区取平均后赋给同街区缺失房价的数据,这样就更合理点,所以上述代码就是这样的精细的补全缺失值。

处理完缺失值,我们就进入了本项目最有价值的地方之一:根据现有字段构建强特征字段,这个也是促使我写下《增强篇》的直接原因。我们先看如下代码。

python 复制代码
# 2.2 常识型强特征构造
# 这些特征在房价题上非常常见且有效(面积合成、浴室总量/密度、门廊合并、房龄等)
def add_strong_features(df):
    df = df.copy()
    # 总面积:地上居住面积 + 地下室总面积(未完工面积也纳入)
    df["TotalSF"] = df.get("GrLivArea", 0) + df.get("TotalBsmtSF", 0)
    # 总"有效浴室数":半卫按 0.5 计入(地上、地下分别统计后合并)
    df["BathTotal"] = (df.get("FullBath", 0) + 0.5 * df.get("HalfBath", 0) +
                       df.get("BsmtFullBath", 0) + 0.5 * df.get("BsmtHalfBath", 0))
    # 浴室密度(每 1000 平方英尺的浴室数),刻画"是否拥挤/舒适"
    # replace(0, np.nan) 避免除以 0,/1000 仅做尺度调整
    df["BathPer1kSF"] = df["BathTotal"] / (df["TotalSF"].replace(0, np.nan) / 1000.0)
    # 门廊/露台面积合并:把多个门廊面积相加,降低稀疏性
    df["PorchSF"] = (df.get("OpenPorchSF", 0) + df.get("EnclosedPorch", 0) +
                     df.get("3SsnPorch", 0) + df.get("ScreenPorch", 0))
    # 房龄、翻新年距、是否"新房"(售出年等于建造年)
    if {"YrSold", "YearBuilt"}.issubset(df.columns):
        df["HouseAge"] = (df["YrSold"] - df["YearBuilt"]).clip(lower=0)  # clip 保证非负
    if {"YrSold", "YearRemodAdd"}.issubset(df.columns):
        df["SinceRemod"] = (df["YrSold"] - df["YearRemodAdd"]).clip(lower=0)
    if {"YrSold", "YearBuilt"}.issubset(df.columns):
        df["IsNew"] = (df["YrSold"] == df["YearBuilt"]).astype(int)
    # 三个"是否存在"类布尔变量,很多模型对"有/无"的信号很敏感
    df["HasGarage"] = (df.get("GarageArea", 0) > 0).astype(int)
    df["HasFireplace"] = (df.get("Fireplaces", 0) > 0).astype(int)
    df["HasPool"] = (df.get("PoolArea", 0) > 0).astype(int)
    return df
  • 可以看到,创造出来的第一个新字段是"总面积":
python 复制代码
    # 总面积:地上居住面积 + 地下室总面积(未完工面积也纳入)
    df["TotalSF"] = df.get("GrLivArea", 0) + df.get("TotalBsmtSF", 0)

我们平时讨论房子,说有120平大三房啥的,这120平就是房屋总面积,很少有人说我买了套客厅40平、主卧40平、客卧30平、卫生间10平的房子,因此,总面积似乎更有意义,也是影响房价的关键因素,为模型的学习减小了代价。

  • 创造出来的第二个新字段是"总有效浴室数":
python 复制代码
    # 总"有效浴室数":半卫按 0.5 计入(地上、地下分别统计后合并)
    df["BathTotal"] = (df.get("FullBath", 0) + 0.5 * df.get("HalfBath", 0) +
                       df.get("BsmtFullBath", 0) + 0.5 * df.get("BsmtHalfBath", 0))

有点类似总面积,总有效浴室数也是衡量房屋价值的一个关键因素,这个字段创造出来的原因也是因为其能统一浴室元素,简化参数,降低学习成本。

  • 生造出来的第三个新字段是"每1000平方英尺浴室数":
python 复制代码
    # 浴室密度(每 1000 平方英尺的浴室数),刻画"是否拥挤/舒适"
    # replace(0, np.nan) 避免除以 0,/1000 仅做尺度调整
    df["BathPer1kSF"] = df["BathTotal"] / (df["TotalSF"].replace(0, np.nan) / 1000.0)

光有浴室总数,但是和总面积相比,每1000平方英尺的浴室数量越大,很明显幸福指数就越高,这样就把浴室个数和总面积联系起来了,不是孤立看待,所以,这又是一个有意义的指标。

  • 创造的第四个字段是"门廊总面积":
python 复制代码
    # 门廊/露台面积合并:把多个门廊面积相加,降低稀疏性
    df["PorchSF"] = (df.get("OpenPorchSF", 0) + df.get("EnclosedPorch", 0) +
                     df.get("3SsnPorch", 0) + df.get("ScreenPorch", 0))

美国的房子,随着房子类型的不同,门廊类型也各不相同,每个种类的门廊不是每栋房屋都有,很多都是0,单独衡量门廊面积的话,可以看到大量的0存在。因此,创造门廊总面积的原因,除了和上面几个字段一样能统一指标降低复杂性,还有一个目的是降低稀疏性。

这里就牵扯到为什么稀疏性会影响模型,稀疏性的影响如下:

对机器学习模型的影响

  • 信息利用率低:大量零值使得模型难以学习有效模式
  • 维度诅咒:过多稀疏特征增加计算复杂度
  • 过拟合风险:稀疏特征容易在训练集上产生偶然相关性

对特征表达的影响

  • 噪声干扰:零值可能掩盖真实信号
  • 权重分散:同一概念被分散到多个特征中

因此,这种类型的字段,就需要合并起来,生成一个统一的新字段,减少稀疏性的影响。

  • 创造的第五个字段是"是否新房":
python 复制代码
    # 房龄、翻新年距、是否"新房"(售出年等于建造年)
    if {"YrSold", "YearBuilt"}.issubset(df.columns):
        df["HouseAge"] = (df["YrSold"] - df["YearBuilt"]).clip(lower=0)  # clip 保证非负
    if {"YrSold", "YearRemodAdd"}.issubset(df.columns):
        df["SinceRemod"] = (df["YrSold"] - df["YearRemodAdd"]).clip(lower=0)
    if {"YrSold", "YearBuilt"}.issubset(df.columns):
        df["IsNew"] = (df["YrSold"] == df["YearBuilt"]).astype(int)

这个就不难理解了,新房总是比二手房贵,这个是和房价非常相关的因素。"翻新年距"也即上次翻新到现在的年限,也是个影响房价的指标,看上去新的也比破破烂烂的值钱。

  • 创造的最后三个新字段就是"是否有车库 "、"是否有壁炉 "、"是否有游泳池":
python 复制代码
    # 三个"是否存在"类布尔变量,很多模型对"有/无"的信号很敏感
    df["HasGarage"] = (df.get("GarageArea", 0) > 0).astype(int)
    df["HasFireplace"] = (df.get("Fireplaces", 0) > 0).astype(int)
    df["HasPool"] = (df.get("PoolArea", 0) > 0).astype(int)

这个也不难理解,以上三个东西有没有,确实影响房价。

好的,到了这里,大家是不是有个疑问,为什么我们要生造这些新的字段?毕竟这些字段的值的来源,就是从原数据集中来的,模型为什么不能自己学习,而需要我们人类来告诉它呢?这个我们下篇文章再分析。

相关推荐
NAGNIP8 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab9 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab10 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP13 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年13 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼14 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS14 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区15 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈15 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang15 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx