机器学习的项目,跟着网上的教程和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)
这个也不难理解,以上三个东西有没有,确实影响房价。
好的,到了这里,大家是不是有个疑问,为什么我们要生造这些新的字段?毕竟这些字段的值的来源,就是从原数据集中来的,模型为什么不能自己学习,而需要我们人类来告诉它呢?这个我们下篇文章再分析。