
简介这份资源是面向计算机相关专业学生与项目实战学习者的二手房房价数据分析与预测完整项目源码源自经导师指导并认可的98分毕业设计可直接用于毕设、课程设计或期末大作业。项目围绕链家二手房数据展开涵盖数据爬取、清洗、特征分析与房价预测建模等环节帮助读者掌握从原始数据到模型输出的完整分析流程。压缩包共17个文件约6.23MB以12个csv数据文件、3个ipynb交互式笔记本、1个py脚本和1份docx说明文档为主分别承担数据存储、分步分析与代码实现等用途目录结构清晰便于按模块查阅。目前已有336人学习下载。项目经过严格调试确保可运行读者可据此快速复现分析过程、理解建模思路并在此基础上调整特征或算法完成自己的课题。1. 从一份 98 分毕设拆开看二手房房价分析与预测到底在做什么如果你正在找一份能直接跑通、结构完整、还带爬虫和建模的 Python 数据分析项目那这份「二手房房价数据分析与预测」源码包大概率能省掉你两周的摸索时间。它不是那种只丢一个 notebook、跑完就报错的半成品而是把数据采集、清洗、可视化、特征工程、模型训练和预测输出串成了一条完整链路。核心文件包括spider/链家网数据爬取.ipynb、sol.ipynb、sol.py和data目录覆盖了从原始房源页面到最终预测结果的全过程。适合谁计算机相关专业做毕设的学生、需要课程设计交作业的人以及想拿一个真实场景练手 Python 数据分析和机器学习的入门者。你拿到手后最该关心的不是它有多少行代码而是数据从哪来、字段怎么对齐、模型怎么选、预测结果怎么解释——这四件事决定了你能不能把它变成自己的东西。2. 数据采集与字段对齐链家网爬取脚本怎么改才不翻车2.1 爬虫脚本的结构与请求参数打开spider/链家网数据爬取.ipynb你会看到典型的 requests BeautifulSoup 组合。它按城市、区域、页码构造 URL逐页抓取房源列表再进入详情页提取总价、单价、户型、面积、楼层、朝向、建成年代等字段。常见做法是把城市拼音和区域编码放在一个字典里循环拼接。下面这段是我一般会改成的结构方便你替换目标城市和区域import requests from bs4 import BeautifulSoup import pandas as pd import time import random # 城市与区域编码链家 URL 结构通常为 /ershoufang/{region}/pg{page}/ city bj regions { dongcheng: 东城, xicheng: 西城, chaoyang: 朝阳, haidian: 海淀 } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def parse_list_page(html): soup BeautifulSoup(html, html.parser) items soup.select(.sellListContent li) rows [] for item in items: title item.select_one(.title a) total_price item.select_one(.totalPrice span) unit_price item.select_one(.unitPrice span) house_info item.select_one(.houseInfo) if not all([title, total_price, unit_price, house_info]): continue rows.append({ title: title.get_text(stripTrue), total_price: total_price.get_text(stripTrue), unit_price: unit_price.get_text(stripTrue), house_info: house_info.get_text(stripTrue), region: region_name }) return rows all_data [] for region_code, region_name in regions.items(): for page in range(1, 51): # 每个区域抓 50 页按需调整 url fhttps://{city}.lianjia.com/ershoufang/{region_code}/pg{page}/ try: resp requests.get(url, headersheaders, timeout10) if resp.status_code ! 200: print(f状态码异常{resp.status_code}跳过 {url}) continue rows parse_list_page(resp.text) all_data.extend(rows) print(f{region_name} 第 {page} 页累计 {len(all_data)} 条) except Exception as e: print(f请求失败{url}原因{e}) time.sleep(random.uniform(1.5, 3.5)) # 随机间隔降低被封风险 df pd.DataFrame(all_data) df.to_csv(data/raw_house.csv, indexFalse, encodingutf-8-sig)逻辑说明先定义城市和区域编码再逐页请求列表页用 CSS 选择器提取标题、总价、单价和房源信息。参数方面range(1, 51)控制每个区域抓取页数实际跑的时候建议先抓 3 页验证字段是否完整time.sleep(random.uniform(1.5, 3.5))是随机延时比固定 sleep 更不容易触发频率限制。encodingutf-8-sig是为了 Excel 打开不乱码。2.2 字段清洗与单位统一爬下来的原始数据里总价常带「万」字单价带「元/平米」户型是「3室1厅」这种字符串面积带「平米」。直接丢给模型肯定不行。我一般会在sol.ipynb里先做一轮字段拆解import re import pandas as pd df pd.read_csv(data/raw_house.csv) # 总价去掉“万”转 float df[total_price] df[total_price].str.replace(万, ).astype(float) # 单价去掉“元/平米”转 float df[unit_price] df[unit_price].str.replace(元/平米, ).astype(float) # 户型拆成室、厅 df[rooms] df[house_info].str.extract(r(\d)室).astype(float) df[halls] df[house_info].str.extract(r(\d)厅).astype(float) # 面积提取数字 df[area] df[house_info].str.extract(r(\d\.?\d*)平米).astype(float) # 朝向、楼层、年代等字段按同样思路提取 df[direction] df[house_info].str.extract(r\| ([东南西北])) df[floor] df[house_info].str.extract(r(\d)层) df[year] df[house_info].str.extract(r(\d{4})年建) df.to_csv(data/clean_house.csv, indexFalse, encodingutf-8-sig)这里的关键是正则要跟实际页面文本对齐。链家的houseInfo字段格式通常是「3室1厅 | 120.5平米 | 南 | 精装 | 中楼层(共18层) | 2010年建」所以用\|分隔后按位置提取更稳。如果发现某个字段大量为空先别急着删回原始页面确认是页面结构变了还是正则写错了。2.3 数据落库与版本管理清洗完的数据建议存成 CSV 的同时也写一份 SQLite方便后续用 SQL 做聚合分析。data目录里如果有.ipynb_checkpoints那是 Jupyter 自动生成的检查点可以忽略但别把它当成数据文件。import sqlite3 conn sqlite3.connect(data/house.db) df.to_sql(house, conn, if_existsreplace, indexFalse) conn.close()参数说明if_existsreplace每次覆盖适合反复调试如果要做增量抓取改成append并加去重逻辑。这一步做完你就有了一份可复用的结构化数据后面所有分析和建模都基于它。3. 探索性分析与可视化把房价分布和区域差异讲清楚3.1 单变量分布与异常值处理拿到clean_house.csv后先看总价和单价的分布。二手房数据里经常出现「1 元/平米」这种钓鱼房源或者总价几千万的别墅这些都会把模型带偏。我一般用 IQR 方法先圈出异常值import pandas as pd import matplotlib.pyplot as plt import seaborn as sns df pd.read_csv(data/clean_house.csv) # 单价分布 plt.figure(figsize(10, 5)) sns.histplot(df[unit_price], bins50, kdeTrue) plt.title(二手房单价分布) plt.xlabel(单价元/平米) plt.show() # IQR 去异常 Q1 df[unit_price].quantile(0.25) Q3 df[unit_price].quantile(0.75) IQR Q3 - Q1 lower Q1 - 1.5 * IQR upper Q3 1.5 * IQR df_clean df[(df[unit_price] lower) (df[unit_price] upper)] print(f原始 {len(df)} 条清洗后 {len(df_clean)} 条)逻辑说明sns.histplot看分布形态kdeTrue叠加核密度曲线。IQR 的 1.5 倍是常规阈值如果数据本身偏态严重可以放宽到 3 倍或者改用分位数截断比如保留 1% 到 99%。清洗后记得把df_clean存回文件后面建模用这份。3.2 区域均价对比与户型影响区域和户型是影响房价最直观的两个维度。用分组聚合加条形图能快速看出哪个区域最贵、哪种户型单价最高# 区域均价 region_price df_clean.groupby(region)[unit_price].mean().sort_values(ascendingFalse) plt.figure(figsize(10, 5)) sns.barplot(xregion_price.index, yregion_price.values) plt.title(各区域二手房均价对比) plt.ylabel(均价元/平米) plt.xticks(rotation45) plt.show() # 户型与单价 room_price df_clean.groupby(rooms)[unit_price].mean() print(room_price)参数说明groupby(region)[unit_price].mean()按区域求均价sort_values(ascendingFalse)降序排列。如果区域太多可以只取前 10 个。户型分析里rooms是数值型直接分组即可如果发现 1 室单价反而高于 3 室别惊讶这通常说明小户型集中在核心地段属于正常现象。3.3 相关性热力图与特征筛选建模前用热力图看各数值特征与单价的相关系数能帮你快速筛掉无关字段numeric_cols [total_price, unit_price, area, rooms, halls, floor, year] corr df_clean[numeric_cols].corr() plt.figure(figsize(8, 6)) sns.heatmap(corr, annotTrue, cmapcoolwarm, fmt.2f) plt.title(特征相关性热力图) plt.show()逻辑说明annotTrue显示数值fmt.2f保留两位小数。重点看unit_price与哪些特征相关性强。通常area和total_price相关性高但area与unit_price可能负相关或弱相关这取决于城市。如果某个特征相关系数低于 0.1建模时可以考虑剔除但别一刀切树模型对弱相关特征也有一定容忍度。4. 特征工程与模型训练从 sol.ipynb 到可复现的预测流程4.1 类别特征编码与训练集划分sol.ipynb里通常会把region、direction、floor这类类别特征做独热编码或标签编码。我一般对树模型用标签编码对线性模型用独热编码。下面以随机森林为例from sklearn.model_selection import train_test_split from sklearn.preprocessing import LabelEncoder from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, r2_score import pandas as pd df pd.read_csv(data/clean_house.csv) # 类别编码 le_region LabelEncoder() le_direction LabelEncoder() df[region_enc] le_region.fit_transform(df[region].astype(str)) df[direction_enc] le_direction.fit_transform(df[direction].astype(str)) # 特征与目标 features [area, rooms, halls, floor, year, region_enc, direction_enc] X df[features].fillna(df[features].median()) y df[unit_price] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) model RandomForestRegressor(n_estimators200, max_depth12, random_state42) model.fit(X_train, y_train) y_pred model.predict(X_test) print(MAE:, mean_absolute_error(y_test, y_pred)) print(R2:, r2_score(y_test, y_pred))参数说明test_size0.2是常见划分比例random_state42保证每次划分一致。n_estimators200是树的数量太少容易欠拟合太多训练慢max_depth12控制树深防止过拟合。fillna(df[features].median())用中位数填充缺失值比均值更抗异常值。跑完看 MAE 和 R2如果 R2 低于 0.6先检查特征里有没有漏掉关键字段比如楼层高低、装修情况、是否靠近地铁。4.2 模型对比与调参思路别只跑一个模型就交差。我一般会对比线性回归、随机森林和梯度提升树用交叉验证看稳定性from sklearn.linear_model import LinearRegression from sklearn.ensemble import GradientBoostingRegressor from sklearn.model_selection import cross_val_score models { Linear: LinearRegression(), RF: RandomForestRegressor(n_estimators200, max_depth12, random_state42), GBDT: GradientBoostingRegressor(n_estimators200, learning_rate0.05, random_state42) } for name, m in models.items(): scores cross_val_score(m, X, y, cv5, scoringr2) print(f{name} 交叉验证 R2{scores.mean():.4f} ± {scores.std():.4f})逻辑说明cross_val_score做 5 折交叉验证scoringr2看拟合优度。如果 GBDT 明显优于线性回归说明特征与目标存在非线性关系。调参时优先动n_estimators和learning_rate再调max_depth。别一上来就网格搜索先手动试几组找到量级再细化。4.3 预测结果输出与解释模型跑通后把预测结果和真实值拼在一起方便对比result pd.DataFrame({ 真实单价: y_test.values, 预测单价: y_pred, 误差: y_test.values - y_pred }) result.to_csv(data/prediction_result.csv, indexFalse, encodingutf-8-sig) print(result.head(10))参数说明y_test.values和y_pred长度一致直接拼接。误差列正数表示预测偏低负数表示预测偏高。如果某类房源误差特别大回看它的特征是不是在训练集里样本太少。这一步做完你就能拿着prediction_result.csv去写毕设的分析章节了。5. 避坑与常见问题跑这份源码时最容易卡住的五个地方5.1 爬虫请求返回 403 或验证码页面现象脚本跑几页后开始报 403或者返回的 HTML 里全是验证码。原因请求频率过高或者 User-Agent 被识别。解决把time.sleep调到 3 到 5 秒每次请求换一个 User-Agent必要时加 Referer 头。如果还是不行先减少抓取页数分时段跑。5.2 字段提取大量为空现象rooms、area等列出现大量 NaN。原因页面结构变了或者正则没匹配上实际文本。解决先打印一条原始house_info看格式再调整正则。比如链家有时用「室」有时用「房间」正则要写成(\d)[室房]这种兼容形式。5.3 模型 R2 很低甚至为负现象交叉验证 R2 低于 0.5或者测试集 R2 为负。原因特征里缺少关键变量或者异常值没清干净。解决先检查unit_price是否还有极端值再补充「楼层高低」「装修情况」「是否满五唯一」等字段。如果数据量太少考虑合并区域或放宽抓取范围。5.4 Jupyter 中 matplotlib 中文显示方块现象图表标题和轴标签中文变成方框。原因默认字体不支持中文。解决在 notebook 开头加两行配置plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] FalseSimHei是 Windows 常见中文字体Mac 可以换成Arial Unicode MS。5.5 sol.py 与 sol.ipynb 结果不一致现象同样的数据.py脚本跑出来的结果和 notebook 不一样。原因notebook 里可能手动改过某些单元格或者执行顺序乱了。解决把 notebook 从头到尾「Restart Run All」一遍确认所有步骤按顺序执行。如果还不行对比sol.py和 notebook 里的特征列表、模型参数是否完全一致。6. 进阶技巧把预测结果做成可交互的查询工具跑通模型只是第一步真正让这份源码在毕设里出彩的是把它变成一个能输入条件、实时返回预测单价的工具。我一般会用ipywidgets在 notebook 里搭一个简易交互界面不用额外部署 Web 服务答辩时直接演示。import ipywidgets as widgets from IPython.display import display area_input widgets.FloatSlider(value90, min20, max300, step5, description面积) rooms_input widgets.IntSlider(value3, min1, max6, step1, description室) year_input widgets.IntSlider(value2010, min1980, max2023, step1, description建成年) region_input widgets.Dropdown(optionslist(le_region.classes_), description区域) def predict(area, rooms, year, region): region_enc le_region.transform([region])[0] direction_enc 0 # 默认朝向可按需扩展 X_new pd.DataFrame([[area, rooms, 1, 5, year, region_enc, direction_enc]], columnsfeatures) price model.predict(X_new)[0] print(f预测单价{price:.2f} 元/平米总价约 {price * area / 10000:.2f} 万) widgets.interactive(predict, areaarea_input, roomsrooms_input, yearyear_input, regionregion_input)逻辑说明ipywidgets提供滑块和下拉框interactive把输入绑定到predict函数。参数方面FloatSlider的min、max、step按你数据里的实际范围调整Dropdown的选项直接取le_region.classes_保证编码一致。这个工具的好处是答辩时能现场改条件、看预测变化比静态图表更有说服力。如果你想让结果更稳还可以加一个「置信区间」输出。用随机森林的predict只能给点估计但你可以用ensemble里每棵树的预测值算标准差import numpy as np def predict_with_interval(area, rooms, year, region): region_enc le_region.transform([region])[0] X_new pd.DataFrame([[area, rooms, 1, 5, year, region_enc, 0]], columnsfeatures) tree_preds np.array([tree.predict(X_new)[0] for tree in model.estimators_]) mean_price tree_preds.mean() std_price tree_preds.std() print(f预测单价{mean_price:.2f} ± {std_price:.2f} 元/平米)参数说明model.estimators_是随机森林里所有决策树遍历每棵树预测再算均值和标准差。标准差越大说明这个房源在训练集里越少见预测越不确定。这个技巧在写毕设的「模型评估」章节时特别有用能体现你对模型不确定性的理解。最后说个血泪经验我每次改完特征或模型参数都会把sol.ipynb从头到尾「Restart Run All」一遍再把关键输出截图存档。因为 notebook 的玄学就在于你永远不知道哪次手动执行漏了哪一步。从那以后我每次交付前都强制走一遍全量重跑确认sol.py和 notebook 结果一致才敢打包。希望帮到你。本文还有配套的精品资源点击获取