
简介面向计算机相关专业学生的 Python 机器学习课程设计与期末大作业该实战项目以北京二手房房价预测为场景完整覆盖爬虫采集链家/安居客数据、数据清洗、特征工程、模型训练与评估全流程经导师指导获得98分高分可作为课程设计或期末答辩的可靠参考。压缩包大小1.28MB共26个文件包括15个Python脚本负责爬虫、建模和可视化、1个Jupyter Notebook及对应HTML报告、2个CSV数据集、README与图片等结构清晰便于对照学习。已有103人学习下载适合需要高完成度参考项目、希望快速理解完整机器学习项目流程的初学者。资源附有详细使用说明与代码注释spiders文件夹内含爬虫源代码可直接运行并替换自有数据可视化脚本与HTML报告也有助于成果展示便于读者在课程设计、结课作业或面试作品中体现扎实的实战能力。1. 用Python机器学习实战做房价预测最怕的不是模型不收敛用Python机器学习实战做房价预测最怕的不是模型不收敛而是辛辛苦苦写了几百行代码最后不知道分数丢在哪。这个经典回归任务看着简单但每个环节都能拉开差距。它不像图像识别需要GPU也不像NLP需要巨大的预处理流程一份结构化表格、几行pandas、一个线性回归就能跑通真正做到基于Python机器学习实战把流程走完。正因如此很多学校的机器学习期末项目都会拿它当默认选题而翻车点通常集中在特征泄漏、指标误读和参数过拟合上。我会按自己的项目习惯从数据准备、模型选型、交叉验证到调参集成把每一步拆开讲清楚。适合刚入门Python、以及想在课程作业里冲高分的读者。2. 数据准备与特征工程房价预测项目的地基2.1 读取CSV后先做三件小事不管数据来自Kaggle的House Prices竞赛还是老师发的Excel导出CSV第一步都是把它塞进pandas。常见做法是直接pd.read_csv(house_prices.csv)然后立刻看三样东西样本量、列类型、缺失值分布。下面这段代码我在每次项目里都会先跑一遍import pandas as pd import numpy as np df pd.read_csv(house_prices.csv) print(样本与特征数量, df.shape) print(数据类型分布) print(df.dtypes.value_counts()) print(缺失值最高的10列) print(df.isnull().sum().sort_values(ascendingFalse).head(10))df.shape会返回(行数, 列数)如果行数只有几百后面交叉验证时K就要设小一点否则每折样本太少。df.dtypes.value_counts()能快速区分数值列和object列object列通常要进入独热编码。最关键的缺失值统计直接决定了数据清洗策略缺失比例超过40%的列我一般直接drop因为填充完也是噪音10%到40%的列再考虑用中位数或众数。注意这里不要一开始就看describe()因为分类列的统计值没有意义先摸清缺失和类型再决定。这一步做得越扎实后面越不会出现测试集和训练集列数量对不上的尴尬。2.2 缺失值填充与目标变量偏度处理缺失值处理有两种常见流派删除和填充。对于房价预测这种结构化表格删除行会丢掉价格信息删除列又会损失特征所以我更倾向于填充。数值列用中位数而不是均值是因为房价相关特征往往右偏比如LotArea偶尔出现一个超大的农场地块均值会被带偏中位数更稳健。分类列用众数填充同时用一个新列记录“这个值原来是缺失的”让模型自己学缺失状态。下面的代码直接展示填充逻辑from sklearn.impute import SimpleImputer num_cols df.select_dtypes(includenp.number).columns cat_cols df.select_dtypes(includeobject).columns df[num_cols] SimpleImputer(strategymedian).fit_transform(df[num_cols]) df[cat_cols] SimpleImputer(strategymost_frequent).fit_transform(df[cat_cols])SimpleImputer的fit_transform会先拟合每列的统计量再替换缺失值。这里有一个容易踩的坑如果只对训练集调用fit_transform测试集必须用同一个imputer对象去transform绝对不能再fit一次否则验证集的信息会泄漏到训练过程。分类列填充后后面还要过独热编码但最近流行的target encoding其实效果更好不过容易过拟合课程项目里为了求稳我建议还是用OneHotEncoder。目标变量SalePrice本身也有偏度问题。一个不平衡的目标会让MSE被少数高价房主导模型会拼命去拟合那些几百万的豪宅忽略大部分普通房子。常用的解决方法是取对数from scipy.stats import skew y df[SalePrice] y_log np.log1p(y)np.log1p是log(1x)避免x0时为负无穷。所有预测完成后记得用np.expm1把预测值还原成真实房价否则提交的不是价格而是log价格。很多人在这个地方栽跟头训练时用了log提交时忘了还原分数直接崩掉。还有一个细节如果数据里SalePrice本身没有极端值可以不做log变换但大多数数据集不做会吃亏建议先用abs(skew(y)) 0.75判断一下。2.3 特征工程把领域常识写成新特征数值清洗做完就该动脑子设计特征了。房价特征里有一类明显的冗余总面积地下室面积一层面积二楼面积车库面积如果不合并线性回归会把四个系数分别估计而它们实际上对价格的贡献类似且相互纠缠。常见做法是显式创建聚合特征df[TotalSF] df[TotalBsmtSF] df[1stFlrSF] df[2ndFlrSF] df[GarageArea] df[HouseAge] df[YrSold] - df[YearBuilt] df[RemodAge] df[YrSold] - df[YearRemodAdd]TotalSF把四个面积加到一个字段让模型只用学习一个权重降低方差。HouseAge和RemodAge表达“房子越老越便宜但翻新过会冲抵一部分折旧”。这两个特征在随机森林里重要度通常排前五。另外邮政编码或MSZoning这类高基数分类特征OneHot后会产生几十个列这时候一定要先做分组把样本量少于20的类别归并成Other否则稀疏二值列会让树模型无谓分裂。特征工程做得好不好最后在验证集上的RMSE会差出几千美元这就是“高分保障”的实际来源。3. 线性回归与梯度下降先把模型跑通再谈高分3.1 为什么从线性回归而不是随机森林开始很多同学一上来就调XGBoost结果解释不清楚模型为什么输出这个价期末答辩被问到核心原理就卡住。我自己的习惯是先用线性回归做一个baseline因为它的损失函数和梯度下降是机器学习最底层的语言。线性回归假设目标值y和特征x满足y w·x b训练过程就是找到一组w和b让预测值与真实值的均方误差MSE最小。MSE的梯度有一个很漂亮的形式对每个权重w_j梯度等于预测误差乘以对应特征值的平均。这就是常说的机器学习中的梯度它是后续所有神经网络反向传播的雏形。先懂这个再去解释树模型才有依据否则即使随机森林分数高也说不清为什么高。3.2 用sklearn搭一个基线回归模型在Python环境配置好之后推荐conda创建独立环境pip装scikit-learn即可下面的代码就是最小可运行版本from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error X df.select_dtypes(includenp.number).drop(columns[SalePrice]) X_train, X_test, y_train, y_test train_test_split(X, y_log, test_size0.2, random_state42) model LinearRegression() model.fit(X_train, y_train) y_pred model.predict(X_test) rmse mean_squared_error(y_test, y_pred, squaredFalse) print(线性回归RMSE, rmse)train_test_split把数据按8:2切分random_state固定成42保证每次运行的结果一致期末提交时老师能复现你的数字。fit(X_train, y_train)是在训练集上学权重predict(X_test)是在没见过的数据上生成预测。mean_squared_error的squaredFalse参数直接返回RMSE不需要再开根号。如果sklearn版本较新可以直接用root_mean_squared_error。注意这里X里包含了前面做好的TotalSF和HouseAge如果没做特征工程RMSE通常会高3%到5%。3.3 正则化解决共线性岭回归和Lasso线性回归有个前提是特征之间不要高度相关否则w的估计方差会爆炸模型训练集得分很高测试集一塌糊涂。我们构造的TotalSF其实和原始面积列就是共线关系所以下一步换上岭回归。岭回归在MSE损失后面加一项L2范数惩罚L MSE α·Σw²这个α控制惩罚力度。Lasso则用L1范数会把部分权重压缩到0天然做特征选择。from sklearn.linear_model import Ridge, Lasso ridge Ridge(alpha10.0) ridge.fit(X_train, y_train) print(Ridge RMSE, mean_squared_error(y_test, ridge.predict(X_test), squaredFalse)) lasso Lasso(alpha0.001, max_iter10000) lasso.fit(X_train, y_train) print(Lasso RMSE, mean_squared_error(y_test, lasso.predict(X_test), squaredFalse))alpha越大惩罚越强权重越接近0太小则退化回普通线性回归。我一般用10的倍数做粗调比如[0.1, 1, 10, 100]再在最优值附近细调。Lasso因为使用坐标下降迭代对特征量级敏感如果各列尺度差太大建议先用StandardScaler标准化后再拟合否则可能出现系数全为0的情况。这两个模型跑完你就能判断线性假设是否够用如果Ridge和LinearRegression的RMSE差距不大说明数据的线性部分已经吃干榨净下一步就要上非线性模型了。4. 交叉验证与评估指标别让R²骗了你4.1 为什么单次切分不够可靠test_size0.2意味着只评价了一次模型在某个固定20%数据上的表现如果这20%里恰好包含几套学区房RMSE会很难看反过来如果全是普通户型分数又会虚高。更可信的做法是K折交叉验证把训练集分成K份轮流拿其中1份做验证其余K-1份训练最终取K次RMSE的平均值和标准差。课程项目里K5是默认样本量小于500时可以设3。交叉验证的另一个好处是能暴露数据泄漏如果预处理里不小心用到了全量数据的统计量每一折的验证误差会异常低低到不合常理。4.2 用KFold计算稳定的RMSE这里我直接用随机森林举例因为它是后面要用的主模型交叉验证同时帮我们判断随机森林的稳定性from sklearn.model_selection import KFold, cross_val_score from sklearn.ensemble import RandomForestRegressor kfold KFold(n_splits5, shuffleTrue, random_state42) rf RandomForestRegressor(n_estimators200, random_state42) scores cross_val_score(rf, X_train, y_train, cvkfold, scoringneg_mean_squared_error) rmse_scores np.sqrt(-scores) print(每折RMSE, np.round(rmse_scores, 4)) print(平均RMSE%.4f 标准差%.4f % (rmse_scores.mean(), rmse_scores.std()))cross_val_score返回的是负均方误差因为sklearn的scoring约定是“越大越好”所以要加负号再开根号得到RMSE。每折RMSE都打印出来看看如果某一折特别高说明那折里包含了极端房价模型没见过如果标准差超过平均值的10%说明数据分布不稳定需要重新检查切分前是否做了shuffle。KFold的shuffleTrue很重要否则直接按顺序切分会把同一年份或同一个社区的数据聚在一折里评估结果没有代表性。另外这个过程已经自动包含训练和预测不需要手动写循环。除了RMSE课程报告里还经常出现MAE和R²。三者的侧重不同整理成下面这张表选哪个指标取决于你要向老师传达什么信息指标关注点使用场景RMSE大误差会被平方放大模型选型和调参时差距明显MAE直接反映平均偏离多少美元汇报给业务方时更直观R²预测值相对均值的解释力课程报告展示模型整体表现4.3 残差图比R²更诚实的检查方式R²在课程报告里很好看但它有两个盲区一它衡量的是预测值跟均值比好多少不代表误差绝对值小二当模型系统性高估低价房、低估高价房时R²可能仍然很高。所以我每次都会画残差图也就是真实值减去预测值再对预测值画散点。残差如果在0附近随机分布说明模型没有漏掉明显的模式如果出现喇叭形说明误差随价格变大而变大这时候就该考虑对目标做log变换或者改用分位数回归。import matplotlib.pyplot as plt residuals y_test - ridge.predict(X_test) plt.scatter(ridge.predict(X_test), residuals, alpha0.5) plt.axhline(y0, colorred, linestyle--) plt.xlabel(Predicted Price (log)) plt.ylabel(Residuals (log)) plt.show()画图时横纵轴都用log价格而不是还原后的美元否则价格跨度太大多数点会挤在左下角。看到喇叭形残差后不要急着上复杂模型先回看是不是漏了交互特征或存在异方差。课程项目里一张规范的残差图往往比一段长篇大论更能说服老师给高分它证明你真正检查了模型的失效模式。5. 调参与集成学习把成绩从及格拉到优秀5.1 随机森林与网格搜索的正确搭配到了这一步baseline已经稳定可以上非线性模型了。随机森林是房价预测里性价比最高的模型不需要标准化特征、几乎不调参就能work、还能输出特征重要性。我一般先把n_estimators固定到200用GridSearchCV去搜树的深度和分裂条件from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200], max_depth: [5, 10, None], min_samples_split: [2, 5, 10] } grid GridSearchCV( RandomForestRegressor(random_state42), param_grid, cv5, scoringneg_mean_squared_error, n_jobs-1 ) grid.fit(X_train, y_train) print(最佳参数, grid.best_params_)网格搜索会把所有参数组合2×3×318种各做5折交叉验证一共90次训练。n_jobs-1让所有CPU核心并行不然会等到怀疑人生。scoring继续用neg_mean_squared_error让GridSearchCV内部选RMSE最小的组合。max_depthNone意味着树不限制深度容易过拟合但如果min_samples_split同步设大一点比如5或10效果往往比限深更好。网格搜索跑完后用grid.best_estimator_替换原来的rf再在测试集上算一次RMSE就能看到比线性模型明显的下降。5.2 XGBoost与LightGBM怎么选如果你的数据特征都是数值型类别已经独热编码XGBoost能带来最后一波提升。但要注意课程项目时间有限随机森林跑通已经能拿70到80分XGBoost是为了冲刺85以上。使用时要格外小心过早停止和过拟合from xgboost import XGBRegressor xgb XGBRegressor( n_estimators500, learning_rate0.05, max_depth4, subsample0.8, colsample_bytree0.8, random_state42 ) xgb.fit(X_train, y_train, eval_set[(X_test, y_test)], early_stopping_rounds20, verboseFalse)n_estimators设大一些learning_rate调小到0.05靠early_stopping_rounds在测试集上连续20轮没变好时自动停止这是防止XGBoost过拟合最有效的习惯。subsample0.8表示每棵树只随机使用80%的行colsample_bytree0.8表示每棵树只用80%的特征两者都引入随机性提高泛化。eval_set传的是测试集但注意这里只是用来做早停的参考不能反复用同一组测试集去选模型否则测试集就变成了训练集的一部分。如果数据量很大LightGBM常常比XGBoost更快但在几千行的房价数据上差距不明显选一个顺手的就好。5.3 特征重要性拿高分答辩时的硬通货集成模型另一个好处是自带feature importance。这里先澄清一个常见误解随机森林这类树模型并不假设特征独立同分布也不要求特征相互独立对共线性不敏感所以在树模型里不需要特意为共线性删特征。期末答辩老师最爱问“你的模型为什么这么选”给出排序图就能把这个问题变成加分项。import pandas as pd import matplotlib.pyplot as plt importance pd.Series(grid.best_estimator_.feature_importances_, indexX_train.columns) importance.sort_values(ascendingFalse).head(10).plot.barh() plt.tight_layout() plt.show()feature_importances_是每棵树在分裂时用该特征带来的不纯度下降加总再在所有树上取平均并归一化。注意它容易高估数值型连续特征的重要性因为树可以在切分点上玩出更多花样。所以报告里要强调这个重要性只能说明特征在拟合时的贡献不能直接解读成因果影响。想严格解释因果可以去学SHAP值但对于课程项目的篇幅重要性排序加上你对TotalSF、HouseAge之类的领域解释已经足够。反过来如果某个原本认为重要的特征排名很低比如GarageArea不在前十就要回溯特征工程确认是否因为缺失值填充把它的信号洗干净了。6. 最后一步用学习曲线验证稳定性和写清决策依据模型调完并不是终点真正拉开高分差距的是你是否验证过“加数据会变好、还是已经收敛”。学习曲线是这个阶段最有用的工具它画出训练集大小从50到全部时训练误差和验证误差的变化。如果两线靠得很近且都在高位是欠拟合要加特征或换更强模型如果训练误差低、验证误差高且随样本增加而缓慢靠近是过拟合增加样本或对模型加正则化。from sklearn.model_selection import learning_curve train_sizes, train_scores, val_scores learning_curve( grid.best_estimator_, X_train, y_train, cv5, train_sizesnp.linspace(0.1, 1.0, 5), scoringneg_mean_squared_error ) train_rmse np.sqrt(-train_scores.mean(axis1)) val_rmse np.sqrt(-val_scores.mean(axis1)) for size, tr, va in zip(train_sizes, train_rmse, val_rmse): print(f{size:5d} 训练RMSE {tr:.4f} 验证RMSE {va:.4f})看到验证误差逐渐逼近训练误差说明模型容量和当前数据量基本匹配如果最后两点验证误差还在明显下降说明加大训练数据对成绩还有帮助可以考虑提交前用完整数据重训一版。这里需要说明最终提交模型时要用全部训练数据重新fit因为交叉验证只是为了选参数选完后没有理由放弃那5%的样本。先用grid.best_estimator_.fit(X, y_log)在完整数据上训练再对测试集做预测和np.expm1还原就能生成可以提交的CSV了。验证完之后写报告时建议固定住三个数线性回归RMSE、随机森林网格搜索RMSE、XGBoost早停RMSE把这几个数字放在同一张表里并在旁边写清每个模型的训练时间。老师看到你记录了模型复杂度与性能的权衡比只贴一个最高分更有说服力。另外把残差图和学习曲线放在附录报告正文里只写结论哪些特征最重要、哪个模型最优、误差分布是否符合假设。这样一来整个项目从数据清洗、特征工程、模型选型到调参验证都有明确决策依据老师能顺着你的步骤复现出同样的结果分数自然不会低。本文还有配套的精品资源点击获取