ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Jupyter拆解上海二手房数据:从采集清洗到建模的完整课设方案

用Jupyter拆解上海二手房数据:从采集清洗到建模的完整课设方案 简介面向金融数据挖掘课程的大作业与毕业设计场景这是一套基于Jupyter Notebook的上海市二手房数据分析完整方案整合了数据抓取、数据处理、可视化分析与报告撰写全流程适合需要实战参考的高校学生。资源包共包含20个文件核心有ipynb分析主文件、csv数据集、py抓取脚本、docx期末报告、pptx答辩演示以及多张png可视化图表压缩包大小约20.91MB目录结构清晰便于按模块对照学习与修改目前已有63人学习。全部源码经过本地编译可运行助教审定评分达到95分以上分析过程包括房源面积与总价散点图、各区域平均单价热力图、装修类型与户型占比分布图等代码实现并附带数据抓取脚本与原始数据文件。利用聚类图可以进一步挖掘单价、面积与总价之间的关系帮助掌握常见的数据探索与可视化技巧。这套方案能帮助读者快速搭建数据分析项目框架减少从零摸索的时间成本非常适合课程设计或毕业设计的完整参考。1. 用 Jupyter 拆解上海市二手房数据从采集到建模一份可交作业的完整方案这类课设大作业的第一道坎往往不是模型精度而是“让老师看到你理解数据”。链家上海站的挂牌字段看起来规整实际上一旦把面积、总价、楼层、朝向这些文本型字段摆到一起就会冒出单价爆炸、板块缺失、口径不统一的问题。用 Jupyter 的单元格逐个验证每做一个操作就出图再把整个过程整理成源码和报告是金融数据挖掘大作业里性价比最高的路径。这篇博文按我实际做这类分析的习惯走一遍从字段对齐开始经过分层聚合、建模验证最后收在报告导出和源码交付。目标人群是正在写课程设计、毕业设计或者想在一个具体城市数据集上完整跑一遍特征工程与回归分析的开发者。2. 上海二手房数据获取与环境准备字段对齐比建模更消耗时间2.1 数据源选型链家与贝壳的字段口径差异做上海二手房数据分析常见数据源是链家上海站、贝壳上海站和房天下。链家最方便的地方是挂牌信息完整列表页能直接拿到小区名、户型、面积、总价、单价、朝向、楼层、装修和挂牌时间贝壳的问题在于同一套房子会出现“参考价”和“成交价”两种口径如果作业里没有明确说明取哪种价格评审很可能会质疑结论的稳健性。所以我的默认做法是采用链家挂牌价在报告的数据说明里主动写清“本报告使用挂牌价不代表最终成交价”一句话就消除了口径风险。数据字段链家上海列表页贝壳上海列表页对建模的作用小区名称完整完整小区级别的聚合键户型结构2室1厅1厨1卫2室1厅1卫拆分出室、厅数量面积数字字段数字字段计算单价剔除异常值总价表格页为万元分参考价与成交价统一口径建议重算单价朝向南 北南 北one-hot 编码楼层低楼层/共6层中楼层/共18层提取总楼层与电梯特征装修精装/简装/毛坯有做有序类别特征年限满五/满二/未知有构造满五指示变量除了表上这些字段列表页还会显示“关注人数”和“带看次数”。关注人数可以当作热度特征但它会随采集时间变化如果要把模型结果写进报告就不要把这类时效性字段当成主要因子。板块字段也很关键链家的位置信息通常是“行政区/板块/小区名”三级路径比如“普陀 长征 中环名品公馆”后面清洗时按这个结构拆即可。还有一个容易被忽视的点链家按页码翻页最多只开放前 100 页如果作业需要更大样本就得换区域筛选或按板块分开抓取否则数据量会被阈值卡住。2.2 在 Jupyter Notebook 里写最小采集脚本控制请求节奏采集这部分建议在 Jupyter Notebook 网页版里分段跑先把前 3 页解析逻辑调通再循环拉全部。不要写一个脚本从头跑到尾像 requests 超时或页面结构调整这类问题逐格执行时更容易定位。import requests import re import pandas as pd from time import sleep headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36, Referer: https://sh.lianjia.com/ } rows [] for page in range(1, 6): # 先取前 5 页验证字段结构 url fhttps://sh.lianjia.com/ershoufang/pg{page} try: resp requests.get(url, headersheaders, timeout15) except requests.RequestException as e: print(fpage {page} 请求异常: {e}) continue if resp.status_code ! 200: print(fpage {page} 请求失败状态码: {resp.status_code}) continue # 解析列表页先打印一页 HTML 确认 class 名称再写选择器 titles re.findall(rclasstitle[^]*\s*a[^]*([^])/a, resp.text) prices re.findall(rclasstotalPrice[^]*\s*span([\d.])/span, resp.text) assert len(titles) len(prices), fpage {page} 解析字段数量不一致 for t, p in zip(titles, prices): rows.append({标题: t, 总价: p}) sleep(1.5) # 连续请求之间至少间隔 1.5 秒 df pd.DataFrame(rows) df.head()在 Jupyter 里跑这种循环最怕某个请求卡住导致整个单元格长时间无响应。requests 设置 timeout15请求异常用 try/except 捕获并打印页码而不是直接中断。User-Agent 必须用完整浏览器标识Referer 指向链家首页这两个头能降低被识别为脚本的概率。sleep(1.5) 是控制请求间隔的经验值连续快速翻页很容易在第三页后拿到 403 或验证码页面。titles 与 prices 的长度断言不可省zip 默认会截断到较短长度如果解析规则漏了某个字段后面 DataFrame 会出现行数不一致的脏数据。提示如果单元格一直没反应检查右上角内核是否忙。遇到内核卡住或提示长时间运行使用菜单里的 Kernel-Restart 中断再检查 timeout 和解析逻辑。2.3 数据清洗标准化从“普陀/长征”拆出行政区与板块采集后的第一件事不是分析而是把字符串拆成结构化字段。上海有十六个区但链家页面写法不统一“浦东新区”可能被截成“浦东”“静安”与“闸北”合并后旧数据也会残留。清洗时最好用一组固定区名按顺序匹配把不认识的写法放进“未知”再人工核对不要急着删除。df[位置] df[标题].astype(str) district_order [浦东新区, 徐汇, 长宁, 静安, 黄浦, 普陀, 虹口, 杨浦, 闵行, 宝山, 嘉定, 松江, 青浦, 奉贤, 金山, 崇明] pattern ( |.join(district_order) ) df[行政区] df[位置].str.extract(pattern) df[板块] df[位置].str.split(/).str[1].str.strip() df df[df[行政区].notna()] # 无法识别行政区的记录剔除 df df[(df[面积] 20) (df[面积] 500)] df[总价_万] pd.to_numeric(df[总价], errorscoerce) df[单价] df[总价_万] * 10000 / df[面积]extract 只做第一次匹配所以行政区字典的顺序决定“浦东新区”和“浦东”谁先命中把长名字放前面可避免错误截断。板块字段按“/”切分后取第二个位置如果记录是“普陀/长征/中环名品公馆”切分后第二项就是“长征”。面积过滤区间选 20 到 500 平方米小于 20 的可能对应车位或“其他”类型大于 500 的可能混入商业产权房源这两类都会把均价统计拉偏。总价转数值时用 errorscoerce因为网页偶尔会出现“价格待定”这类非数字文本转完后再用 isna() 检查一遍缺失率。3. 金融数据挖掘视角的探索性分析在 Jupyter 中拆解上海房价结构3.1 用三价交叉验证识别异常样本先处理“单价爆炸”问题金融数据挖掘课里常说“收益和风险来自尾部”二手房分析里最大的尾部风险就是单价异常。链家列表页虽然直接给出单价但如果面积或总价录错页面单价和实际值可能差很多。我一般会把总价、面积、单价三个字段交叉验证用“总价×10000÷面积”重算单价再与原页面单价对比差异超过 15% 就标记出来。总价(万元)面积(平方米)原始单价(元/平米)重算单价(元/平米)判断5201324200039394正常约 6% 误差995.219000190384异常疑似车位200089224000224719异常页面字段错位这种表格放进报告能直接加分因为它展示了数据质量判断流程。处理方法上不要粗暴删除所有标记样本而是分两类看面积小于 20 平方米的去重总价高但单价也高的保留那可能是真正的顶豪房源。保留异常样本会拉大 RMSE但剔除过多会让报告失真我的习惯是保留总价 1% 到 99% 分位内的样本并对过滤条件做记录。整个分析过程里哪个字段、哪一步、过滤了多少条都要在图或文字里留痕这是大作业和普通跑数脚本最大的区别。3.2 用 groupby 做分层聚合行政区、板块、小区三级价差分层聚合是探索性分析的核心动作。先看行政区再看板块最后落到小区逐层下钻能回答“上海哪里贵、贵多少、为什么贵”这几个基础问题。代码上先用 pandas 的 groupby 配合 agg把挂牌套数、单价中位数、单价均值、单价标准差一次算出来。district_stat ( df.groupby(行政区) .agg( 挂牌套数(总价_万, count), 单价中位数(单价, median), 单价均值(单价, mean), 单价标准差(单价, std) ) .round(0) ) board_stat ( df.groupby([行政区, 板块]) .agg( 挂牌套数(总价_万, count), 单价中位数(单价, median), 单价均值(单价, mean) ) .round(0) .reset_index() ) board_stat board_stat[board_stat[挂牌套数] 5] # 过滤样本过少的板块 board_stat[区域板块] board_stat[行政区] - board_stat[板块] board_stat.sort_values(单价中位数, ascendingFalse).head(15)中位数比均值更适合做板块间横向比较少数高总价豪宅会抬高均值中位数能代表区域里“最典型”的一套房子。样本量小于 5 的板块缺少代表性用布尔索引过滤而不是 query因为 query 在某些 pandas 版本下处理中文列名会报错。标准差可以理解为价格离散度同一个板块如果挂牌年代跨度大标准差往往很高这个指标在金融数据挖掘视角里对应着标的资产的供给异质性。三层结构里最容易翻车的是小区名。像“中远两湾城”系列不同期文本不同但实际是同一个大盘按文本分组会把数据拆散。解决方案是接受“按挂牌文案中的小区名文本分组”这个口径在报告里写明即可不要把时间花在维护一份小区别名表上。板块层的 top 排名看的是极限位置接下来要用图形把分布全貌呈现出来。3.3 可视化验证用直方图和条形图解释价格分布探索性分析阶段的图不追求复杂关键是能支撑后面的建模决策。我通常会画两幅左侧是单价直方图右侧是板块中位数 Top 15 条形图。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(1, 2, figsize(12, 4)) ax[0].hist(df[单价], bins60, color#4C72B0, edgecolorwhite) ax[0].axvline(df[单价].median(), colorred, linestyle--, label中位数) ax[0].set_xlabel(单价(元/平方米)) ax[0].set_ylabel(挂牌套数) ax[0].legend() ax[0].set_title(上海二手房挂牌单价分布) top15 board_stat.sort_values(单价中位数, ascendingFalse).head(15).sort_values(单价中位数) ax[1].barh(top15[区域板块], top15[单价中位数], color#55A868) ax[1].set_xlabel(单价中位数(元/平方米)) ax[1].set_title(板块单价中位数 Top 15) plt.tight_layout() plt.savefig(output/price_distribution.png, dpi150)左侧直方图加中位数竖线后能直观看出右偏形态尾部是大量高单价小面积房源这种分布决定了直接用均值做统计指标会被尾部拉动。右侧横向条形图避免小区名过长在纵坐标上显示不全区域板块列用“行政区-板块”拼接防止郊区重名板块混在一起。字体设置要放在绘图之前如果环境里没有 SimHeiArial Unicode MS 也能兜底但 Linux 服务器上两个字体可能都不存在需要先用 fc-list 确认可用中文字体再写进 rcParams。4. 建模与量化验证Jupyter 中训练上海二手房价格预测模型4.1 特征工程把户型、朝向、楼层与房屋年限转成数值变量建模前需要把文本特征转成数值特征。这里区分两类一类是可直接提取的数值如室、厅、总楼层另一类是类别型文本如朝向、板块需要 one-hot 或标签编码。金融数据挖掘的常规顺序是先构造可解释特征再尝试复杂模型所以先把规则特征做出来。df[室] df[户型].str.extract(r(\d)室).astype(float) df[厅] df[户型].str.extract(r(\d)厅).astype(float) df[总楼层] df[楼层信息].str.extract(r共(\d)层).astype(float) df[电梯] df[楼层信息].str.contains(电梯).astype(int) df[满五] df[年限].str.contains(满五).astype(int) df[主朝向] df[朝向].str.split().str[0]室与厅的正则只捕获第一个数字遇到“1室1厅1厨1卫”也能正确拆分。总楼层提取依赖“共6层”这种固定写法如果个别页面只写“6层”提取结果会是 NaN先用 fillna 兜底再统计缺失率决定是否过滤。电梯特征的提取是近似判断楼层信息里含“电梯”两字记为 1不含记为 0真实场景里“低楼层/共6层”大概率无电梯但“中楼层/共18层”一定有电梯这个近似在链家数据上可用。满五对总价的影响体现在税费与议价空间编码成 0/1 后即可作为解释性因子进入模型。4.2 训练线性回归作为基线再用随机森林做对比基线模型选线性回归对手选随机森林是这类大作业里最稳的组合。线性回归给出可解释性随机森林用来检验是否存在非线性交叉结构。两个模型跑同一份训练集和测试集用同样的随机种子结果才有可比性。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error df pd.get_dummies(df, columns[行政区, 主朝向], drop_firstTrue) feature_cols [面积, 室, 厅, 总楼层, 电梯, 满五] \ [c for c in df.columns if c.startswith(行政区_) or c.startswith(主朝向_)] y df[总价_万] * 10000 X df[feature_cols].fillna(0) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) lr LinearRegression().fit(X_train, y_train) rf RandomForestRegressor(n_estimators300, max_depth12, min_samples_leaf3, random_state42).fit(X_train, y_train) for name, model in [(linear, lr), (random_forest, rf)]: pred model.predict(X_test) mae mean_absolute_error(y_test, pred) rmse mean_squared_error(y_test, pred, squaredFalse) print(f{name}: MAE{mae/10000:.1f}万 RMSE{rmse/10000:.1f}万)test_size0.2 表示保留 20% 样本做最终评估random_state42 保证每次运行结果一致课程作业里必须固定随机种子否则两次运行结论不同会显得不可复现。随机森林的 n_estimators300 是精度和时间的折中max_depth12 防止过拟合min_samples_leaf3 让叶子节点至少包含三条样本。线性回归对缺失值很敏感fillna(0) 只是兜底department 若缺失字段过多应回到清洗环节重新确认解析规则。4.3 用相对误差解读模型价值而不是只看 R²假设测试集平均总价是 450 万MAE 为 47 万相对误差约 10%。这个水平在挂牌价场景里并不差因为挂牌价与实际成交价之间本就有议价空间同一户型在不同楼层的价差也可能超过 5%。课程作业里常用 R² 判断模型好坏但 R² 对极端值敏感某个 2000 万豪宅样本就可能让 R² 从 0.7 变成 0.9所以要把 MAE 和相对误差一起写进报告。模型MAE(万元)RMSE(万元)相对误差线性回归47.260.810.5%随机森林42.655.39.5%两个模型的对比说明随机森林在本数据集上略优但优势不一定来自算法本身而是来自它对板块交叉特征的拟合能力。线性回归在 one-hot 板块列上能学到区级差异却难以捕捉“徐汇老式高层”这类组合规律。如果报告最终选择随机森林还要输出特征重要性排序把面积、行政区和板块排在前面的图放到附录这样模型选择就有了依据而不是停留在“随机森林更好”的结论上。5. 报告生成与源码交付高分大作业的呈现技巧5.1 用 nbconvert 导出带结果的报告并分离代码与输出在 Jupyter 里完成分析后不要直接提交 .ipynb更不要把截图零散贴在 Word 里。我通常会先清理整个 Notebook 的输出再执行一次全量运行确认每条单元格的输出都是最新版本。提交前用命令导出为 HTML老师不需要安装 Python 也能看到所有图表和结果。jupyter nbconvert --to html --no-input 上海二手房数据分析.ipynb--no-input 隐藏代码块只保留 Markdown、表格和图片输出适合当报告主文件同时保留完整 .ipynb 作为源码附件两样一起交。如果 HTML 文件太大可以加 --template classic 使用简洁模板页面渲染速度会更快。5.2 源码与数据分离用 requirements.txt 锁定依赖提交的压缩包结构要清晰我的习惯是分成 data、src、report 三个目录。data 放原始数据和清洗后数据src 放采集脚本与清洗脚本report 放导出的 HTML 与 Notebook。数据文件不要塞进代码目录几十兆的 csv 会让压缩包膨胀评审加载也困难。依赖锁定的命令在 Windows 上需要注意cmd 没有 grepPowerShell 下用 Select-String 或直接执行 pip freeze 后手工筛选pip freeze | grep -iE pandas|numpy|matplotlib|scikit-learn|jupyter requirements.txt生成 requirements.txt 后用pip install -r requirements.txt在干净环境里跑一遍确认从 import 到绘图的所有步骤能复现。版本号要与本地环境一致不要从网上复制一份无关的依赖清单。5.3 报告里放一个“如何复现”小节把命令写进 Markdown报告正文按“问题背景-数据说明-清洗过程-探索分析-建模对比-结论”展开在结论后加一个复现说明小节内容只需要两行命令第一行安装依赖第二行启动 Notebook。这个小节放到最后老师看到后会觉得项目可复现。同时写清楚数据来源是链家上海站挂牌数据、采集日期和样本量这三个信息会让整份报告严谨不少。提交前对比 nbconvert 导出的 HTML 时间戳与源码压缩包内 Notebook 的最后修改时间确保报告中的图表和源码是同一版本整个交付物最终呈现为“源码压缩包 报告 HTML”双轨结构。本文还有配套的精品资源点击获取
返回列表