news 2026/9/15 6:22:05

Python智能旅游推荐系统实战:协同过滤与Flask完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python智能旅游推荐系统实战:协同过滤与Flask完整实现

简介:基于Python的智能旅游推荐系统毕业设计资料包,面向计算机专业学生、Python开发者和旅游平台研发人员,提供从协同过滤等推荐算法、数据库设计到前后端工程实现的完整参考,可直接用于毕业设计或课程实训。压缩包共800个文件、约24.55MB,含45个Python源码与40个pyc编译文件、53个Vue组件、53个HTML页面及配套CSS/JS,另有SQL数据库脚本、论文docx和配置txt等说明文档,文件类型覆盖后端算法、前端界面、数据脚本与文档素材,便于按目录整体还原项目。目前已有224人学习下载。配套毕业论文覆盖系统设计、技术选型与实验验证,详细阐述数据采集、特征处理、推荐算法选型及评估指标;说明文档给出环境配置、依赖库清单和常见排错思路,内置安装、运行、构建批处理脚本可快速启动;模块化设计便于二次扩展,前端页面和可视化图表也适合答辩演示。

1. 智能旅游推荐系统翻车,多半不是挂在算法上

毕业设计选「Python + 智能旅游推荐系统」这个组合的同学,十有八九一开始都在研究协同过滤和相似度算法,总觉得把推荐算法写得越复杂越能体现水平。等真正开始拼装项目,才发现最大的坑根本不是算法,而是数据从哪来、格式怎么对齐、评分矩阵怎么构建。我见过太多人卡在 pandas 处理一张几千行的 csv 上,一卡就是两天,最后草草用随机数填充交差。这篇就按毕设实际交付的顺序来拆:数据准备、协同过滤实现、Flask 接口落地、效果验证四件事,串成一条从零到能答辩的完整链路。适合正在做选题的信息类、管理类学生,也适合想快速搭一个推荐 Demo 验证想法的工程师照着抄。

2. 智能旅游推荐系统的数据准备:从原始表到评分矩阵

2.1 毕设数据集选型:公开数据、模拟数据还是爬虫

智能旅游推荐系统的数据,业内公开的惯例做法是模仿 MovieLens 那套三表结构:用户表(users)、物品表(attractions)、评分表(ratings)。这个结构的好处是几乎所有推荐算法代码都能直接套用——评分表维护 user_id、item_id、rating 三个核心字段,算法层不关心景点长什么样,只关心编号。很多毕设源码里已经带了 CSV 格式的数据文件,但如果你拿到的数据或者自己造的数据不规范,后面全盘皆输。

关于数据来源,常见有三条路。第一是找公开数据集换肤,把电影的 item_id 映射成景点,评分字段不变,这是成本最低的方案,缺点是答辩时容易被问到「你的数据是怎么采集的」。第二是写脚本模拟生成用户和评分,适合你明确知道业务想表达什么场景(比如城市周边游、亲子游、老年团),我一般建议用这种方式保底。第三是爬虫,只采集公开可访问的数据,注意频次和内容边界,毕设场景下适度使用即可。

2.2 用 pandas 把三张原始表拼成评分矩阵

无论数据来自哪条路,最终都要把三张表喂给推荐算法。推荐算法不认文本描述,只认矩阵,所以核心转换动作就是构建「用户-景点评分矩阵」,行是用户,列是景点,单元格是评分。下面这段代码是用 pandas 实现转换的标准写法,也是毕设里复用率最高的片段。

import pandas as pd # 读三张原始表,编码用 utf-8 指定以防乱码 users = pd.read_csv('users.csv', encoding='utf-8') attractions = pd.read_csv('attractions.csv', encoding='utf-8') ratings = pd.read_csv('ratings.csv', encoding='utf-8') # 合并成一张宽表,便于查看和调试 merged = ratings.merge(users, on='user_id').merge(attractions, on='item_id') # 用户-景点评分矩阵:空值即该用户未评分 rating_matrix = ratings.pivot_table( index='user_id', columns='item_id', values='rating' ) # 把评分统一成 1-5 的整数,去掉异常值 rating_matrix = rating_matrix.applymap(lambda x: int(x) if pd.notna(x) else x) # 稀疏度检查:矩阵中非空占比 sparsity = 1 - (rating_matrix.notna().sum().sum() / rating_matrix.size) print(f'评分矩阵形状: {rating_matrix.shape},稀疏度: {sparsity:.2%}')

