
房价预测的核心是什么这段代码值得你跑一遍这两年身边越来越多朋友开始学Python但大部分人在学完基础语法之后就卡在了“下一步干什么”这个阶段。循环、列表、字典都会写了可真要面对一个实际问题又不知道从哪下手。我特别推荐拿“房价预测”这个题目来练手因为它的数据结构简单、目标明确、可扩展性也强而且背后用到的线性回归模型正是机器学习里最基础也最能帮你建立直觉的一个算法。你不需要上来就啃复杂的神经网络也不需要装一堆跑不动的“大家伙”只要环境里有Python和几个常用库把这些代码跑通基本就等于一只脚已经迈进机器学习大门了。这篇实战笔记不会跟你绕弯子会直接给你一套可以跑通的代码和完整的建模流程。数据我们先用内置的波士顿房价数据集做演示后面我会告诉你如果换成真实房源数据哪些地方需要调整。如果你是刚接触这方面的纯新手建议你把每一段代码都亲手敲一遍再回头看看最终的预测效果如果你已经有一些编程基础可以直接跳到后面的问题排查部分里面有几个常见坑我想重点提一下。1. 项目整体思路从数据到预测线性回归需要走哪几步1.1 为什么选房价预测当练手项目房价预测之所以在机器学习入门项目里出现的频率这么高不是因为这门课把复杂问题简化了而是它的数据特性非常适合用来讲清楚“回归”这个概念。什么是回归简单说就是根据已知的特征值去预测一个连续的数值结果。房价是连续的数值不是一个分类标签比如你猜测某个小区的房子大约值两百万还是三百万这是一个区间性的估值问题跟判断“是猫还是狗”这种分类问题不太一样。跟其他常见数据集相比房价数据还有几个优点特征基本是结构化数据没有图片或文本那种复杂的预处理需求特征之间大多存在相关的、可解释的关系方便你做各种可视化样本量不大普通个人电脑跑起来几乎没有压力。对于刚入门的人来说这种“轻量但是五脏俱全”的项目正合适用来熟悉机器学习的完整流程。1.2 线性回归建模的基本流程拆解打个比方你去买二手房时要考虑哪些因素地段、面积、楼层、朝向、房龄这些都是判断房价的输入变量而最终的成交价就是这个输入变量映射出来的结果。线性回归做的事就是找到一组权重系数让这些特征值通过加权求和后跟你手头已知的真实房价尽可能接近。这里先给你一个极简版的理解方式在只有面积这一个特征的情况下预测房价其实就是在坐标轴上画一条直线。横轴是面积纵轴是价格我们要找到一条“画得最准”的直线让它穿过尽可能多的已知数据点。当特征变成一个以上时这条直线就升级成一个多维空间里的函数但核心思想不变还是找合适的系数让拟合误差最小。1.3 准备环境没有这些库代码跑不起来动手之前先把环境准备好。我默认你用的是Python 3.8以上的版本如果版本太低部分依赖库可能会报错。当你打开终端或者命令行需要确认几件事Python环境没问题、pip可用、能正常安装第三方库。如果你还没配置过我建议用conda或者venv单独创建一个虚拟环境来跑这个项目避免跟系统里其他项目互相干扰。我测试用的环境配置大概是这样的Python 3.10pandas 2.0numpy 1.24scikit-learn 1.2matplotlib 3.7。这几个版本搭配起来运行很稳定如果你用的版本跟这个有一定差距只要不是太旧一般问题不大。2. 数据预处理的细节拿到数据第一件事不是建模2.1 认识波士顿房价数据集波士顿房价数据集是经典教材里最常见的练习数据之一虽然现在新版scikit-learn里已经把它移除了但我们仍然可以换个渠道加载它。整个数据集一共有506条样本每条样本包含13个特征和一个目标值这13个特征分别对应城镇各种属性教育、交通、就业、房价等相关的统计指标。我先把每个特征用表格给你列出来方便你建立初步印象特征名含义说明CRIM城镇人均犯罪率数值越大通常代表治安风险越高ZN占地面积规划用途相关的比例数据INDUS城镇非零售商业用地比例衡量区域商业配套情况CHAS是否邻近河流虚拟变量取值为0或1NOX一氧化氮浓度空气质量的间接指标RM平均房间数反映房屋体量AGE自住房屋房龄数值越高说明老房子越多DIS到就业中心的距离衡量通勤便利度RAD径向公路可达指数交通便利程度的度量TAX房产税率税率越高可能对应更好的公共服务PTRATIO师生比间接反映教育资源配置B黑人比例相关的统计量过去数据有历史争议现在很少用LSTAT低收入人群占比区块收入水平的一个指标严格说这个老牌数据集里的个别特征放在今天来看是有争议的我们不展开谈这个作为教学用途它的核心价值在于能让初学者快速跑通各个流程。2.2 为什么新版scikit-learn里找不到load_boston了不少同学会照着我这种老教程去写代码结果一条“import error”就卡住了。原因不复杂从scikit-learn 1.2版本开始波士顿房价数据集就被正式移除了官方给出的理由是这些数据带有一些历史和社会偏见的问题。如果非要用原始数据网上还能找到load_boston的替代写法也就是从外部加载一个数据文件但更省事的做法是直接用sci-kit learn内置的fetch_california_housing数据集或者自己构造一份模拟数据。在这个实战里我用一个相对均衡的方案继续使用波士顿房价数据但是通过远程URL加载的方式把它读进来。这种方式既能保留原数据集的特征结构也不依赖特定版本的库大家照着写就能跑通。2.3 用pandas检查数据质量数据加载进来之后不要急着训练模型要像看体检报告一样先瞄一眼数据的“健康状态”。我们要确认三点数据规模是多少、有没有缺失值、特征的数值范围是不是互相差异太大。下面这段代码可以帮你快速掌握数据的总体情况import pandas as pd import numpy as np # 通过远程URL加载波士顿房价数据 data_url http://lib.stat.cmu.edu/datasets/boston raw_df pd.read_csv(data_url, sep\\s, skiprows22, headerNone) data np.hstack([raw_df.values[::2, :], raw_df.values[1::2, :2]]) target raw_df.values[1::2, 2] df pd.DataFrame(data, columns[ CRIM, ZN, INDUS, CHAS, NOX, RM, AGE, DIS, RAD, TAX, PTRATIO, B, LSTAT ]) df[MEDV] target print(数据集形状:, df.shape) print(前5行数据:) print(df.head()) print(缺失值统计:) print(df.isnull().sum().sum())跑完之后你应该能看到的输出是506行数据14个字段缺失值数量为0。如果缺失值不是0那你就要认真处理了常见办法可以是直接删除包含缺失值的行也可以用均值或中位数去填充。处理缺失值的思路取决于数据量大小和缺失比例如果只有个别样本缺失直接删掉影响不大如果某个字段缺失比例很高则需要考虑这个特征还有没有保留的必要。还有一点值得注意数据里有几个特征的量纲差异是挺大的。比如TAX的数值可能是几百而CHAS基本就是0和1。在后面对模型做可解释性分析时这种差异会影响你的判断不过在纯scikit-learn的线性回归里量纲差异不会直接导致训练中断但对有些算法来说就会变成问题。为了稳妥起见我后面会专门做一步特征标准化。3. 核心代码实现手写线性回归公式与调用sklearn3.1 从数学原理讲透线性回归的损失函数线性回归的目标就是找到一组参数w让预测值跟真实值的差距尽可能小。那怎么衡量这个差距最常用的方式是均方误差也就是把所有样本的预测误差先平方、再求平均。为什么是平方而不是绝对值因为平方之后误差大的样本会受到更重的“惩罚”这样模型会更认真地对待那些偏差大的数据点。用公式来表示就是J(w) (1/2m) * Σ( h(x_i) - y_i )^2这里的h(x_i)是预测值y_i是真实值m是样本数。前面那个1/2纯粹是为了后面求导数时计算方便不影响最终结果。有了这个损失函数接下来就是怎么让它最小化的问题。最常见的求解方法有两种正规方程法和梯度下降法。正规方程是直接通过矩阵运算求出闭式解优点是只需一步就能算完缺点是当特征非常多或矩阵不可逆时会失效梯度下降则是通过反复迭代更新参数让损失函数的值一步一步变小。新手可以先理解梯度下降的思路好比你在山里大雾弥漫找不到路但你只需要朝最陡的下坡方向迈步每一步都在下降慢慢就会走到谷底。3.2 自己动手写梯度下降理解核心原理对着公式看总有点“纸上谈兵”的感觉真正确保你理解算法的是手写一遍。下面这段代码是一个简化版梯度下降实现特征只有一维方便你直观观察参数是如何变化的import numpy as np # 构造简易的房价与面积数据 area np.array([50, 70, 90, 110, 130, 150, 170, 190]) price np.array([150, 200, 230, 280, 340, 380, 430, 480]) # 简单归一化方便梯度下降计算 area_norm (area - area.mean()) / area.std() def compute_cost(w, b): pred w * area_norm b cost np.mean((pred - price) ** 2) / 2 return cost def gradient_descent(area, price, w, b, lr, iterations): m len(area) for i in range(iterations): pred w * area b dw (1 / m) * np.sum((pred - price) * area) db (1 / m) * np.sum(pred - price) w - lr * dw b - lr * db if i % 100 0: print(fIteration {i}: w{w:.3f}, b{b:.3f}, cost{compute_cost(w, b):.3f}) return w, b # 将学习率设为0.01迭代1000次 w, b gradient_descent(area_norm, price, 0.1, 100, 0.01, 1000) print(f最终w: {w:.3f}, b: {b:.3f})运行这段代码你能非常直观地看到参数是怎么从初始值一步步调整到稳定状态的。一开始损失函数值很大多次迭代之后慢慢收敛到一个较小的数值。这就是“训练”的本质看懂了这里后面再调用sklearn的现成接口时你会清楚包在它内部的到底发生了什么。3.3 用sklearn一行代码拟合线性回归模型理论懂了之后我们就要进入实战环节。前面手写的代码只是为了让你理解原理真正做项目时没必要自己造轮子直接用scikit-learn封装的LinearRegression就好它内部实现更稳定性能也更好。完整代码如下import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error, r2_score import matplotlib.pyplot as plt # 继续使用前面加载的df数据 X df.drop(MEDV, axis1) y df[MEDV] # 划分训练集和测试集test_size0.2表示20%的样本用于测试 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) model LinearRegression() model.fit(X_train, y_train) # 预测 y_pred model.predict(X_test) # 评估 mse mean_squared_error(y_test, y_pred) rmse np.sqrt(mse) r2 r2_score(y_test, y_pred) print(f模型截距: {model.intercept_:.2f}) print(f模型系数: {model.coef_}) print(f均方误差MSE: {mse:.2f}) print(f均方根误差RMSE: {rmse:.2f}) print(f决定系数R2: {r2:.4f})跑完你能得到一组模型参数和评估指标。R2这个数值我们先记住正常数据下模型能取得0.7以上的决定系数说明模型已经捕捉到大部分数据规律了。random_state这里设置成42是机器学习社区里一个很有名的“彩蛋”它的作用是在随机划分数据时保证结果可复现同一份代码在不同机器上跑出来的数据分布是一致的。3.4 特征标准化什么时候必须做我不止一次在评论区看到有人问为什么我用LinearRegression做得好好的换到Lasso或者Ridge之后不收敛呢这就是特征尺度在捣乱。线性回归本身不管特征尺度也能跑因为回归系数会自动适应数值范围做缩放但是当模型里加入正则化项之后惩罚项默认是按“所有特征的系数平方和”来算的如果特征之间尺度差异很大就会导致某些特征的权重被不公平地压缩。解决办法很简单用StandardScaler把特征变成均值为0、标准差为1的分布from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) model_scaled LinearRegression() model_scaled.fit(X_train_scaled, y_train)注意一个细节StandardScaler必须在训练集上先fit然后带着同样的参数去transform测试集。绝对不能对整个数据集一次性做标准化再切分那样会引入数据泄漏data leakage问题导致测试结果虚高无法真实反映模型在实际场景里的表现。4. 完整实操流程与可视化输出看懂模型结果才算掌握4.1 训练集和测试集怎么切分最合理数据划分是整个机器学习流程里的关键一步。我见过很多新手朋友在切分时不设置random_state结果每次跑出来的结果都不同还以为模型出了问题。这里有一个通用原则训练集负责让模型“学习”测试集负责检查模型学得好不好两者绝不能有任何交集。切分比例一般根据数据量来定。506条样本不算多所以用8:2的划分就已经比较合适如果你的数据量在几千到几万条可以考虑用9:1或者更大的训练比例。除了简单随机划分有时候还会用交叉验证。说白了交叉验证就是把数据切成几块轮流拿其中一块当验证集其余用来训练最终把多次结果平均起来训练出的模型更稳健也不容易因为某次切分运气不好而得出偏差结论。4.2 用可视化的方式检查预测效果数字指标让人觉得抽象可视化能让你一眼看出模型在哪些地方表现好、哪些地方偏差大。我习惯画两张图一张是“真实房价 vs 预测房价”的散点图越贴近对角线说明预测越准另一张是残差分布图残差就是真实值减预测值如果残差随机分布在0线附近且没有明显趋势就说明模型没有遗漏什么重要的系统性误差。plt.figure(figsize(14, 5)) plt.subplot(1, 2, 1) plt.scatter(y_test, y_pred, alpha0.6) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], r--, lw2) plt.xlabel(真实房价) plt.ylabel(预测房价) plt.title(真实房价 vs 预测房价) plt.subplot(1, 2, 2) residuals y_test - y_pred plt.scatter(y_pred, residuals, alpha0.6) plt.axhline(y0, colorr, linestyle--, lw2) plt.xlabel(预测房价) plt.ylabel(残差) plt.title(残差分布) plt.tight_layout() plt.show()第一张图里如果散点分布紧贴对角线说明预测效果好。第二张图里我们重点看残差是否呈“漏斗形”扩散如果随着预测值增大残差也在扩散就可能存在异方差性问题也就是在不同房价区间模型的稳定程度不一样。这是线性回归假设检验里值得深挖的点有兴趣可以继续了解但在入门阶段对这个现象有感知就够了。4.3 特征重要性怎么解读线性回归的系数能告诉我们每个特征对房价的影响方向和大体程度。比如RM平均房间数的系数如果是正数说明房间越多、预测房价越高CRIM犯罪率系数如果是负数说明这个指标越高、预测房价越低。这个直觉符合生活常识也是线性回归“可解释性”的优势所在。不过要提醒的是直接比较系数大小判断重要性是不严谨的。因为不同特征量纲不同某个特征系数小不代表它不重要。想要更合理地比较可以先做标准化再训练模型标准化后的系数绝对值大小可以用来近似衡量这个特征的影响程度。即便如此当特征之间存在较强相关性时比如TAX和RAD就跟区域特点有关系数解释依然要谨慎。4.4 模型改进的初步方向线性回归做完不等于完事了对这个项目来说还可以做几件锦上添花的事。一种方式是用交叉验证去评估不同划分下的稳定性另一种是尝试更复杂的回归模型比如在sklearn里直接调用LassoCV或者RidgeCV它们自带正则化可以在一定程度上防止过拟合。如果你想测试自己对流程的掌握程度可以把波士顿房价换成加州房价数据集也可以去网上找新的真实房源数据。真实数据普遍没有这么“干净”你会遇到日期、文本、缺失分类变量等各种问题但这也正是学习中最有价值的部分。每种数据处理方式的背后都是现实问题的映射。5. 常见问题与排查技巧这些坑我帮你提前踩过了5.1 数据加载失败和找不到模块怎么办我写公众号文章时问得最多的就是“为什么代码复制过去还是报错”。就拿加载波士顿数据这段来说如果你参与的教程里还在用load_boston()那先检查scikit-learn版本如果大于1.2那该升级思路了。换成我给的远程加载方式或者直接换个数据源。另一个高频报错是ModuleNotFoundError: No module named sklearn这个简单打开终端敲一句pip install scikit-learn就能解决。还有人在装库时把库名写错成scikit安装之后依然找不到模块这里特意说一下装的时候名字带横杠导入的时候要去掉横杠。5.2 模型效果特别差先查数据问题还是模型问题判断顺序很重要。我自己的排查顺序是先检查有没有明显的脏数据比如空值、异常值、重复行再检查特征标准化有没有写错最后才考虑模型选型有没有问题。70%以上的情况问题都出在前面两环节。如果发现预测结果全是一个常数大概率是标准化时数据泄漏导致的对照前面给的代码自查。另外如果你在代码里分割数据之前没有打乱顺序有时会出现训练集全是老房子、测试集全是新房子的极端情况。这种情况下模型在新数据上的表现自然很差但这不代表模型本身有问题。所以train_test_split默认会先打乱数据再划分如果你有特殊需求想保留顺序设置shuffleFalse即可。5.3 评估指标怎么看才客观初学者在看MSE、RMSE、R2时很容易只看一个数值。MSE单位是平方不太好直观理解RMSE因为开了根号量纲和房价一致能粗略告诉你“模型平均偏差多少钱”。R2的范围理论上从0到1越接近1表示模型解释力度越强。有一个补充分享R2还可以出现负数这代表你的模型比“直接猜平均值”还要差。还有一个容易忽略的点用测试集的评估结果来反复调整模型结构本质上也算某种意义上的“信息泄漏”。每一次看到测试分数你都在无意中做决策最终模型分数可能虚高部署到新数据上会打回原形。所以如果你要做严谨的项目通常还需要独立划分一个验证集或者用交叉验证来替代单次测试集评估。入门时不必太纠结但脑海里要有一个概念。5.4 数据量更大时内存够不够用用线性回归跑506条样本数据感受不到性能压力。但如果换成几百万条样本就要考虑效率问题了。scikit-learn的线性回归在数据量特别大时底层计算用的还是常规矩阵运算方法内存占用会明显上升。这时可以考虑改用SGDRegressor也就是随机梯度下降回归它在每次更新参数时不需要把全部数据载入内存而是每次随机取一批样本计算梯度效率会高很多。from sklearn.linear_model import SGDRegressor model_sgd SGDRegressor(max_iter1000, tol1e-3) model_sgd.fit(X_train_scaled, y_train) y_pred_sgd model_sgd.predict(X_test_scaled)这种实现方式的代价是训练过程变得更依赖学习率和迭代次数的调试不像LinearRegression那样可以一步到位。实际项目里如果数据量在几十万以内且字段数量不多直接用LinearRegression通常没问题直观也稳定。5.5 多尝试几次经验都是练出来的把这里的代码完整跑完一遍之后你还可以多做几个小实验换一个random_state观察评估指标变化只选一两个最相关的特征训练模型体会不同维度的预测效果或者故意把缺失值填成完全离谱的值看模型错误率如何变化。这些看似无关紧要的小操作反而能帮你建立起对模型行为的最直接感知。机器学习的检测标准就是“在没见过的数据上表现如何”你折腾得越多对这个标准的理解就越深。最后留一个小建议不管你现在学Python是为了数据分析还是以后转算法岗纸上得来终觉浅是绝对的真理。线性回归从公式到代码只有一层窗户纸你亲手把它捅破了后面的机器学习之路会顺畅很多。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。