简介:面向初学者数据分析员的这份PDF文档,围绕加州房价数据集(housing.csv)给出完整商业数据分析案例,适合对商业智能和假设检验感兴趣的学员。文档从数据基本描述与缺失值确认入手,深入探索房价中位数、湾区接近度等关键变量,并完成多变量相关性分析;针对变量预缩放问题,文档对比不同缩放比例并给出处理建议,还可引导读者新建“每户人口数”等新特征以提升分析信息量。通过分组计算均值与标准差,结合假设检验论证湾区位置对房价中位数的期望差异,使读者借助Python工具掌握从数据探索到建模分析的全流程。资源为单个PDF文件,压缩包大小为167KB,文档结构清晰、步骤完整,适合跟随实践。目前已有126人学习,是商业数据分析入门者可参考的案例资料。
1. 商业数据分析入门,从加州房价数据开始
商业数据分析入门,最怕第一课就把时间耗在"调参数、跑模型"上。加州房价数据正好能帮你绕开这个问题:它只有10来个字段,量纲、缺失、异常都差不多齐了,而且背后是"地段、人口收入、房屋属性如何影响房价"这个非常真实的商业问题。整个项目跑下来,其实是在练一个分析师最核心的套路:先把业务问题拆成可计算的指标,再让数据回答"影响因素的大小和方向",最后把结果放回业务场景里解释。我会按平时做项目的顺序展开,适合刚入门数据分析、或已经是开发但想补商业视角的读者。
2. 载入数据与业务口径:让加州房价数据变成可算的指标
第一件要做的事不是pd.read_csv,而是先把"价格"说清楚。加州房价数据里的median_house_value,单位是美元,统计的是某个"区块"的中位数房价,不是单套成交价。如果把它当成"单套房屋价格"去建模,后面所有解释都会变味。我一般先把字段列表打印出来,逐列确认业务含义,再动手写代码。下表是这份数据最常见的字段及其口径,后续所有特征和预测都围绕这些输入列展开。
| 字段 | 业务含义 | 单位/取值 | 建模时怎么用 |
|---|---|---|---|
| longitude / latitude | 区块中心经纬度 | 十进制数 | 转成地理特征,不能直接当数值用 |
| housing_median_age | 区块内房屋年龄中位数 | 年 | 可做分箱,或原样输入 |
| total_rooms | 区块内总房间数 | 间 | 结合人口做"人均间数" |
| total_bedrooms | 区块内总卧室数 | 间 | 缺失较多,需处理 |
| population | 区块总人口 | 人 | 和总收入配对计算 |
| households | 区块内家庭数 | 户 | 构造"户均人口" |
| median_income | 区块内家庭收入中位数 | 万美元,通常已缩放 | 最重要的连续特征 |
| median_house_value | 目标变量 | 美元 | 预测的 y |
2.1 先固定"价格口径",再做载入
看这张表时会发现median_house_value在很多公开版本里被截断到上限附近,通常是一串类似 500001 的数字。这带来的直接后果是:如果直接拿它做回归,模型会把所有高于上限的样本都当成同一个数,从而低估高价区域。处理方式通常有两种:一是把截断样本单独标记,在业务上另列一个"是否超上限"的二值特征;二是用专门处理截断的模型,但对入门阶段太复杂。最务实的做法是先给这些样本打一个标记列,保留在训练集里,但不要把它们的预测值当成精确价格。
确定口径之后,载入代码就变成了一段很短的惯例:
import pandas as pd df = pd.read_csv("california_housing.csv") print(df.shape) print(df.head()) # 给截断值打标:业务上"超上限"和"刚好上限"是两回事 df["is_price_capped"] = (df["median_house_value"] >= 500001).astype(int) # 先看缺失率,不急着填充 miss_rate = df.isnull().mean().sort_values(ascending=False) print(miss_rate[miss_rate > 0])这段代码里,read_csv之后先看行列数和前几行,是为了确认列名和单位没有因来源不同而变样。is_price_capped把价格截断变成显式标记,后面建模时可以作为特征,也可以用来单独评估子集表现。最后输出缺失率而不是直接填充,是因为total_bedrooms这类字段的缺失可能隐含"小户型区块没有统计",需要结合业务判断。
2.2 缺失值、异常值与"不该删"的样本
入门项目最容易犯的错误是把total_bedrooms的缺失值行整行删掉。实际处理时我会先算一下缺失率,如果只是少量随机缺失,用中位数填充就够;如果缺失集中在某个地理区域,就不能全局填充,而要按经纬度分组后取组内中位数。对于异常值,total_rooms除以households得到一个"户均房间数",如果大于 10 或小于 1,先查原始数据是不是口径错了,而不是直接删。
# 按地理分桶填充:先把经纬度切成 20 段,再拼接成桶 df["geo_cluster"] = pd.cut(df["longitude"], bins=20).astype(str) + "_" + \ pd.cut(df["latitude"], bins=20).astype(str) df["total_bedrooms"] = df.groupby("geo_cluster")["total_bedrooms"] \ .transform(lambda x: x.fillna(x.median()))这段逻辑是:用pd.cut把经纬度分别切成 20 个区间,拼接成地理分桶,再在分桶内部填充缺失值,替代全局中位数。transform保证填充后的 DataFrame 索引不变,方便原地赋回。切 20 桶是经验值,样本量少时可以降到 10,保证每个桶至少有几十个样本落进来,避免中位数因为样本太少而噪声过大。填充完,可以再用df.isnull().sum()复查一遍;如果仍有缺失,说明某些桶内全是空值,那就退回全局中位数。
2.3 数据类型的隐性问题:经纬度不要当普通数字
另一个入门阶段容易踩的坑是:longitude和latitude看起来是浮点数,直接塞进线性模型也能跑,但"位置"诱导的房价变化是非线性的。湾区、洛杉矶和内陆山谷的房价差距来自地理区块属性,而不是经纬度的线性单调关系。因此我会把这两个字段拆出两种用法:一是保留原始值给树模型做切分,二是基于经纬度构造地理分桶特征。入门阶段更简单的方式是先保留经纬度,同时加入geo_cluster作为类别特征,后面在特征工程章节再详细展开。
提示:不要用
df.describe()里的数值范围去判断经纬度是否异常。加州的经度在 -124 到 -114,纬度在 32 到 42,这本来就是真实分布,不是离群点。
到这一步,你已经得到的是一份"口径清楚、缺失填好、价格上限被标记"的干净表格。这个阶段花掉整个项目 30% 的时间是正常的;后续建模再快,也弥补不了脏数据导致的错误商业结论。
3. 特征工程与洞察:从人口、收入、地理位置里拆出"房价解释变量"
数据清洗完之后,不要马上跑模型。商业数据分析的核心步骤是"让解释变量有明确的业务含义",这一步也叫特征工程。加州的房价数据天然自带这样一个优势:它包含的社会经济变量(收入、人口、家庭数)和地理变量(经纬度、房龄)都能直接映射到"住在这里的人是谁、住得怎么样"的商业叙事。在做建模前,我会先做一个相关性矩阵,目的是找到"单变量与房价关系最强"的字段,避免一开始就陷入复杂模型。
3.1 用相关性矩阵找"初步解释力"
numeric_cols = df.select_dtypes(include=["number"]).columns corr = df[numeric_cols].corr()["median_house_value"].sort_values(ascending=False) print(corr)这里select_dtypes排除geo_cluster这种字符串列,corr()默认用皮尔逊相关系数,评估的是线性相关性。在公开的加州房价数据上,median_income通常是唯一一个相关系数超过 0.6 的字段,其他如房间数、家庭数都是靠"合成特征"才能提高解释力。这说明两件事:第一,模型的起点应该放在收入上;第二,单纯把total_rooms、population都塞进模型不会带来太多增量,反而会增加多重共线性风险。
如果是用 Jupyter 而不是纯脚本,pd.plotting.scatter_matrix可以画散点矩阵,但字段一多就看不清楚。我一般只挑median_income、median_house_value、latitude三个字段做两两散点,因为latitude能直观看到南北加之间的价格分层。
3.2 构造有可解释性的合成特征:人均而不是总量
原始字段里有很多"总量"字段,但商业决策更关心"密度"。total_rooms大,不一定代表居住密度高,可能是这个区块以大户型的独栋为主。所以我会构造这三个比例特征:
df["rooms_per_household"] = df["total_rooms"] / df["households"] df["bedrooms_per_room"] = df["total_bedrooms"] / df["total_rooms"] df["people_per_household"] = df["population"] / df["households"]这三个特征分别回答三个问题:房屋大不大(房间数/户数),卧室密度高不高(卧室数/房间数),住得挤不挤(人口/户数)。它们比原始字段更接近"居住形态"这个真实概念,而且量纲统一在 1 附近,线性模型更容易收敛。构造完之后,我习惯用df[["median_house_value"] + 这三个列].corr()再跑一次相关性,确认这些比例特征没有偷走原始字段的所有信息。
3.3 对房龄、收入做分箱:把连续变量改成业务段位
连续数值直接进模型时,默认的是"每增加一个单位,房价变化一个固定斜率",这对收入并不成立。收入在 2 万和 5 万之间的房价增量,远大于 8 万和 11 万之间的增量。所以我习惯把收入分成几个业务档位:2.5 万以下、2.5-5 万、5-7.5 万、7.5-10 万、10 万以上,然后做 one-hot 或 ordinal。同样,房龄也可以分成"新房(10年内)""主力房(20-50年)""老房(50年以上)"。
分箱不是越多越好。过细的区间会让某些类别在后续交叉分析时样本太少,过粗又会丢掉曲线形态。入门阶段先用 4-6 个区间,再看每个区间里的平均房价,确认单调性是否和业务直觉一致。如果出现"收入越高房价反而下降"的异常箱,先回到数据里看该箱的样本量,通常是因为样本太少或包含被截断的异常值。商用落地时不要把这个分箱阈值写死,应该把它和模型注册表放在一起,方便后续版本回溯。
3.4 经纬度怎么用:分桶、局部均值与聚类编码
把geo_cluster直接作为类别特征是一种方式,但它太粗糙,20×20 分桶在数据稀疏时会产生大量空桶。更稳妥的做法是用"局部均值编码":对每个桶计算历史平均房价,把这个平均值作为一个特征给到模型。这会带来标签泄漏风险,必须用交叉验证内部分开计算;入门阶段也可以简化为用KMeans对经纬度做聚类,把簇号作为类别特征。
from sklearn.cluster import KMeans kmeans = KMeans(n_clusters=15, random_state=42, n_init="auto") df["region_id"] = kmeans.fit_predict(df[["longitude", "latitude"]])n_clusters=15是把加州按地理位置聚成 15 个片区,fit_predict给每个样本打上片区标签。这个特征进来之后,等于告诉模型"这个位置属于南加沿海还是中央山谷"。实际使用中,可以把n_clusters调成 20-30 看验证集变化,如果提升不明显就保留 15,避免过拟合。还要注意:KMeans对尺度敏感,经纬度本身已经处于同样的度量尺度(度数),所以不需要标准化;这是特征工程里一个少见的例外。
完成这一步后,模型可用的特征扩展到十几个:"人均"类特征 3 个,分箱类别 2 个,区域编号 1 个,再加上原始 8 个字段。接下来要做的不是继续堆特征,而是用一版简单模型评估它们的实际贡献。
4. 用线性回归和随机森林跑通一版基准模型:参数怎么设、指标怎么读
特征工程做完,就可以进入建模。商业数据分析的建模和算法比赛不一样,目的不是为了把 RMSE 压到最低,而是搞清楚"哪些因素在解释房价差异、解释力度有多强"。所以我会同时做两个模型:一个线性回归作为可解释基线,一个随机森林作为非线性对照。如果线性回归已经能解释 60% 以上的方差,通常不需要太重的高阶模型;如果差距明显,说明变量之间确实有非线性关系,再上树模型。
4.1 训练集和测试集:按区域切分,避免"同区信息泄漏"
做切分前,先想一个问题:同一套数据里相邻经纬度的样本,环境高度相似,随机切分会让模型"记住"地理位置而不是学到规律。我一般会先按geo_cluster分组,用GroupShuffleSplit来切分,而不是默认的train_test_split。入门项目如果不想引入额外复杂度,至少也要设置random_state固定,并记录切分结果,否则后续修改特征后很难对比效果。
from sklearn.model_selection import train_test_split feature_cols = [ "longitude", "latitude", "housing_median_age", "median_income", "rooms_per_household", "bedrooms_per_room", "people_per_household" ] X = df[feature_cols].copy() y = df["median_house_value"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) print("train size:", X_train.shape, "test size:", X_test.shape)这里先用 7 个字段做一版最小模型。test_size=0.2是常见的 80/20 切分;如果样本量只有几千,建议用test_size=0.25或交叉验证,保证测试集上的误差估计更稳定。random_state=42只是固定种子,换成其他数字结果差别不大,但必须有。真正到商业环境里,我会把这次切分的索引文件另存下来,以便以后加入新数据时做等长对比。
4.2 线性回归基线:标准化、共线性与系数解释
线性回归对特征尺度敏感度不高,但如果你后续要做Lasso或Ridge,就必须标准化。至少对树模型以外的模型,我会统一加一个StandardScaler,避免收入和房龄量纲差异造成数值问题。同时,检查一下特征间的相关性,特别是rooms_per_household和people_per_household之间,若不放心可以做一个方差膨胀因子检验,但入门阶段可以直接观察相关性矩阵。
from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, r2_score model_lr = make_pipeline(StandardScaler(), LinearRegression()) model_lr.fit(X_train, y_train) y_pred_lr = model_lr.predict(X_test) print("MAE:", mean_absolute_error(y_test, y_pred_lr)) print("R2:", r2_score(y_test, y_pred_lr)) # 查看系数方向是否符合业务直觉 coef = model_lr.named_steps["linearregression"].coef_ for col, c in zip(feature_cols, coef): print(f"{col}: {c:.1f}")make_pipeline把标准化和回归串起来,fit时会先在训练集上计算均值和方差,再用同一组参数变换测试集,避免测试集信息混入训练过程。MAE是直接的单位误差,能让你理解"平均差多少钱";R2则是模型解释力。系数方向很关键:正常情况下median_income系数为正、latitude系数为负(加州北部大多数项目相对便宜),如果方向反了,说明前面清洗时把字段单位搞错了,或者存在严重的共线性。
4.3 随机森林对照:先设n_estimators和max_depth
随机森林在表格数据上通常比线性回归好,但代价是解释性下降。我用它主要做两件事:一是验证线性模型是否有捕捉不到的非线性;二是用feature_importances_反向检查特征构造有没有用。参数设置上,先固定n_estimators=500,max_depth控制在 15 以内,其余用默认值。n_estimators过大只会增加训练时间,500 棵树足够稳定;max_depth限制单棵树深度,避免模型为记忆一个异常样本无限分裂。
from sklearn.ensemble import RandomForestRegressor model_rf = RandomForestRegressor( n_estimators=500, max_depth=15, min_samples_leaf=5, random_state=42, n_jobs=-1 ) model_rf.fit(X_train, y_train) y_pred_rf = model_rf.predict(X_test) print("RF MAE:", mean_absolute_error(y_test, y_pred_rf)) print("RF R2:", r2_score(y_test, y_pred_rf)) for name, imp in zip(feature_cols, model_rf.feature_importances_): print(f"{name}: {imp:.3f}")min_samples_leaf=5强制叶子节点至少要有 5 个样本,是防止过拟合的常见做法。n_jobs=-1告诉模型用满所有 CPU 核。特征重要性输出后,如果某个没人见过的合成特征(比如bedrooms_per_room)排很靠前,说明它对预测有实质贡献;如果原始字段全部排后面,则需要回到特征工程审视是否构造出了噪声。
4.4 残差落在哪里?用空间分布发现模型盲区
训练结束后,比指标更有价值的是残差分析。把y_test - y_pred_rf按经纬度画成散点图,能直接看出模型在哪些地理片区系统性高估或低估。我通常的做法是:
- 把测试集预测结果和经纬度放进一个临时 DataFrame;
- 按残差绝对值分组:超过 10 万美元的标成"高误差";
- 统计高误差样本集中在哪些
region_id,并回去查这些区域的平均房龄、收入,判断是数据稀疏还是特征缺失。
这一步能给出直接的商业建议:如果你要为一个新的在建楼盘估值,但这个楼盘位于模型高误差地带,那么模型输出只能作为粗筛,需要人工实地校准。残差空间化是"商业数据分析"与普通"机器学习练习"的分水岭:前者必须把模型失败的地方翻译成业务风险。
5. 用模型结果给业务一个可操作的答案:从"预测价格"到"分析结论"
最后一章不再追求调参,而是把模型翻译成业务口径。你在交付时可能面对的是运营、投资或数据产品负责人,他们不关心R2,更关心"哪个因素每上升一档,房价大约变化多少"。所以,我会把回归系数转成业务语言,同时给出验证清单,防止后续别人重复使用时踩坑。
5.1 把回归系数做成"每单位影响"的表格
用线性回归时,系数可以直接作为解读。注意:由于做了标准化,系数代表的是"该特征变化一个标准差时房价的变化"。如果要看原始单位,需要对比每个特征的均值差。给业务看时,不如做一个简易对照表,比如收入从 3 万到 6 万美元时,模型预测房价变化的幅度。
import numpy as np baseline = X_train.median().to_dict() income_lo = baseline.copy(); income_lo["median_income"] = 3.0 income_hi = baseline.copy(); income_hi["median_income"] = 6.0 pred_lo = model_lr.predict(pd.DataFrame([income_lo]))[0] pred_hi = model_lr.predict(pd.DataFrame([income_hi]))[0] print(f"收入从 3 万到 6 万,房价预测变化: {pred_hi - pred_lo:,.0f} 美元")切换到income_lo和income_hi时,其他特征统一用训练集中的中位数,这叫"单变量边际测算"。它反映的是"在其他条件相同时,收入档位的价格影响",是向业务解释回归系数最直观的方式。这个表格可以直接导出到 Excel 作为交付物的一部分。
5.2 三个隐藏的业务坑:截断、共线性、不可外推
第一,价格截断导致的高价区域被低估。模型预测值不应该被当作精确报价,而是看作"在当前样本分布内的排序估计"。第二,median_income和地理位置高度相关,某个系数可能吸收了"片区"的影响,不要对它的绝对值过度解读。第三,模型只适用于加州范围内类似形态的区块推广;当你把预测应用到全新开发的区域时,特征范围可能超出训练数据,树模型会给一个平庸的平均值而不是合理外推。
5.3 给分析师的快速验证清单
最后分享一个我每次交模型成果前都会过一遍的清单:
- 重新跑一次
df.info()和df.describe(),确认没有把字符串列漏进模型; - 检查
train_test_split的随机种子是否固定,能不能复现; - 看训练集和测试集的目标变量均值是否接近,如果差异超过 10%,说明切分不随机;
- 对特征系数/重要性做一次方向检查,反向的必须有解释,不能带着"模型自己学的"就交付;
- 找一个高误差区域,人工调查一下该区域最近有没有新建大型基础设施,这些无法从静态数据中看到的变化是模型误差的来源。
把这个清单固化成一个脚本,每次拿到新数据都自动跑一遍,你就把一次性的项目变成了可持续迭代的日常分析流程。
本文还有配套的精品资源,点击获取