先说一下这个项目做到最后是什么状态:一个能直接演示的Web系统,用户登录后能看到推荐给他的房源列表,每套房源旁边标注着预测租金区间,首页有全国主要城市租赁市场的可视化大屏,里面包含租金热力地图、户型占比饼图、租金走势折线图、区域均价排行Top10。答辩现场打开浏览器跑一遍全流程,从推荐到预测再到可视化,每个模块的代码都能讲清楚来龙去脉,这才叫把"基于Python租房推荐系统"这个题目真正做透了。
很多人看到"租房推荐系统"就觉得是电商推荐系统的换皮,其实差别非常大。电商推荐的数据非常稠密,用户会频繁点击、加购、下单,行为日志一抓一大把。而租房是一个典型的低频高客单价场景,用户可能一年只租一次房,浏览行为稀疏到令人发指,这意味着你不能照搬MovieLens电影推荐那一套。也正因为如此,这个题目对毕业设计来说含金量很高——它逼着你去解决冷启动、数据稀疏、混合推荐、多特征融合这些实际问题,而不是调一个现成的库跑个曲线就完事。
1. 课题拆解:这个标题下到底藏着几个系统
1.1 一个标题里的五个核心模块
"基于Python租房推荐系统"这个题目,信息量比表面看起来大得多。我把它拆成了五个模块来设计:
- 数据采集模块:爬取租房平台的房源数据(租金、面积、户型、朝向、楼层、小区名、经纬度、配套设施)以及用户的浏览、收藏、预约看房行为数据。
- 推荐模块:协同过滤推荐算法,包括基于用户的UserCF和基于物品的ItemCF,最终以混合推荐的形式对外提供服务。
- 预测模块:租金预测算法,基于房源特征预测该房源的市场租金区间,辅助用户判断性价比。
- 可视化模块:租房数据的可视化大屏,用图表展示城市租金分布、区域均价、户型结构、价格走势等。
- Web应用模块:将以上所有能力整合到一个可交互的Web系统中,用户能注册登录、浏览房源、获取推荐结果、查看可视化面板。
这五个模块不是互相独立的。推荐模块需要用户行为数据的支撑,预测模块需要房源特征数据的支撑,可视化模块需要前面所有模块的数据沉淀。把它们串起来,才是一个能让答辩老师眼前一亮的完整作品。
1.2 租房推荐场景与电商推荐的核心差异
我在动手写代码之前,最先做的一件事不是装环境,而是想明白租房场景和经典电商推荐的区别。这里直接说结论:
传统电商推荐系统面临的是"人和商品"的高频交互,用户的行为数据是海量的。而租房平台里,一个用户一天可能只会浏览几十套房源,收藏几套,真正约看房的更少。这个数据稀疏度直接决定了算法的选型和评估方式。
如果照抄MovieLens案例用UserCF,你会发现用户之间的共同评分房源太少,相似度矩阵接近全零。这也是很多网上教程跑不通的根本原因。租房推荐系统必须接受"数据稀疏"这个设定,然后想办法在稀疏数据上做出合理推荐,这恰恰是毕业设计里最能体现算法功力的地方。
核心思路:主推ItemCF(基于物品的协同过滤),用房源本身的内容特征做相似度补充,再用规则策略做冷启动兜底。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与数据底座:动工之前先解决这三个问题
2.1 主流技术栈的对比与选择
这个项目不推荐一上来就搭微服务,毕业设计的核心是"模块完整、逻辑清晰、能演示、能讲清楚"。我最终选用的技术栈如下:
| 层次 | 技术选型 | 选择理由 |
|---|---|---|
| 开发语言 | Python 3.9 | 生态完善,爬虫、数据分析、机器学习、可视化一站式解决 |
| Web框架 | Flask 2.x | 轻量,适合单机部署,模板渲染方便,比FastAPI更适合新手理解 |
| 数据库 | MySQL 8.0 + Redis | MySQL存结构化业务数据,Redis缓存推荐结果和热门房源 |
| 算法库 | scikit-learn + surprise | surprise自带多种推荐算法实现,便于对比实验;但核心算法建议手动实现 |
| 可视化 | ECharts + pyecharts | 中文文档友好,地图、散点、热力图、词云都有现成组件 |
| 前端 | Bootstrap + jQuery | 不引入复杂前端框架,降低毕设项目的上手门槛 |
| 爬虫 | requests + BeautifulSoup4 | 单机爬取万级数据量完全够用,不需要Scrapy的分布式能力 |
有同学可能会问,为什么不直接用Django?Django的功能更全面,但对单机毕业设计来说太重了,而且Flask的路由和请求处理逻辑更直观,答辩时讲代码更容易讲明白。前端也不用Vue或React,一方面是要避免前后端分离带来额外的联调成本,另一方面是毕业设计的评分重点在算法和数据分析,而不是前端工程化。
2.2 数据库建模:从房源到用户行为的四张核心表
数据表设计直接决定了后面所有算法的实现难度。我踩过一个坑:一开始把用户评分设计成单独一张评分表,结果发现用户根本没有评分行为,后来改成了"隐式反馈"建模。最终的核心表结构如下:
- 房源表(house):房源ID、城市、区域、小区名、租金、面积、户型(几室几厅)、朝向、楼层、装修情况、经度、纬度、出租方式(整租/合租)、发布时间、配套设施标签、房源描述文本。
- 用户表(user):用户ID、用户名、密码(MD5加盐存储)、注册时间、常驻城市、预算上限、偏好的户型。
- 行为日志表(behavior):行为ID、用户ID、房源ID、行为类型(浏览/收藏/约看)、行为时间。这是推荐算法最核心的输入。
- 预测结果表(prediction):房源ID、预测租金、置信区间下界、置信区间上界、模型版本。租金预测模块的输出会固化到这里,避免每次展示都重新跑模型。
我在行为日志表上专门加了索引(user_id + house_id + behavior_type),因为协同过滤算法需要频繁按用户和房源维度聚合行为数据,没有索引的情况下数据量到两万条以上时,聚合查询会变得非常慢。
另外,推荐结果不要每次实时计算,把Top-N推荐结果存到Redis里,key设计成rec:user:{user_id}:{strategy},value用JSON序列化存储,过期时间设为一小时。这样页面加载推荐列表时直接读缓存,响应时间能压到200毫秒以内。
2.3 数据采集与清洗:爬虫跑完只是开始
数据源我用了链家和贝壳的公开房源页面,爬取的目标是北京、上海、广州、深圳、杭州、成都六个城市的租房信息。单机爬虫加随机延时(每次请求间隔2至4秒),一天能采集一万条左右,完全够用。
真正花时间的是数据清洗。原始爬下来的字段乱象丛生:
- 租金字段是"8500元/月"这种带单位的字符串,需要正则提取并转成int。
- 面积字段是"65.23㎡",同样需要清洗。
- 户型字段"2室1厅1卫"需要拆成室、厅、卫三个独立字段。
- 经纬度有时候爬到的是小区中心点,有时候是空的,空的要用小区名调高德地图API补齐。
- 部分房源描述里带"急租"、"首次出租"这类营销词,清洗时保留,因为它们对租金预测是有用的文本特征。
清洗后的数据质量直接决定了预测模型的上限。我建议每个字段都做一遍缺失值统计和异常值筛查,比如租金小于500元/月或面积大于500㎡的房源基本是异常值,可以直接剔除。
3. 协同过滤推荐算法落地:从相似度计算到评分预测
3.1 为什么主推ItemCF而不是UserCF
前面说过,租房行为数据极其稀疏。我爬下来加模拟生成的用户行为数据共约5万条,用户数量3000人,房源数量8000套。理论上用户-房源交互矩阵是3000×8000,非零元素只有5万,稀疏率高达99.8%。
在这样一个矩阵上用UserCF,算出来的用户相似度几乎没有参考价值——两个用户共同浏览过的房源可能只有一两套,余弦相似度要么为0,要么被极少量的共同行为主导,推荐结果噪音非常大。
ItemCF的核心逻辑是"喜欢这个房源的人,可能也喜欢跟它相似的房源"。对同一房源来说,被哪些用户收藏过,这个"物品画像"相对稳定,即使在稀疏矩阵上,房源间的共性也能保持一定的统计意义。所以我的主推荐策略采用了ItemCF,同时用房源特征相似度(建筑面积、户型、租金区间、区域)作为补充。
3.2 相似度计算与评分预测的代码实现
推荐模块的实现我建议完全手动编码,不要直接用surprise库的KNNBasic跑完就交差。手动实现一遍才能真正讲清楚算法的每一步,答辩的时候老师一旦追问细节,你心里才有底。
核心代码如下:
python复制import numpy as np
import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity
# 构造用户-房源隐式反馈矩阵
# behavior_type: 浏览=1, 收藏=3, 预约看房=5
df = pd.read_sql("SELECT user_id, house_id, weight FROM behavior_with_weight", engine)
pivot = df.pivot_table(index='user_id', columns='house_id', values='weight', fill_value=0)
house_matrix = pivot.T # 转置为房源-用户矩阵
# 计算房源之间的余弦相似度
house_sim = cosine_similarity(house_matrix)
house_sim_df = pd.DataFrame(
house_sim,
index=house_matrix.index,
columns=house_matrix.index
)
def itemcf_recommend(user_id, top_n=10):
# 获取该用户产生过行为的房源列表
user_rated = pivot.loc[user_id]
user_rated_items = user_rated[user_rated > 0].index.tolist()
if not user_rated_items:
return [] # 走冷启动策略
# 加权累加相似房源的得分
scores = {}
for item in user_rated_items:
sim_scores = house_sim_df[item]
for candidate, sim in sim_scores.items():
if candidate in user_rated_items:
continue # 过滤掉已看过的房源
scores[candidate] = scores.get(candidate, 0) + sim * user_rated[item]
# 按得分排序返回TopN
ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n]
return [house_id for house_id, _ in ranked]
有几个细节容易踩坑,单独提出来说:
- 行为加权:不要把所有行为都当成同样的权重。浏览、收藏、预约看房,这三种行为的意图强度完全不同,直接给浏览记1分、收藏记3分、预约看房记5分,推荐效果会显著提升。
- 相似度归一化:ItemCF算出来的原始相似度只用于排序,但不同房源被点击的次数差异很大,热门房源容易被过度推荐。可以加一个惩罚系数
1 / log(1 + house_click_count),降低热门房源的权重。 - 向量化加速:虽然示例代码用的双重循环,但实际跑的时候我推荐用矩阵运算,否则8000套房源的两两计算会非常慢。
3.3 冷启动问题的三层兜底策略
协同过滤最怕的场景是:新注册用户没有任何行为数据,或者新上架房源没有被任何人浏览过。针对这两种情况,我设计了三级兜底策略:
第一层,如果是新用户,完全跳过协同过滤,直接走"城市+预算+户型偏好"的规则推荐。用户注册时填了常驻城市和预算上限,就用这些条件过滤房源,然后按"热度分"排序。热度分定义为0.4*近7天浏览次数 + 0.3*收藏次数 + 0.3*预约次数,这个分数能快速给用户一组合理的初始房源。
第二层,如果用户有少量行为(少于3次),不急着跑ItemCF,而是把行为过的房源的相似房源直接推给用户,同时混入该城市的热门房源,保证推荐列表不过于单一。
第三层,如果用户行为达到一定阈值,才启用完整的ItemCF + 热门惩罚策略。
这三层策略对应到代码里就是一个策略路由函数,根据用户行为量动态决定走哪条推荐逻辑。这个设计在答辩时可以好好讲一讲,它体现的是对推荐系统实际工程问题的理解,而不是只会调库。
4. 租金预测算法:特征工程比模型本身更决定上限
4.1 特征工程:把中文地址和户型描述变成数值特征
租金预测这个模块,很多人直接拿"面积、户型、朝向"这几个字段喂给线性回归就完事,结果R²只有0.3,然后拼命调模型调参,效果还是上不去。问题几乎都出在特征工程上。
我最终用的特征分成了五组:
- 数值型特征:建筑面积、所在楼层、总楼层、房龄(如果有)、距最近地铁站步行距离。
- 分类型特征:城市、区域、朝向、装修情况、出租方式、所在环线位置(如果在一线城市)。这些用独热编码(One-Hot Encoding)处理。
- 文本型特征:房源描述文本,先做jieba分词去掉停用词,然后用TF-IDF向量化,保留Top 200维特征。里面"地铁""精装""朝南""拎包入住"这类词对租金预测非常有效。
- 聚合特征:小区内所有房源的平均租金、区域平均租金、房源租金与区域均价的比值。这类聚合特征能反映房源在所属区域内的相对水平,对模型提升非常明显。
- 时间特征:发布时间距今天的间隔天数。毕业设计的数据是静态采集的,但带上这个特征可以反映"新上架房源"的定价策略。
特征都准备好之后,再用train_test_split按8:2切分训练集和测试集,注意一定要按房源ID做随机切分,不要按城市切分,否则模型没有见过某些城市的数据,测试效果会很差。
4.2 三个模型横向对比:线性回归、随机森林与XGBoost
我分别跑了线性回归、随机森林和XGBoost三个模型,对比结果如下:
| 模型 | RMSE(元) | MAE(元) | R² | 训练耗时 |
|---|---|---|---|---|
| 线性回归 | 1532 | 1187 | 0.51 | <1s |
| 随机森林(100棵树) | 1246 | 926 | 0.63 | 9.3s |
| XGBoost(默认参数+早停) | 1021 | 763 | 0.71 | 16.8s |
真实租金范围集中在2000到15000元,RMSE能压到1000元左右,已经算不错了。随机森林和XGBoost的差距主要来自R²,实际使用中随机森林的稳定性也够用了。我建议毕设项目中两个模型都保留,用XGBoost作为主模型,随机森林作为对比模型放进实验分析章节,这样论文里的对比实验部分会丰富很多。
XGBoost的核心代码不复杂:
python复制import xgboost as xgb
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
model = xgb.XGBRegressor(
n_estimators=500,
max_depth=6,
learning_rate=0.05,
subsample=0.8,
colsample_bytree=0.8,
reg_lambda=1.0 # L2正则,防止过拟合
)
model.fit(
X_train, y_train,
eval_set=[(X_test, y_test)],
early_stopping_rounds=20,
verbose=False
)
reg_lambda这个参数要特别说一句,它对租金的数值型预测很友好,能大幅减少极端值的干扰。我一开始没加正则,测试集上偶尔会出现预测租金为负数的荒唐结果,加了L2正则之后就再也没出现过了。
4.3 预测结果如何反哺推荐系统
推荐系统和预测模块不是各干各的,它们应该联动。我在最终项目里做了这样一个联动:推荐列表里的每套房源,都会同时显示ItemCF的推荐得分和预测租金的对比。具体来说,当ItemCF推荐出来一套房源,接口会同时查询该房源的预测租金,并标记"预测租金低于同区域平均价8%以上"的房源性价比较高,在页面上打一个标签。
这样设计的好处是,推荐结果不只是"你可能喜欢这套房",而是"你可能喜欢这套房,而且它的租金低于同区域平均水平"。这在答辩时很容易讲出一个完整的业务故事。
5. 可视化大屏:从ECharts配置到交互联调的细节复盘
5.1 图表选型和布局思路
可视化大屏是每个到访评委都会第一眼看到的东西,它的完成度直接决定了第一印象。我采用的是经典的管理驾驶舱布局:顶部是标题栏,中间的主要区域给地图,左右两侧放统计图表。
最终选用的图表组件如下:
- 地图散点图(中国地图):展示六个目标城市的房源分布和平均租金,颜色越深代表租金越高。
- 热力图(城市级):点击某个城市后,城市内各区域的租赁热度热力图。
- 环形图(户型占比):各户型数量的占比情况,鼠标悬停显示具体数值。
- 折线图(租金走势):近一年六个城市的平均租金走势。
- 柱状图(区域均价Top10):每个城市租金最高的前10个区域。
- 词云(房源标签):房源描述中"地铁""精装""朝南"等关键词的频次可视化。
我个人体会是,大屏最忌讳一屏塞满十几个图表,信息过载会显得很杂乱。6个图表刚好合适,既覆盖了主要分析维度,又保证了视觉效果。
5.2 地图散点与热力图的实现细节
地图部分是可视化模块的难点,我用的是pyecharts配合全国地图GeoJSON。核心配置大致是这样的:
python复制from pyecharts import options as opts
from pyecharts.charts import Geo
from pyecharts.globals import ChartType, SymbolType
geo = Geo(init_opts=opts.InitOpts(width="100%", height="600px", bg_color="transparent"))
geo.add_schema(maptype="china", itemstyle_opts=opts.ItemStyleOpts(color="#1E2B3A", border_color="#4A6B8A"))
# 添加城市平均租金数据
geo.add(
"平均租金",
city_rent_list,
type_=ChartType.EFFECT_SCATTER,
symbol_size=8,
)
# 添加区域热力图
geo.add(
"区域热度",
region_heat_list,
type_=ChartType.HEATMAP,
blur_size=30,
)
这块有两个容易踩坑的地方。第一,pyecharts的地图数据格式要求经纬度匹配到市级,如果直接传区级名称,散点可能显示不出来。第二,city_rent_list必须是[(城市名, 数值), ...]的列表格式,字典格式在部分版本里会静默失效,一点报错都没有,就是图上没数据,排查起来非常恼火。
5.3 大屏与后端的数据联调
可视化大屏的数据不能是死的,我把它做成了动态接口,后端提供/api/visualization/overview、/api/visualization/city/{city}等API,前端页面加载时用Ajax请求数据并渲染图表。数据来源是MySQL中的房源表,通过SQL聚合查询得到。
这里有个性能问题值得专门说一下:六个图表如果每个都独立请求一次,页面首屏会有明显等待。我的做法是用一个聚合接口,一次性返回所有图表需要的数据,前端拿到JSON后分发给对应的图表对象。这样首屏接口请求数从6次降为1次,配合后端的Redis缓存,大屏的加载时间能控制在1秒内。
另外一个容易被忽视的细节是,大屏页面要全屏自适应。我的做法是CSS里采用rem单位加媒体查询,图表尺寸全部设置成百分比,保证不同分辨率的投屏都能正常显示。
6. 深度踩坑记录:数据稀疏、中文分词与模型过拟合的完整排查链路
6.1 推荐结果全为空:从评分矩阵到相似度计算的逐层排查
这个Bug困扰了我整整两天。现象是:某几个用户登录后,推荐列表一直是空的,而其他用户正常。直接看代码逻辑,itemcf_recommend函数理论上不会返回空列表,除非传入的user_id没有任何行为记录。但排查后发现,出问题的用户明明有浏览记录。
排查链路是这样的:
第一步,检查行为日志,发现该用户有20条浏览记录,数据没问题。第二步,检查评分矩阵构建,发现pivot表中该用户对应的行确实有非零值,矩阵构建没问题。第三步,检查相似度计算,发现house_sim_df[item]返回的是全零序列。
问题找到了:这些用户浏览过的房源,全部是只有个位数用户看过的"长尾房源",这些房源与其他房源之间的共同用户数为0,所以余弦相似度全是0,加权累加之后得分还是0,排序结果自然为空。
解决办法是在相似度计算中加入"物品内容相似度"作为兜底:当协同过滤相似度为0时,用房源的内容特征(面积、户型、租金区间、区域)计算一个内容相似度作为补充。这样即使两个房源没有被同一用户浏览过,只要它们的特征相似,也能产生推荐关系。
这个排查过程我建议完整写进论文的"系统测试与问题分析"章节,它真实地反映了推荐系统冷启动和稀疏性问题在实际中的表现,比纸上谈兵地讲原理要有说服力得多。
6.2 中文地址分词不准导致地图散点飘到海里
地图可视化的另一个经典问题:房源经纬度来自爬虫,但部分房源只有地址文本没有经纬度。我用高德地图地理编码API补全时,地址格式是"北京市朝阳区望京街道望京SOHO T1",直接请求高德API能返回准确的经纬度,但问题是高德API每日免费配额只有有限次数,六千条地址要跑很久。
后来换了个思路,先按小区名聚合,只对唯一的小区名调用地理编码API,然后把经纬度批量回填到同一小区的所有房源。这样唯一小区名数量降到一千多个,API配额完全够用。
真正让我头疼的是另一个问题:列表页的地址在存入数据库时,部分老数据的小区名带上了"()"全角括号或者空格,比如"朝阳区望京SOHO(T1)",高德API匹配失败后返回经纬度为0,导致这些散点被绘制到了地图左上角的经纬度(0,0)位置,视觉上就是"散点飘到了海里"。
排查过程:先在数据库里查经纬度为0的记录,发现全部是匹配失败的数据。进一步观察,发现失败地址几乎都包含全角括号,用正则把全角括号替换成半角、多余空格去掉之后,重新跑地理编码,匹配成功率从82%提升到了97%。剩下3%的地址确实无法解析,最终用该区域的中心点坐标做了兜底。
6.3 租金预测在验证集上表现不错,新数据却偏差很大
这个问题的本质是数据泄漏。我一开始做特征工程时,把"小区平均租金"直接作为特征放入训练集。当时跑出来的测试集R²有0.85,我非常高兴,结果拿爬虫新抓的房源做验证时,预测误差大得离谱。
排查链路是这样的:先怀疑是模型过拟合,加了正则、调小了树深度,没用。后来逐步去掉特征做消融实验,发现去掉"小区平均租金"这个特征后,测试集R²从0.85掉到0.71,但新数据验证误差反而大幅下降,这才意识到问题出在数据泄漏上。
小区平均租金是用训练集数据算出来的聚合统计量,它本身包含了目标变量(租金)的信息。测试集和训练集来自同一批小区,所以测试时会表现得很好,但新数据来自没有出现在训练集中的小区,特征就算不出来,或者算出来也是错的。
解决办法是:聚合特征必须基于全量数据一次性计算,而不是基于训练集单独计算。换句话说,聚合统计量只作为房源自身的一个属性,不参与训练/测试的划分。这种做法虽然在测试集上的R²没有之前那么高,但模型在新数据上的表现才是真实的。
这个坑在各类数据竞赛和毕业设计中非常常见,答辩时如果能主动讲出"我发现了数据泄漏问题并修正了它",会是一个很大的加分项。
写在最后:把项目做成作品,而不是交作业
如果你准备用这个题目做毕业设计,我最后想分享三点个人体会。
第一,系统的完整性比单点的技术深度更重要。一个推荐效果不是最优但全链路跑通、每个模块都有交互、有数据、有可视化展示的系统,在答辩时远比一个只写了推荐算法但没有任何工程落地的项目更受欢迎。
第二,文档和代码的习惯要提前养成。我强烈建议从一开始就给每个模块建立独立的README文件,记录模块功能、依赖、启动方式、已知问题,这样写论文时很多内容可以直接复用,不用回头翻代码回忆当时是怎么实现的。
第三,答辩前一定要做三轮全流程演示测试。第一轮测试全功能,第二轮测试数据量大的情况,第三轮测试断网、数据库未启动等意外情况。我答辩前一天发现可视化接口在数据量超过两万条时会偶尔返回超时,紧急加了一层Redis缓存才解决。这种临场问题如果能提前发现,会少掉很多不必要的紧张。
租房推荐系统的技术栈跨度很大,从爬虫、数据库、协同过滤、机器学习预测到前端可视化都有涉及,好好做完一个项目,你的数据处理能力和工程落地能力都会有一次实打实的提升。
