Python房价预测系统实战:从爬虫、MySQL到XGBoost模型全链路

发布时间:2026/9/30 0:50:40
Python房价预测系统实战:从爬虫、MySQL到XGBoost模型全链路 简介这份资源是一篇完整的计算机毕业设计论文文档主题为基于Python的房价预测系统面向计算机相关专业学生及需要完成毕业设计的人群帮助解决选题、系统设计与论文撰写等实际问题。压缩包内共1个doc文件大小约2.55MB内容涵盖摘要、绪论、系统设计与实现、测试等完整论文结构。论文围绕网络爬虫技术展开以58同城网房屋数据为研究对象详细阐述了数据爬取、信息提取、MySQL数据库存储与管理、数据清洗与预处理等环节并运用线性回归、决策树、随机森林、神经网络等机器学习算法建立房价预测模型同时涉及前端查询界面、可视化大屏展示及后台管理系统的设计思路。目前已有373人学习下载适合作为毕业设计选题参考、论文写作模板或房价预测类项目的技术方案借鉴读者可从中获取完整的系统架构、算法选型依据与测试验证流程为自身课题开发与论文撰写提供实用参考。1. 从一份「房价预测系统论文.doc」说起这套东西到底在做什么很多人第一次看到「基于Python的房价预测系统论文.doc」这个标题脑子里冒出来的画面是一份几十页的 Word 文档里面塞满公式、截图和参考文献最后附一段跑得起来的代码。但真正做过这类课题的人知道论文只是外壳内核是一套完整的数据链路——从链家、安居客这类二手房平台把房源数据抓下来清洗成规整的表格存进 MySQL再用 Python 训练一个回归模型最后把预测结果可视化出来。这条链路里爬虫、数据库、特征工程、模型调参、可视化每一环都能单独写一篇东西。我见过太多人卡在第一步数据从哪来。公开的波士顿房价数据集只有 506 条、13 个特征跑出来的 R² 好看得离谱但拿去预测真实小区房价误差能到几十万。所以这套系统的价值不在于模型多花哨而在于它逼着你走完「真实数据采集 → 存储 → 建模 → 落地」的全流程。适合谁适合正在做课程设计、毕业设计或者想从纯调包转向完整项目的新手也适合已经会写爬虫、但没把数据真正用起来的熟手。下面我按自己实际搭过一遍的顺序把每个环节拆开讲。2. 数据从哪来爬虫抓房源与 MySQL 落库的完整链路2.1 为什么选 requests BeautifulSoup 而不是 Selenium房源平台大致分两类一类是服务端渲染HTML 里直接带数据另一类是前端 JS 渲染得等浏览器执行完才出内容。链家、安居客的列表页和详情页大部分关键字段总价、单价、面积、户型、楼层、小区名在服务端返回的 HTML 里就能拿到用 requests 直接请求配 BeautifulSoup 解析速度比 Selenium 快一个数量级资源占用也低得多。Selenium 不是不能用而是没必要。它的优势在于处理登录、验证码、复杂交互但代价是每个页面要等浏览器渲染抓几千条数据时慢得让人想砸键盘。我的做法是先用 requests 试探如果返回的 HTML 里能正则匹配到目标字段就继续用如果发现关键数据是空的再考虑上 Selenium 或者直接找接口。提示抓之前先看目标站点的 robots.txt控制请求频率单线程加 12 秒随机延时别把人家服务器打挂。这不是技术问题是基本规矩。2.2 一个能跑的最小爬虫脚本下面这段代码抓的是列表页里的房源摘要信息字段包括标题、总价、单价、面积、小区、区域。实际项目里我会把详情页也抓一遍补上朝向、楼层、装修、房龄这些字段。import requests from bs4 import BeautifulSoup import time import random import pymysql # 数据库连接配置后续所有写入复用这个连接 conn pymysql.connect( hostlocalhost, userroot, passwordyour_password, databasehouse_price, charsetutf8mb4 ) cursor conn.cursor() # 建表语句字段类型按实际数据调整 create_table_sql CREATE TABLE IF NOT EXISTS house_info ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200), total_price DECIMAL(10,2), unit_price DECIMAL(10,2), area DECIMAL(8,2), community VARCHAR(100), district VARCHAR(50), crawl_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; cursor.execute(create_table_sql) conn.commit() headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def parse_list_page(url): 解析列表页返回房源字典列表 resp requests.get(url, headersheaders, timeout10) if resp.status_code ! 200: print(f请求失败状态码{resp.status_code}) return [] soup BeautifulSoup(resp.text, html.parser) items [] # 这里的 class 名要按目标站点实际结构调整 for li in soup.select(.sellListContent li): try: title li.select_one(.title a).get_text(stripTrue) total_price li.select_one(.totalPrice span).get_text(stripTrue) unit_price li.select_one(.unitPrice span).get_text(stripTrue) area li.select_one(.houseInfo).get_text(stripTrue).split(|)[1].strip() community li.select_one(.positionInfo a).get_text(stripTrue) district li.select_one(.positionInfo).get_text(stripTrue).split(/)[-1].strip() items.append({ title: title, total_price: float(total_price.replace(万, )), unit_price: float(unit_price.replace(元/平米, ).replace(,, )), area: float(area.replace(平米, )), community: community, district: district }) except Exception as e: # 单条解析失败不影响整体记录后跳过 print(f解析单条失败{e}) continue return items def save_to_mysql(items): 批量写入 MySQL用 executemany 减少连接开销 sql INSERT INTO house_info (title, total_price, unit_price, area, community, district) VALUES (%s, %s, %s, %s, %s, %s) data [(i[title], i[total_price], i[unit_price], i[area], i[community], i[district]) for i in items] cursor.executemany(sql, data) conn.commit() print(f成功写入 {len(data)} 条) if __name__ __main__: base_url https://example.com/sale/pg{}/ for page in range(1, 51): # 抓前 50 页 url base_url.format(page) items parse_list_page(url) if items: save_to_mysql(items) time.sleep(random.uniform(1.5, 3.0)) # 随机延时降低被封风险 cursor.close() conn.close()这段代码的逻辑很直白请求列表页 → BeautifulSoup 解析 → 提取字段 → 批量写库。几个关键参数说明一下。timeout10是防止某个请求卡死拖垮整个脚本random.uniform(1.5, 3.0)是随机延时比固定 sleep 更难被识别executemany比逐条 insert 快很多抓几千条时差距明显。字段里的total_price和unit_price做了字符串清洗去掉「万」「元/平米」这些单位转成 float 再入库不然后面建模还得再处理一遍。2.3 MySQL 建库建表与连接池的取舍建库很简单一条命令的事CREATE DATABASE house_price DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;但连接管理有讲究。上面脚本里是单连接跑到底抓几十页没问题。如果要上分布式爬虫或者爬虫和训练脚本同时读写就得考虑连接池。Python 里常用DBUtils的PooledDB或者直接用 SQLAlchemy 的create_engine配pool_size。我一般在小项目里不折腾连接池单连接加conn.ping(reconnectTrue)就够了数据量上到十万条以上再换连接池。注意MySQL 8.0 默认认证插件是caching_sha2_password老版本的 pymysql 可能报错。解决办法是升级 pymysql或者建用户时指定mysql_native_password。这个坑我踩过报错信息是Authentication plugin caching_sha2_password cannot be loaded查了半天才发现是版本问题。字段设计上unit_price和total_price用 DECIMAL 而不是 FLOAT避免浮点精度问题crawl_time默认 CURRENT_TIMESTAMP方便后续按时间筛选。如果要做去重给title加唯一索引插入时用INSERT IGNORE或ON DUPLICATE KEY UPDATE。3. 从原始表到模型输入特征工程里最容易被忽略的几步3.1 缺失值、异常值和类别编码的处理顺序数据从 MySQL 读出来之后第一件事不是急着喂给模型而是看数据长什么样。我一般用 pandas 做一轮 EDAimport pandas as pd import numpy as np from sqlalchemy import create_engine engine create_engine(mysqlpymysql://root:passwordlocalhost:3306/house_price?charsetutf8mb4) df pd.read_sql(SELECT * FROM house_info, engine) # 看缺失情况 print(df.isnull().sum()) # 看数值分布 print(df[[total_price, unit_price, area]].describe()) # 面积小于 10 平米或大于 1000 平米的基本是录入错误或特殊房源直接剔除 df df[(df[area] 10) (df[area] 1000)] # 单价用 1% 和 99% 分位数做截断避免极端值拉偏模型 q_low df[unit_price].quantile(0.01) q_high df[unit_price].quantile(0.99) df df[(df[unit_price] q_low) (df[unit_price] q_high)] # 类别特征做独热编码district 这种取值不多的适合 df pd.get_dummies(df, columns[district], drop_firstTrue)处理顺序很重要先剔除明显异常再做分位数截断最后编码。如果先编码再截断独热出来的列会被误伤。缺失值方面数值型用中位数填充类别型用众数填充别用均值——房价分布是右偏的均值会被高價房源拉高。3.2 构造比原始字段更有信息量的特征原始字段里area和unit_price是最强的两个预测因子但光靠它们不够。我一般会加几个衍生特征特征名计算方式作用房龄当前年份 - 建成年份老房子通常单价低但学区房例外户型房间数从 title 正则提取「X室X厅」房间数影响总价楼层类别低/中/高 三档中间楼层通常最贵小区均价按 community 分组求 unit_price 均值捕捉小区整体水平区域均价按 district 分组求 unit_price 均值捕捉地段价值小区均价和区域均价这两个特征特别有用因为它们把「地段」这个难以直接量化的因素变成了数值。计算时要注意用训练集的统计量别把测试集的信息泄漏进去。我一般先划分训练集和测试集再在训练集上算分组均值然后 map 到测试集缺失的用全局均值兜底。from sklearn.model_selection import train_test_split train_df, test_df train_test_split(df, test_size0.2, random_state42) # 在训练集上计算小区均价 community_mean train_df.groupby(community)[unit_price].mean() train_df[community_avg_price] train_df[community].map(community_mean) test_df[community_avg_price] test_df[community].map(community_mean) # 测试集里出现训练集没有的小区用全局均值填充 test_df[community_avg_price].fillna(train_df[unit_price].mean(), inplaceTrue)这段代码的核心是「统计量只在训练集上算」。很多人图省事在全量数据上算均值再划分结果模型在测试集上表现虚高上线就翻车。这是血泪经验别问我怎么知道的。4. 模型选型与训练线性回归、随机森林和 XGBoost 到底选哪个4.1 三个基线模型的对比与参数设置房价预测本质是回归问题常见做法是先用线性回归跑一个基线再上树模型。线性回归的好处是可解释性强每个特征的系数能直接看出影响方向和大小坏处是它假设特征和 target 是线性关系而房价和面积、地段的关系往往是非线性的。随机森林和 XGBoost 能自动捕捉非线性关系通常效果更好但调参更麻烦。我的习惯是三个都跑一遍用交叉验证比 RMSE 和 R²选表现最好的那个做后续优化。from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from xgboost import XGBRegressor from sklearn.model_selection import cross_val_score from sklearn.metrics import mean_squared_error, r2_score import numpy as np # 准备特征矩阵和标签 feature_cols [c for c in train_df.columns if c not in [id, title, total_price, unit_price, community, crawl_time]] X_train train_df[feature_cols].select_dtypes(include[np.number]) y_train train_df[unit_price] X_test test_df[feature_cols].select_dtypes(include[np.number]) y_test test_df[unit_price] models { LinearRegression: LinearRegression(), RandomForest: RandomForestRegressor(n_estimators200, max_depth12, min_samples_leaf3, random_state42), XGBoost: XGBRegressor(n_estimators300, learning_rate0.05, max_depth6, subsample0.8, random_state42) } for name, model in models.items(): # 5 折交叉验证scoring 用负 RMSE scores cross_val_score(model, X_train, y_train, cv5, scoringneg_root_mean_squared_error) model.fit(X_train, y_train) y_pred model.predict(X_test) rmse np.sqrt(mean_squared_error(y_test, y_pred)) r2 r2_score(y_test, y_pred) print(f{name} | CV RMSE: {-scores.mean():.2f} | Test RMSE: {rmse:.2f} | R2: {r2:.4f})参数说明随机森林的n_estimators200是树的数量太少欠拟合太多训练慢且收益递减max_depth12控制树深防止过拟合min_samples_leaf3保证叶子节点至少有 3 个样本。XGBoost 的learning_rate0.05配合n_estimators300是常见的慢学习率多轮迭代组合subsample0.8做行采样增加泛化。4.2 用网格搜索把 XGBoost 再压一压如果 XGBoost 表现最好可以进一步调参。我一般用GridSearchCV在小范围里搜别一上来就大网格跑一天都跑不完。from sklearn.model_selection import GridSearchCV param_grid { max_depth: [4, 6, 8], learning_rate: [0.03, 0.05, 0.1], n_estimators: [200, 300, 500], subsample: [0.7, 0.8, 1.0] } xgb XGBRegressor(random_state42) grid GridSearchCV(xgb, param_grid, cv3, scoringneg_root_mean_squared_error, n_jobs-1, verbose1) grid.fit(X_train, y_train) print(f最佳参数{grid.best_params_}) print(f最佳 CV RMSE{-grid.best_score_:.2f})n_jobs-1用满所有 CPU 核心cv3比 5 折快适合调参阶段。搜完之后用最佳参数重新训练在测试集上验证。注意别用测试集调参否则测试集就失去了「未见数据」的意义。提示如果 RMSE 降不下去先别怀疑模型回头检查特征。我遇到过特征里混进了total_price而total_price unit_price × area等于把答案泄漏给了模型R² 直接 0.99纯属自欺欺人。5. 避坑与排查这套系统里最容易翻车的五个地方5.1 爬虫被反爬返回空列表或验证码页现象脚本跑了几页之后parse_list_page返回空列表或者 resp.text 里出现「访问过于频繁」「请验证」之类的字样。原因请求频率太高、User-Agent 单一、IP 被标记。有些站点还会检测 Cookie 和 Referer。解决加随机延时1.53 秒、轮换 User-Agent、带上 Referer 头。如果还不行降低并发或者换时间段抓。别硬刚硬刚的结果是 IP 被封得不偿失。5.2 MySQL 写入中文乱码现象数据库里的小区名、区域名显示成问号或乱码。原因建库或建表时字符集不是 utf8mb4或者连接时没指定 charset。解决建库用DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci连接时加charsetutf8mb4。如果表已经建了用ALTER TABLE house_info CONVERT TO CHARACTER SET utf8mb4;改过来。5.3 训练集和测试集分布不一致导致 R² 虚高现象交叉验证 R² 0.85测试集 R² 0.6差距很大。原因划分数据时没打乱或者用了时间顺序划分但数据本身有时间趋势。房价数据如果按抓取时间排序早期和晚期的房源分布可能不同。解决用train_test_split时加shuffleTrue和random_state保证随机性。如果数据有时间维度考虑按时间切分但要在特征里加入时间相关变量。5.4 独热编码后特征维度爆炸现象district有 200 个取值独热之后多了 199 列训练慢且过拟合。原因类别型特征取值太多独热编码不适合高基数类别。解决改用目标编码Target Encoding或者频率编码。目标编码是用类别对应的 target 均值替换类别值但要注意在训练集上算、测试集上 map防止泄漏。也可以把低频类别合并成「其他」。5.5 模型上线后预测结果离谱现象线下 R² 0.8线上预测出来的房价比实际低一半。原因线上输入的特征分布和训练集不一致比如面积单位从「平米」变成了「平方米」或者小区名在新数据里没出现过community_avg_price全是 NaN。解决在预测脚本里加输入校验检查字段类型、取值范围、类别是否在训练集出现过。对未见过的类别用全局均值兜底。上线前用一批真实数据做回归测试别只看线下指标。6. 把预测结果用起来可视化与批量预测的两个实用技巧模型训练完不是终点得让人能看懂、能用上。我一般做两件事一是用 pyecharts 或 matplotlib 画特征重要性和预测值 vs 真实值的散点图二是写一个批量预测脚本输入一个 CSV 就能输出带预测单价的表格。特征重要性用 XGBoost 自带的feature_importances_就能拿到画成横向条形图一眼看出哪个特征贡献最大。我跑过的项目里area、community_avg_price、district_avg_price通常排前三这也符合直觉——面积和地段是房价的两大支柱。批量预测脚本的关键是把训练时的预处理逻辑封装成函数预测时复用同一套逻辑。下面是一个简化版import joblib import pandas as pd # 加载训练好的模型和特征列 model joblib.load(xgb_house_model.pkl) feature_cols joblib.load(feature_cols.pkl) def predict_batch(input_csv, output_csv): df pd.read_csv(input_csv) # 复用训练时的预处理异常值处理、编码、衍生特征 df df[(df[area] 10) (df[area] 1000)] df pd.get_dummies(df, columns[district], drop_firstTrue) # 对齐特征列缺失的补 0 for col in feature_cols: if col not in df.columns: df[col] 0 df df[feature_cols] df[predicted_unit_price] model.predict(df) df.to_csv(output_csv, indexFalse, encodingutf-8-sig) print(f预测完成结果已保存到 {output_csv}) predict_batch(new_houses.csv, predicted_result.csv)这里有个细节get_dummies在预测时可能生成和训练时不同的列所以要用feature_cols对齐缺失的列补 0。encodingutf-8-sig是为了 Excel 打开 CSV 不乱码这个坑我踩过好几次。最后一个习惯每次训练完模型把模型文件、特征列、预处理参数一起打包保存别只存模型。不然过两个月想复现发现预处理逻辑找不到了只能重跑一遍。这个习惯帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取