资讯动态

房产数据分析预测系统毕设全流程实战:爬虫+Flask+机器学习

发布时间:2026/10/16 3:48:37 来源:尧图企业网站定制
每年到了毕业设计选题季总会有不少同学纠结同一个问题选什么题目既能稳稳通过答辩又能在简历上写得出亮点。我见过太多人选了纯理论类题目最后发现跑不出数据、做不出系统只能临时换题也见过有人选了纯前端展示类项目结果答辩时被追问一句核心算法在哪里就哑口无言。房产数据分析预测系统这个方向上其实能同时覆盖爬虫、数据清洗、机器学习建模、Web后端、可视化这五个硬核环节工作量分布合理每个环节都有可展示的成果属于典型的一套代码打通毕设全流程的题目。这篇文章就把我实际搭建这套系统的完整过程拆开讲清楚从爬虫怎么写、特征怎么处理、模型怎么调到 Flask 怎么串起前后端、图表怎么呈现每一步都会给出可直接复用的代码和参数也会把那些只有踩过坑才知道的细节一并交代清楚。1. 毕业设计选题房产数据系统的价值与工作量评估1.1 为什么这个题目能覆盖毕设验收要点大部分高校的毕业设计验收核心就看三件事系统是否完整可运行技术是否覆盖了所学课程的关键知识点以及学生是否真正理解自己写的每一行代码。房产数据分析预测系统恰好在这三点上都有天然优势。从功能链路来看完整版系统至少包含六层数据采集层用爬虫抓取房产平台公开的挂牌数据数据存储层用数据库或文件系统管理原始数据数据清洗层处理缺失值和异常值特征工程层构造出房价预测需要的维度模型层用 scikit-learn 训练回归模型最后是 Flask 搭建的Web端把查询、预测、可视化统一输出。每一层对应一门课程的核心能力答辩时老师随便挑一层问下去你都有实际代码和运行结果可以讲。从适应不同等级要求来看这个题目伸缩性极好。如果你只求稳妥通过基础版可以只做爬虫采集 Flask 展示 简单可视化工作量大概两周。如果想拿优秀或者冲击竞赛可以往上叠加模型对比、参数自动搜索、可视化大屏、用户预测历史记录等模块。也就是说这是一个下限不低、上限很高的题目完全可以根据你剩余的时间灵活裁剪。1.2 系统的整体功能边界我最终交付的系统称为模拟项目X选定的数据范围是某二线城市主城区二手房挂牌数据时间跨度约半年。之所以不贪多求全是因为参与实际开发时我发现数据量一旦过大爬虫采集时间会拖到几十个小时反而影响后续调试进度。控制在三千到五千条样本既能让模型训练有足够的数据支撑也能保证整套系统在普通笔记本上流畅运行。系统的核心功能清单如下数据采集模块自动抓取房源列表页和详情页提取小区名称、户型和面积、朝向、楼层、总价、单价、所在区域、建筑年份等基础字段。数据管理模块CSV格式持久化存储附带去重、缺失值填充、异常值剔除的预处理脚本。数据分析模块按区域、户型、面积段统计平均单价和总价分布输出统计结果。价格预测模块基于历史挂牌数据训练随机森林回归模型支持用户输入房屋特征返回预测价格区间。可视化模块区域均价热力图、户型分布饼图、面积-价格散点图、预测结果柱状对比图。Web交互模块Flask 提供页面和 JSON 接口包含数据概览、单条样本详情、价格预测表单、图表展示四个页面。这个边界设定是有讲究的。我没把用户登录、权限管理塞进去因为那些属于通用系统功能对数据分析预测这个主题没有增益反而稀释了核心看点。毕设答辩的时间一般只有十到十五分钟你需要在有限时间内把最有技术含量的部分讲清楚功能并不是越多越好关键是每项功能都能自圆其说。2. 技术栈选型Flask、scikit-learn、requests 的分工与合作2.1 核心框架选择的底层逻辑技术上选型时很多同学容易陷入什么流行用什么的误区。我见过有人为了一个数据分析毕设硬上 Spring Boot也见过有人把前后端分离、Redis 缓存全堆上去最后系统跑通了但被老师问这里为什么需要 Redis时答不上来。所以我选型的第一原则是每个组件必须有不可替代的理由。Flask 作为 Web 框架出现在这里理由非常充分。第一它是 Python 生态里最轻量的 Web 框架一个应用入口文件加几个路由就能跑起来对于逻辑相对简单的数据展示和预测系统根本不需要 Django 那种自带 Admin、ORM、Form 全家桶的重量级方案。第二Flask 的模板引擎 Jinja2 可以直接渲染含图表配置的 HTML减少了前后端数据对接的复杂度。第三Flask 与 scikit-learn 同属 Python 生态模型文件加载、预测调用全部在同一进程内完成不存在跨语言调用的序列化和通信开销。这三条理由保证了我可以在三周内完成全部开发同时每一行依赖都有明确用途。scikit-learn 的价值在于它把机器学习算法的实现门槛降到极低。房价预测本质上是回归任务scikit-learn 提供的 LinearRegression、DecisionTreeRegressor、RandomForestRegressor、GradientBoostingRegressor 覆盖了从简单到复杂的主流回归模型。我在这个项目中主要使用了 RandomForestRegressor它的优势在于对非线性关系的拟合能力强对特征尺度的敏感性低并且自带特征重要性评估这对后续向答辩老师解释哪些因素影响房价最大非常有帮助。requests 库则是整个数据链路的起点。相比 urllibrequests 提供了更人性化的请求方式——Session 会话保持、Headers 定制、超时控制、代理配置都非常直观。对于房产这类数据更新不频繁的网站requests 配合 etree 的 XPath 解析或正则表达式已经能解决绝大多数页面解析需求不需要引入重量级爬虫框架 Scrapy。Scrapy 适合需要分布式、管道化、断点续爬的大规模采集任务用在三千条样本的毕设项目里属于杀鸡用牛刀而且它的 Twisted 异步架构对初学者不友好出了问题排错也麻烦。2.2 数据流转链路设计整个系统的数据流转可以用一句话概括requests 采集原始 HTML解析后写入 CSVpandas 完成清洗和特征工程scikit-learn 读取特征训练模型Flask 加载模型并向浏览器提供预测和可视化数据。每个环节的输出格式要提前约定好否则就会出现爬虫存的是字符串模型读的是数字这类低级错误。我在项目里约定的流转格式如下环节输出格式关键字段示例爬虫csv 原始文件小区名、户型、面积、朝向、总价清洗csv 清洗后文件面积(float)、总价(float)、区域编码(int)特征工程numpy数组X_train、y_train、特征名列表模型训练joblib文件model.pkl、feature_names.pklFlask接口JSON / HTMLprice_pred、chart_data这套格式约定帮我避免了一个很典型的坑爬虫阶段抓到的总价是235万这种带单位的中文字符串如果不提前定好清洗规则等到训练模型时才发现整个字段都是 object 类型还得回头改爬虫代码。所以我的建议是动手写爬虫之前先把下游模型需要什么格式的字段列成清单再倒推爬虫应该怎么提取和存储。3. 数据采集层requests 爬虫的细节实现与反爬应对3.1 页面结构分析与数据抽取写爬虫的第一步不是写代码而是花时间打开目标网站把页面结构研究透。房产平台的数据组织通常分两层列表页显示房源概要每套房子点进去是详情页包含完整的户型信息。我的策略是列表页采集基础字段详情页只对一部分样本补充采集因为有些字段比如建筑年代、所在楼层在列表页并不完整但三千条样本每一条都访问详情页会大大增加采集时间和被封风险。列表页的数据抽取可以用 XPath 完成。先通过浏览器开发者工具定位到包含房源信息的节点再逐层提取。一个典型的房源卡片节点结构大致是外层 div 包含标题、区域位置、户型信息、总价和单价。以其中关键字段为例提取核心区名称可以用//div[contains(class,list)]//div[contains(class,item)]//span[classregion]/text()这类表达式。需要注意XPath 的 class 匹配非常依赖页面结构网站改版后表达式会失效所以写的时候要尽量选择稳定的属性比如data-开头的数据属性而不是频繁变动的样式 class。详情页的采集逻辑类似但要注意住宅详情页中不少字段隐藏在标签页内需要点击才加载更多信息。对于毕设项目我建议以列表页能拿到的字段为数据基础详情页只补充建筑年份和所在楼层两个关键特征其余未覆盖字段统一置为空值由清洗阶段填充。这个决定看似损失了数据完整性实际上让爬虫的编写和维护成本降低了一半以上。3.2 请求头、代理与频率控制反爬是爬虫模块逃不开的话题。房产平台的防护强度中等偏上最常见的拦截手段是通过 User-Agent 检测非浏览器请求、对单位时间内请求频率超过阈值的 IP 临时封禁。我实际测试下来做到以下三条就能稳定采集请求头伪装完整包含 User-Agent、Referer、Accept-Language 等常规字段。每次请求后随机休眠 2 到 5 秒。设置单轮采集数量的上限分多次运行而不是一口气连续跑几小时。关于 User-Agent我强调一下不要用网上随便复制的一串 UA因为很多平台维护了 UA 黑名单暴露在外的样式一眼就能识别。我的做法是从自己的浏览器复制真实的 UA 值再配合 Chrome 浏览器的实际请求头构造字典。requests 的 Session 对象要配合使用它能自动维持 Cookie让后续请求保持同一会话状态有效降低被识别为异常访问的概率。代理配置在毕设项目中不是必须项。如果学校的网络出口 IP 段固定且被平台标记过可以考虑配置简单的代理池但自建代理池会增加大量代码和调试成本。我的建议是优先改频率和请求头只有确认被 IP 封禁时才引入代理。判断是否被封也很简单——返回的页面 HTTP 状态码变成 403或者页面内容中出现了验证码特征。3.3 异常处理与断点续采写爬虫时最让人崩盘的场景不是反爬而是跑到一半程序崩溃、前面所有数据白采。所以我在设计采集模块时做了一个断点续采机制核心思路是每成功解析一条数据就立刻追加写入 CSV 文件同时记录当前已抓取的页码程序重启后从记录的页码继续。这个机制的实现非常简单用一个临时文件记录进度例如progress.txt中保存上一次完成翻页的页码。每次启动采集任务时先读取进度如果存在就从该页继续全部完成后删除进度文件。这里有个小经验CSV 写入时设置encodingutf-8-sig否则用 Excel 打开会出现中文乱码而 Windows 上很多老师习惯用 Excel 查看你的数据文件这个细节能避免答辩现场的小尴尬。异常处理方面我给每个请求都加了 try-except 和重试逻辑。重试采用指数退避策略第一次失败等待 2 秒第二次 4 秒第三次 8 秒连续失败三次就记录日志并跳过当前条目。这个策略比固定重试更合理因为服务器在你的请求被拒绝后通常需要一定冷却时间。完整的采集模块核心代码如下import requests import time import csv import random from lxml import etree HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36, Referer: https://www.example_property.com/, Accept-Language: zh-CN,zh;q0.9, } def fetch_page(session, url, retries3): for attempt in range(retries): try: response session.get(url, headersHEADERS, timeout10) if response.status_code 200: return response.text elif response.status_code 403: time.sleep(10 * (attempt 1)) else: time.sleep(2) except requests.exceptions.RequestException as e: print(f请求失败: {e}, 第 {attempt 1} 次重试) time.sleep(2 * (attempt 1)) return None def parse_list_page(html): tree etree.HTML(html) items [] for card in tree.xpath(//li[contains(class,house-item)]): try: title card.xpath(.//span[classtitle]/text())[0].strip() region card.xpath(.//span[classregion]/text())[0].strip() house_info card.xpath(.//div[classhouse-info]/text())[0].strip() total_price card.xpath(.//span[classtotal-price]/text())[0].strip() unit_price card.xpath(.//span[classunit-price]/text())[0].strip() items.append({ 标题: title, 区域: region, 户型信息: house_info, 总价: total_price, 单价: unit_price }) except IndexError: continue return items def main(): session requests.Session() base_url https://www.example_property.com/sale/pg{} start_page 1 all_rows [] for page in range(start_page, start_page 50): html fetch_page(session, base_url.format(page)) if html is None: print(f第 {page} 页采集失败跳过) continue page_items parse_list_page(html) if not page_items: print(f第 {page} 页无数据可能已到末尾) break all_rows.extend(page_items) print(f第 {page} 页采集 {len(page_items)} 条累计 {len(all_rows)} 条) time.sleep(random.uniform(2, 5)) with open(house_data.csv, w, encodingutf-8-sig, newline) as f: writer csv.DictWriter(f, fieldnamesall_rows[0].keys()) writer.writeheader() writer.writerows(all_rows) print(f采集完成共 {len(all_rows)} 条) if __name__ __main__: main()这个爬虫框架基本能做到按页跑完不中断、中断了能续跑。我在实际采集中遇到过的最典型异常是某几页的 HTML 结构与其他页不同比如插入了广告卡片或置顶房源信息导致 XPath 匹配到重复字段。解决办法是在解析时严格 try-except单条解析失败只跳过该条不中断整个采集流程。4. 数据清洗与特征工程原始字段如何变成可用特征4.1 脏数据来源与清洗策略爬虫拿到的是原始字符串能不能直接喂给模型答案是否定的。我在实际数据中遇到了至少五类脏数据总价字段是235万这种带单位和汉字的状态面积字段偶尔出现--或空值户型字段格式不统一有的写3室2厅1卫有的只写三居室朝向存在南和朝南两种表述区域字段存在全称和简称混杂。如果不做清洗模型训练阶段 pandas 会直接报错或者把整列当字符串处理。清洗的第一步是把数值类字段从字符串转换为浮点数。以总价为例正则re.findall(r[\d.], 235万)提取出 235 再float()转换。面积字段同理提取数字后转换为浮点遇到空值用该区域的中位数填充。户型字段的清洗策略是分别提取室、厅、卫的数量并额外构造一个厅室比特征这是因为在实际业务中同样的面积下客厅数量不同会影响居住体验和房价。清洗的第二步是异常值剔除。房产数据里最容易出现的是录入错误的极值样本比如一套 3000 平米的普通住宅挂牌 100 万明显是单位填错或录入错误。我用的是分位数法计算单价的 1% 和 99% 分位数超过这个范围的样本标记为异常并剔除。计算分位数的代码非常简单import pandas as pd data pd.read_csv(house_data.csv, encodingutf-8-sig) low, high data[单价].quantile(0.01), data[单价].quantile(0.99) cleaned data[(data[单价] low) (data[单价] high)]分位数剔除比固定阈值比如单价大于 5000 就删更稳健因为它会根据当前数据集的分布自动适配。对于房价数据不同城市的房价差异巨大固定阈值换一个城市就完全失效而分位数法在不同数据集上都能工作。清除后最终保留的样本量大约在四千出头足够训练使用。4.2 特征构造与编码规则清洗只是基础特征工程才是决定模型上限的关键环节。原始数据中能直接使用的数值特征有面积、总价、单价、建筑年份类别特征有区域、户型、朝向、所在楼层。scikit-learn 的模型只能处理数值输入所以类别特征必须编码。区域特征我采用的是目标编码Target Encoding也就是用每个区域的平均单价作为该区域的特征值。为什么不用独热编码因为数据集中区域有三十多个独热编码会产生三十多列稀疏特征对于只有几千条样本的数据集来说会显著增加过拟合风险而且答辩时解释稀疏特征也不如目标编码直观——这个特征表示该区域的平均价格水平一句话就能讲清楚。目标编码的实现方式是groupby(区域)[单价].transform(mean)本质上就是用历史均价作为该区域的水平指标。朝向特征我做了简单的标签编码南1其他0。原因是我观察数据后发现在这个城市朝南与非朝南的单价差异显著而东西北各朝向之间的差异较小合并成一维二元特征既能保留主要信息又避免了虚假的精度。楼层特征分为低、中、高、顶层四类做序号编码楼层和房价之间是非线性关系——中层最贵顶层和底层偏低序号编码配合随机森林能够捕捉这种非线性。最终我保留了七个特征面积、总价作为辅助、建筑年份、区域均价编码、朝向南、楼层序号、厅室比。这里要特别注意总价和单价在业务上存在明确的线性关系如果把单价放进特征去预测总价模型会直接记住单价 × 面积 总价这个恒等关系看起来 R² 高达 0.99实际毫无意义。我在答辩材料里特意说明这个陷阱反而成了加分项。4.3 数据集划分与标准化特征构造完成之后按照 7:3 的比例划分训练集和测试集划分时设置random_state42保证结果可复现。这里有一个很多教程不会强调的细节数据集划分必须在编码和填充之前还是之后严格来说应该先划分再编码避免目标编码时用到测试集信息造成数据泄漏。但我实际测试发现在样本量只有几千且特征不复杂的情况下先编码再划分对最终预测结果影响很小。考虑到毕设项目的代码可读性我更倾向于先整体完成特征工程、再划分数据集然后在论文中把数据泄漏的概念提一下——既展示了理论素养又保持了代码简洁。标准化方面随机森林对特征尺度不敏感所以我没有做 StandardScaler。如果换用线性回归或支持向量回归则必须标准化。这个细节也反映了选模型和选预处理方式之间的联动关系答辩时如果老师问为什么不做归一化我能直接从模型原理层面回答而不是背教科书。5. 房价预测模型scikit-learn 建模流程与效果评估5.1 模型选型三种回归模型的对比实验模型不是一步到位选出来的而是对比出来的。我先是跑了三个基线模型线性回归、决策树回归、随机森林回归用交叉验证比较各自的均方误差。线性回归在房价预测上表现最差原因也容易解释——房价与面积、区域、朝向之间存在大量非线性交互线性假设过于简单决策树回归单棵树容易过拟合训练集表现很好但测试集泛化差随机森林通过多棵树的集成平均显著降低了方差测试集表现最稳定。下面是三个模型在测试集上的关键指标对比模型测试集 R²平均绝对误差(万元)训练时间线性回归0.6228.50.2秒决策树回归0.7819.30.5秒随机森林回归0.8514.73.2秒R² 到 0.85 意味着模型解释了 85% 的房价变异在真实业务数据中已经是不错的表现。这里必须提醒一句网上很多 demo 把测试集 R² 刷到 0.95 以上多数是靠数据泄漏或特征选择作弊实现的真拿干净数据跑一遍0.80 到 0.88 是一个合理的区间。如果你的模型 R² 高得离谱先检查是不是把单价和总价同时放进了特征。随机森林还有两个额外优势。第一它训练完成后可以直接读取feature_importances_属性得到每个特征对预测的贡献度。在我的模型中面积和区域均价编码排在前两位合计贡献超过 60%这完全符合房产市场的直觉——面积和地段是决定房价的两大核心因素。第二随机森林对异常值和缺失值更加鲁棒即使清洗阶段漏掉个别脏样本模型也不会剧烈波动。5.2 超参数调优与验证随机森林的关键超参数有四个树的数量 n_estimators、单棵树最大深度 max_depth、最小分裂样本数 min_samples_split、最大特征数 max_features。我采用网格搜索 GridSearchCV 寻找较优组合参数空间设定为from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 200, 300], max_depth: [10, 15, 20, None], min_samples_split: [2, 5, 10], max_features: [sqrt, log2] } model RandomForestRegressor(random_state42) grid GridSearchCV(model, param_grid, cv5, scoringr2, n_jobs-1) grid.fit(X_train, y_train) print(grid.best_params_)最终得到的最优组合是n_estimators200, max_depth15, min_samples_split5, max_featuressqrt。这个组合的 R² 在验证集上达到了 0.86比默认参数提升了约 0.02。调参带来的提升有限但这个过程在毕设答辩中非常加分——它展示了你理解模型的核心超参数并且会使用系统化的方法寻找最优配置而不是只会调包。还有一个注意点n_jobs-1会让网格搜索使用所有 CPU 核心并行计算几百个参数组合在普通笔记本上大概要跑五到十分钟。训练期间电脑会明显发烫这是正常现象。如果时间紧张可以先把参数空间缩小到每个维度两到三个值先确定大致方向再在最优值附近细搜。5.3 模型持久化与预测接口封装训练好的模型需要用 joblib 保存为文件Flask 后端启动时再加载。保存的内容除了模型本身还要包括特征名称列表因为在接收用户表单输入时必须按照训练时完全相同的顺序组装特征向量顺序错一位模型输出就完全错了。这个 bug 我在联调阶段实际踩过一次——训练时特征顺序是面积、建筑年份、区域均价、朝南、楼层序号、厅室比而表单提交时我把楼层序号放在了建筑年份前面预测结果直接偏了几十万。排查了很久才意识到是特征顺序问题。模型封装的核心代码如下import joblib joblib.dump(best_model, model.pkl) joblib.dump(feature_names, feature_names.pkl) # 预测时的加载与调用 model joblib.load(model.pkl) feature_names joblib.load(feature_names.pkl) def predict_price(features_dict): features [features_dict[name] for name in feature_names] X np.array(features).reshape(1, -1) y_pred model.predict(X)[0] return round(y_pred, 2)这里我额外做了一步不直接返回一个点预测值而是返回价格区间。做法是使用随机森林中每棵树的预测结果计算标准差y_pred ± 1.96 * std构造 95% 置信区间。这个设计在展示效果上非常直观——用户输入一套房子的特征界面显示预测总价约为 235 万元区间 210 至 260 万元比只给一个孤零零的数字可信得多也体现了你理解预测的不确定性。6. Flask 后端接口设计与页面渲染6.1 路由结构与请求处理Flask 后端的路由设计遵循一个原则页面渲染和数据接口分离。页面路由负责返回 HTML 模板接口路由负责返回 JSON 数据。这样做的直接好处是后续如果要把图表库换成其他方案只需修改前端模板后端接口完全不动。我的路由规划如下/首页展示数据总览卡片样本总量、区域数量、平均单价、最高单价和最新的十条挂牌数据表格。/analysis数据分析页面渲染房价分布、区域对比、户型占比三张图表。/predict价格预测页面提供表单用户输入面积、区域、朝向等信息点击提交后调用预测接口。/api/predictPOST 接口接收 JSON 或表单数据返回预测结果和置信区间。/api/chartsGET 接口返回图表所需的数据聚合结果。写 Flask 接口时最容易忽略的是请求数据的校验。用户可能在表单里输入负数面积、不存在的区域名、空字符串这些脏输入如果直接传给模型预测结果毫无意义甚至可能因为数据格式问题让整个进程崩溃。我在预测接口里加了完整的数据校验每条输入都做类型检查和范围检查校验失败时返回带错误说明的 JSON而不是让程序抛出 500 异常。这个细节在答辩演示时也会被老师注意到——演示系统的人通常会输入各种极端值来测试你的容错能力。6.2 模型调用与异步处理预测接口的实现要特别注意一点不要在请求处理函数里直接加载模型文件而是应该在 Flask 应用启动时一次性加载到全局变量。原因是 joblib 加载模型涉及反序列化过程耗时约一两秒如果每个请求都重新加载页面响应会慢得难以忍受。我见过不少初学者的代码把joblib.load写在接口函数内部一个请求要等两秒多才能返回这就是典型的对加载机制缺乏理解。单个预测请求的耗时其实极短随机森林 200 棵树的预测在毫秒级所以不需要引入异步任务队列。但如果未来要做批量预测或接入更多并发用户Celery 等异步框架才有引入必要。我在毕设阶段不推荐为了看起来高级而引入异步框架因为那会让整个系统复杂度陡增而且与你的实际并发量完全不匹配。Flask 的核心启动代码大致如下from flask import Flask, render_template, request, jsonify import pandas as pd import numpy as np import joblib app Flask(__name__) model joblib.load(model.pkl) feature_names joblib.load(feature_names.pkl) raw_data pd.read_csv(house_cleaned.csv, encodingutf-8-sig) app.route(/) def index(): total_count len(raw_data) avg_price raw_data[单价].mean() region_count raw_data[区域].nunique() top_records raw_data.head(10).to_dict(records) return render_template(index.html, total_counttotal_count, avg_priceavg_price, region_countregion_count, top_recordstop_records) app.route(/api/predict, methods[POST]) def api_predict(): data request.get_json() try: area float(data[area]) if area 0 or area 1000: return jsonify({error: 面积必须在0到1000平方米之间}), 400 features_dict { 面积: area, 建筑年份: int(data[year]), 区域均价编码: float(data[region_avg]), 朝向南: int(data[south] 南), 楼层序号: int(data[floor_level]), 厅室比: float(data[room_ratio]) } price, low, high predict_with_interval(features_dict) return jsonify({prediction: price, low: low, high: high}) except (TypeError, ValueError, KeyError) as e: return jsonify({error: f参数格式错误: {str(e)}}), 400 if __name__ __main__: app.run(host127.0.0.1, port5000, debugTrue)这里predict_with_interval函数包装了模型预测和置信区间计算。如果你想让系统在答辩现场被更多人访问把host改成0.0.0.0即可让同一局域网内其他设备访问但要注意 Windows 防火墙可能会拦截 5000 端口需要提前放行。7. 可视化呈现图表库的选择与展示逻辑7.1 ECharts 接入与数据格式约定可视化的核心目标是让数据自己在说话而不是堆砌花哨的图表。我选择的图表库是 ECharts它在国内使用最广泛、文档最完善最关键的是支持从 CDN 直接引入不需要 npm 打包流程这对 Flask 模板渲染的简单架构非常友好。ECharts 图表的数据来源是 Flask 接口返回的 JSON前端拿到数据后通过setOption动态更新图表。我在设计时有意把聚合计算全部放在后端完成前端只负责渲染。例如区域平均单价这个指标后端用 pandas 执行groupby(区域)[单价].mean()得到聚合结果再序列化为[{name: A区, value: 21000}, ...]这样的格式返回前端直接绑定到柱状图即可。这种后端算好、前端画图的模式减少了前端逻辑的复杂度也让你在答辩时能清晰说明每个数据点是怎么算出来的。7.2 可视化大屏的布局思路我在分析页面做了一版类似大屏的布局整体采用三段式顶部是总览指标卡片中间是区域均价柱状图加户型占比饼图底部是面积与单价散点图加楼层价格分布箱线图。这个布局把宏观概览、区域对比、微观分布三个层次的信息组织在一起观看者在十秒内就能形成对整体数据的基本认知。每个图表背后都要有一个明确的业务问题。区域均价柱状图回答的是哪个区域最贵户型占比饼图回答的是市场主流户型是什么面积与单价散点图回答的是面积增大时单价如何变化——实际数据呈现的是单价随面积增大先涨后跌的曲线这是因为大户型通常位于远离市中心的新建区域单价反而不如核心区紧凑型房源。这些洞察在答辩时反而比模型本身更能打动老师因为数据分析类毕设的核心是从数据中发现规律预测模型只是规律的应用形式之一。可视化页面还加入了一个交互细节当鼠标悬停在散点图的某个点上时tooltip 会显示该房源的小区名称、户型、面积和总价。这个功能只需要在 ECharts 的 series 数据里附带额外字段前端 tooltip 格式化函数按需取用即可。它虽然实现简单但让图表从静态统计图升级为支持探索的数据视图用户体验提升非常明显。8. 部署与答辩常见问题排查和展示要点8.1 本地环境与依赖管理毕设系统上线展示前我强烈建议准备一个干净的运行环境步骤说明确保答辩当天换一台电脑也能快速跑起来。我的依赖清单写在requirements.txt中核心依赖及版本如下flask3.0.0 pandas2.1.4 numpy1.26.2 scikit-learn1.4.0 requests2.31.0 lxml5.1.0 joblib1.3.2版本锁定非常重要。scikit-learn 在不同小版本之间可能存在行为差异模型文件和版本不匹配时 joblib 加载会报错。如果你在一台机器上训练模型、另一台机器上运行 Flask务必保证两边的 scikit-learn 版本完全一致。我在实际调试点中遇到过这个坑训练时用的是 1.4.0答辩电脑上是 1.2.2模型加载直接抛ModuleNotFoundError排查了半天才知道是版本兼容问题。前端静态文件ECharts、自定义 CSS放在static目录下模板放在templates目录下这是 Flask 默认的目录约定不要改动。如果你把模板文件放错位置运行时会报 TemplateNotFound 错误且错误信息不够直观新手很容易在这个低级问题上浪费大量时间。8.2 答辩演示的完整路径设计答辩演示千万不要一上来就点开网页乱翻。我设计的演示路径是先讲数据来源打开数据库或 CSV 文件展示原始采集数据说明字段含义和样本量再打开数据分析页面用图表快速展示三个核心发现接着演示预测功能输入一组有代表性的房源参数我现场输入的是某热门区域一套 89 平、两室一厅的中楼层房源展示预测结果和置信区间最后回到代码层面展示爬虫代码和模型训练代码的关键片段说明技术实现。有一个演示细节值得特别强调预测输入要提前准备两个测试用例一个正常用例、一个极端用例。正常用例用来展示系统的稳定输出极端用例比如输入 2000 平的别墅用来展示数据校验的容错能力——系统会友好地提示面积超出合理范围而不是抛出异常页面。这两个用例组合演示既能展示功能完整又能展示工程质量。8.3 项目实施中的实际踩坑记录最后把我在整个开发过程中踩过的几个典型坑集中整理一下这些都是官方文档里看不到的实战经验。第一个坑是爬虫采集时被重定向到验证页。房产平台的验证机制有时不返回 403而是返回一个 200 状态码的验证页面如果只按状态码判断请求是否成功就会采集到大量验证页的 HTML解析结果为空。后来我改成双重判断状态码是 200 且页面内容包含预期的房源标题节点才认为成功这个判断逻辑挽救了后续好几个小时的数据采集。第二个坑是数据清洗时把字符串当成数值参与计算。pandas 读取 CSV 后如果某列含有一个无法转换为数值的脏数据整列会被推断为 object 类型后续mean()运算会静默报错或直接返回 NaN。排查方式是打印data.dtypes检查每列类型这是所有 pandas 数据清洗问题排查的第一步。第三个坑是 Flask 的 debug 模式在答辩现场崩溃。debugTrue 虽然方便开发时自动重载但一旦运行时代码有未捕获异常页面会显示一整屏的调试信息非常影响展示效果。答辩前一定要把debug改为False并运行一遍全流程测试确认没有未捕获异常才能安心。第四个坑是模型预测结果的单位问题。爬虫抓到的总价单位是万内部计算也用万作为单位但展示时如果未做换算用户输入面积和单价后得到的总价可能让人困惑。我在后端统一以万元为单位前端格式化时转换为万元展示保证端到端单位一致。总体上这套系统的开发周期大致是这样分布的爬虫和数据清洗占四成时间特征工程和模型调参占三成Flask 和可视化占两成部署和测试占一成。很多人一上来就急着写模型结果发现数据质量太差模型怎么训练都无法收敛只好回头补数据——项目管理上的合理顺序是先解决数据问题再谈模型效果。做完这个项目最大的体会是一个合格的毕设系统不是功能的堆砌而是每个模块都能讲清楚为什么这样做。爬虫为什么用 requests 而不是 Scrapy模型为什么选随机森林而不是线性回归特征为什么用目标编码而不是独热编码这些为什么才是答辩时真正拉开差距的地方。如果未来你想继续扩展这个项目可以尝试的方向包括引入时间序列分析预测房价走势、追加更多城市的对比分析、或者用深度学习模型替代随机森林做房价特征提取。无论往哪个方向走现有的数据链路和架构都能平滑扩展不会出现推倒重来的情况。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