逻辑说明:pivot_table是这里的主心骨,values='rating'指定用评分列填充交叉格,某个用户没评过某个景点就留下 NaN,这正是协同过滤需要处理的缺失值。注意稀疏度打印出来大概率是 90% 以上,这不是数据坏了,而是正常现象——一个用户怎么可能玩过几百个景点。后面做相似度计算时,这些 NaN 会被过滤掉而不是当成 0 分。

2.3 评分稀疏对后续算法的影响

矩阵太稀疏,直接后果是相似度计算结果不稳定。假设用户 A 和用户 B 共同评过的景点只有 1 个,算出来的相似度就算接近 1,可信度也很低。毕设里应对稀疏的常见做法有两个:一是设定「共同评分数量下限」,比如至少共同评过 3 个景点才计算相似度,否则记为 0;二是在算相似度时用「加权皮尔逊系数」给共同评分少的用户对降权。

稀疏度区间常见影响处理方式
80% - 90%相似度噪声大,推荐结果波动明显设置共同评分下限;用调整余弦相似度
90% - 95%冷启动用户占比高,部分景点无评分引入基于内容推荐做兜底(第 4 章展开)
95% 以上矩阵几乎不可用,协同过滤失效考虑纯基于内容的推荐或矩阵分解降维

这里给一个判断标准:如果稀疏度超过 95%,协同过滤的预测结果已经不具备参考性,这时候应该调整策略,别硬算。

3. Python 实现协同过滤:相似度计算与 Top-N 排序

3.1 毕设推荐算法选型:为什么优先做基于物品的协同过滤

协同过滤分两大类:基于用户的(User-Based)和基于物品的(Item-Based)。旅游推荐这个场景,我一般建议优先做基于物品的协同过滤。原因有两个:第一,景点的数量通常远小于用户数量,物品相似度矩阵可以提前算好存起来,推荐时直接查表,响应速度快。第二,用户的兴趣会随出行目的变化,但景点与景点的属性关系(比如「故宫」和「颐和园」都是历史人文类)相对稳定。

吉利斯定理说「用户喜欢与他历史偏好相似的物品」,基于物品的协同过滤就是抓住「物品之间的相似度」来推荐:用户给「故宫」打了 5 分,系统找出于故宫最相似的 3 个景点推荐给用户。这套逻辑在毕设答辩时也容易讲清楚,不需要铺垫复杂的数学背景。

3.2 余弦相似度与皮尔逊相关系数的 Python 实现

实际做毕设时,面对评分矩阵算相似度,最常用的不是直接套 sklearn,而是手写一版,因为手写才能应对 NaN。下面这段代码实现的是「调整余弦相似度」(Adjusted Cosine),核心思想是先对每个用户的评分做均值中心化,再算两个物品在共同被评过的用户上的相似度。

import numpy as np def adjusted_cosine_similarity(matrix, item_a, item_b): # 提取两个物品的评分列 col_a = matrix[item_a] col_b = matrix[item_b] # 只取两者都有评分的用户行 common = ~(col_a.isna() | col_b.isna()) if common.sum() < 3: return 0.0 vec_a = col_a[common].values.astype(float) vec_b = col_b[common].values.astype(float) if vec_a.std() == 0 or vec_b.std() == 0: return 0.0 # 皮尔逊相关系数:衡量两个向量线性相关程度 corr = np.corrcoef(vec_a, vec_b)[0, 1] return round(corr, 4) # 构建物品相似度矩阵(只算评分矩阵中存在的物品) items = rating_matrix.columns sim_matrix = pd.DataFrame(0.0, index=items, columns=items) for i in items: for j in items: if i >= j: continue sim = adjusted_cosine_similarity(rating_matrix, i, j) sim_matrix.loc[i, j] = sim sim_matrix.loc[j, i] = sim

逻辑说明:common.sum() < 3是刚说的共同评分数量下限,少于 3 个直接返回 0,避免小样本噪声。np.corrcoef算的是皮尔逊相关系数,它的取值范围是 -1 到 1,越接近 1 说明两个景点获得的评分模式越像。注意外层循环用i >= j跳过重复计算,把计算量减半。这段代码的时间复杂度是 O(n²),景点数量在几百个以内完全能跑,毕设数据量不用担心性能。

3.3 预测评分公式与 Top-N 推荐生成

有了物品相似度矩阵,下一步是预测用户对未评分物品的评分。常见做法是加权求和:取用户已评分的物品,按相似度加权汇总,除以权重绝对值之和,公式为:

P(u, i) = Σ (S(i, j) × R(u, j)) / Σ |S(i, j)|

其中 S(i,j) 是物品 i 和 j 的相似度,R(u,j) 是用户 u 对物品 j 的评分。分母用绝对值是为了避免正负相似度抵消。下面这段代码生成用户的 Top-N 推荐列表。

def recommend_top_n(user_id, rating_matrix, sim_matrix, top_n=5): user_ratings = rating_matrix.loc[user_id] # 用户还没评过分的物品,作为候选推荐池 candidates = user_ratings[user_ratings.isna()].index # 用户已评分的物品及对应分数 rated_items = user_ratings.dropna() scores = {} for cand in candidates: # 候选物品与所有已评分物品的相似度 sims = sim_matrix[cand].loc[rated_items.index] # 只取正相似度,负相似度表示不相关,过滤掉 positive_mask = sims > 0 if positive_mask.sum() == 0: continue numerator = (sims[positive_mask] * rated_items[positive_mask]).sum() denominator = sims[positive_mask].abs().sum() scores[cand] = round(numerator / denominator, 3) if not scores: return pd.Series(dtype=float) # 按预测分从高到低排序,取前 top_n 个 top = pd.Series(scores).sort_values(ascending=False).head(top_n) return top # 调用示例:给用户 1 推荐 5 个景点 recommendations = recommend_top_n(1, rating_matrix, sim_matrix, top_n=5) print(recommendations)

逻辑说明:候选池是用户没有评过分的所有物品,这一步保证了推荐结果不会推荐用户已经去过的景点。positive_mask = sims > 0把负相似的物品剔除掉,只保留有正向参考价值的物品。最终排序用的sort_values(ascending=False)取预测分数最高的前 5 个。这个方法的效果上限取决于相似度矩阵的质量,第 5 章会讲怎么量化验证。

3.4 推荐效果评估:MAE、RMSE 和覆盖率

推荐系统做得怎么样,不能靠肉眼感觉,要量化。毕设论文里最常见的评估指标有三个:MAE(平均绝对误差)、RMSE(均方根误差)和覆盖率。MAE 衡量预测评分与真实评分的平均差距,RMSE 对大误差的惩罚更狠,覆盖率衡量推荐列表覆盖了多少物品。

指标公式含义适用说明合理阈值参考
MAE预测分与真实分绝对差值的平均直观反映预测偏多少分0.8 以下可接受
RMSE差值平方再取均值的平方根大误差敏感,答辩更认这个1.0 以下可接受
覆盖率推荐列表中出现的物品数 / 总物品数反映长尾挖掘能力20% 以上

这三个指标的计算代码将在第 5 章和验证脚本一起给出,因为它们必须跑在同一份测试集上才有意义。现在先把推荐算法的主体逻辑立住,接下来把它接成 Web 服务。

4. 智能旅游推荐系统落地:Flask 接口与冷启动兜底

4.1 Flask 最小推荐接口与 curl 验证

推荐算法在 Jupyter Notebook 里跑通只是第一步,毕设要交付的是一个能打开网页操作的系统。常见做法是用 Flask 包一层 HTTP 接口,前端页面通过 AJAX 调用接口拿推荐结果。如果只做打包,有时候需要考虑加密、完整性校验,行业合规需求下要配置合适的打包工具。下面给出最小可用的 Flask 推荐接口,代码量不长但五脏俱全。

from flask import Flask, jsonify, request import pandas as pd app = Flask(__name__) # 启动时加载数据,避免每次请求都重新读文件 rating_matrix = pd.read_csv('rating_matrix.csv', index_col=0) sim_matrix = pd.read_csv('sim_matrix.csv', index_col=0) @app.route('/recommend', methods=['GET']) def recommend(): user_id = request.args.get('user_id', type=int) top_n = request.args.get('top_n', default=5, type=int) if user_id not in rating_matrix.index: return jsonify({'error': 'user not found'}), 404 # 走协同过滤主流程 result = recommend_top_n(user_id, rating_matrix, sim_matrix, top_n) if result.empty: # 冷启动兜底:基于景点属性的内容推荐 result = content_based_fallback(user_id, top_n) return jsonify({ 'user_id': user_id, 'recommendations': result.index.astype(str).tolist(), 'scores': result.values.tolist() }) if __name__ == '__main__': app.run(debug=True, port=5000)

参数说明:user_id是必传参数,top_n默认 5,type=int会让 Flask 自动做类型转换,传非数字会返回 400 而不是 500,这个小细节答辩时能加分。把rating_matrixsim_matrix放在函数外加载,是因为每次请求都读 CSV 会拖慢响应,而且矩阵属于静态数据,不需要热更新。启动后访问http://127.0.0.1:5000/recommend?user_id=1&top_n=5就能看到 JSON 结果,用 curl 验证更直观:

curl "http://127.0.0.1:5000/recommend?user_id=1&top_n=5"

4.2 冷启动场景:基于景点标签的内容推荐兜底

协同过滤有一个先天缺陷:新用户没有评分记录,无法算相似度。毕设里不能回避这个问题,因为答辩老师必然会问「一个新用户注册后,你给他推荐什么」。常见做法是补一条「基于内容」的推荐路:给每个景点打标签,比如「历史人文」「自然风光」「亲子」「美食街区」,新用户注册时勾选感兴趣的主题,系统按标签匹配景点。

下面给出简化版实现:

def content_based_fallback(user_id, top_n): # 取该用户偏好标签,实际项目中存在用户表里 prefer_tags = ['历史人文', '自然风光'] # 景点表带 tags 字段,多个标签用逗号分隔 attractions = pd.read_csv('attractions.csv', encoding='utf-8') # 计算全景点与用户偏好的标签重合度 attractions['tag_hit'] = attractions['tags'].apply( lambda s: len(set(str(s).split(',')) & set(prefer_tags)) ) # 按命中数排序,取前 top_n top = attractions[attractions['tag_hit'] > 0] top = top.sort_values('tag_hit', ascending=False).head(top_n) top_scores = top['tag_hit'] / len(prefer_tags) return pd.Series(top_scores.values, index=top['item_id'], name='score')

逻辑说明:tag_hit计算的是景点标签与用户偏好标签的交集大小,排序后取前 N 个返回。这里的评分逻辑没有用到真正的评分数据,所以只是兜底方案。实际毕设中建议把这个函数挂在/recommend接口的冷启动分支上,即user_id合法但无评分时触发,不要让用户看到空推荐列表。

4.3 说明文档与论文结构怎么对应源码

毕设交付物里的说明文档和论文,核心不是写代码怎么实现,而是写「系统怎么设计、为什么这么设计」。我在整理自己项目时总结了这几组对应关系。

论文章节对应源码目录关键内容提示
第 3 章 系统设计data/utils/三表结构设计、评分矩阵构建流程、相似度矩阵预计算策略
第 4 章 系统实现recommend/app.py协同过滤函数、Flask 接口定义、冷启动兜底逻辑
第 5 章 系统测试evaluate.pytest/留一法评估、MAE/RMSE 计算、接口响应测试
结论与展望docs/数据稀疏问题、未来可以做的矩阵分解改进

说明文档里建议放一张系统的架构图,将请求流转写成「前端页面 → Flask 路由 → 推荐引擎 → 数据层」四层结构。不要贴全部代码,挑核心函数各贴 10-20 行并配文字说明就够,论文查重时代码一多反而容易被标红。

5. 用留一法验证推荐质量:把 MAE 压到 0.8 的调参技巧

5.1 留一法评估脚本:每次留一个评分当测试集

留一法(Leave-One-Out)是毕设里最容易被认可的评价方案:对每个用户的评分序列,每次留出一个评分当作「未知」,用其余评分去预测它,最后累计所有预测值与真实值的差值。它能最大化利用有限的数据,缺点是计算量大,但毕设的数据量完全撑得住。评估脚本实现如下。

import numpy as np def leave_one_out_evaluate(rating_matrix, sim_matrix): errors = [] coverage_items = set() total_items = len(rating_matrix.columns) for user_id in rating_matrix.index: user_ratings = rating_matrix.loc[user_id].dropna() for item_id, true_rating in user_ratings.items(): # 把当前评分挖掉,用剩余数据重新构建该用户的预测 reduced = rating_matrix.loc[user_id].drop(index=item_id) # 若剩余评分不足(比如少于3个),跳过该样本 if len(reduced) < 3: continue # 复用预测逻辑:计算带预测分 sims = sim_matrix[item_id].loc[reduced.index] positive_mask = sims > 0 if positive_mask.sum() < 1: continue numerator = (sims[positive_mask] * reduced[positive_mask]).sum() denominator = sims[positive_mask].abs().sum() pred = numerator / denominator errors.append(abs(pred - true_rating)) coverage_items.add(item_id) mae = np.mean(errors) if errors else 999 rmse = np.sqrt(np.mean(np.square(errors))) if errors else 999 coverage = len(coverage_items) / total_items return {'MAE': round(mae, 3), 'RMSE': round(rmse, 3), 'coverage': round(coverage, 3)} # 执行评估 metrics = leave_one_out_evaluate(rating_matrix, sim_matrix) print(metrics)

逻辑说明:drop(index=item_id)模拟了「用户还没去过这个景点」的状态,再走一遍和第 3 章相同的加权求和逻辑,拿到预测值后与真实值true_rating做差值。coverage_items用来累计至少被成功预测过一次的物品 ID,覆盖率能直接反映推荐结果有没有卡在少数头部景点上。跑完如果 MAE 在 0.8 以下,说明系统的预测精度基本过关。

5.2 参数调试技巧:k 值与相似度阈值的配合

留一法跑出来的指标不是一次就合格的,需要调参。协同过滤两个直接影响结果的参数是「相似度阈值」(第 2 章提到的共同评分下限)和「最终推荐条数 k」。前者控制相似度可信度,后者控制推荐列表的多样性。我常用的调参顺序是固定 k,逐步提高相似度阈值,观察 MAE 的下降趋势,找到拐点即可。

相似度阈值MAE 变化趋势覆盖率变化趋势调参结论
1 个共同评分偏低,但噪声大不稳定覆盖率最高不适合答辩展示,波动大
3 个共同评分明显下降,趋于稳定覆盖率下降约 10%比较理想的起点
5 个共同评分变化不明显,可能回升覆盖率继续下降提升有限,收益变低

调参时的验证技巧是把recommend_top_ntop_n分别跑 3、5、8,再把指标贴进论文的测试章节,形成对比表格。真正拉高 MAE 的往往不是算法本身,而是评分矩阵里存在「用户只评过一个景点」的极端行,这类行会在留一法中被跳过,但如果跳过的样本太多,评估本身就失真了。我一般会先把评分少于 5 条的用户过滤掉,再跑评估,得到的数据才是系统正常状态下的真实水平。用一句概括这个验证思路:评估脚本和推荐逻辑共用一个预测核心函数,调哪里、效果怎么样,一眼就能看明白。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 6:20:05

基于HuggingFace的聊天机器人开发实战指南

1. 项目概述&#xff1a;基于HuggingFace的聊天机器人开发实战去年在开发一个智能客服系统时&#xff0c;我首次尝试用HuggingFace的预训练模型搭建对话引擎。当时被其开箱即用的效果震惊——仅用20行代码就实现了接近商业产品的对话能力。这种低门槛的AI开发方式正在改变整个行…

作者头像 李华
网站建设 2026/9/15 6:19:37

光学衍射神经网络在图像加密中的应用与实现

1. 光学衍射神经网络多图像加密与隐藏技术解析在数字信息爆炸式增长的今天&#xff0c;图像数据的安全传输与存储成为了一个关键挑战。传统加密方法如AES、RSA虽然成熟&#xff0c;但在处理图像这类高维数据时往往效率不足。最近我在实验室尝试了一种基于光学衍射神经网络&…

作者头像 李华
网站建设 2026/9/15 6:19:32

广告加工厂转型:从拼设备到拼服务的实战路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 6:19:19

rDock与强化学习:SBMolGen 分子从头设计全流程解析

简介&#xff1a;SBMolGen是一套面向药物研发与AI医学应用场景的分子从头设计工具资源&#xff0c;将深度学习CNN与RNN模型和分子对接打分相结合&#xff0c;帮助科研人员快速生成候选药物分子&#xff0c;并评估其与靶标蛋白的结合能力&#xff0c;适用于药物发现、靶点筛选、…

作者头像 李华
网站建设 2026/9/15 6:19:16

WSL2环境下部署OpenClaw自动化工具全指南

1. 项目背景与核心价值去年在帮朋友处理一个自动化任务时&#xff0c;偶然发现了OpenClaw这个开源工具。它原本设计用于Linux环境下的自动化操作&#xff0c;但很多开发者其实更习惯在Windows下工作。经过多次尝试&#xff0c;终于在WSL2环境中找到了完美的部署方案。WSL2&…

作者头像 李华
网站建设 2026/9/15 6:19:05

组态王与S7-200在火电厂脱硫系统中的通信与调试实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华