ARTICLE DETAIL

资讯详情

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

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

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, encodingutf-8) attractions pd.read_csv(attractions.csv, encodingutf-8) ratings pd.read_csv(ratings.csv, encodingutf-8) # 合并成一张宽表便于查看和调试 merged ratings.merge(users, onuser_id).merge(attractions, onitem_id) # 用户-景点评分矩阵空值即该用户未评分 rating_matrix ratings.pivot_table( indexuser_id, columnsitem_id, valuesrating ) # 把评分统一成 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是这里的主心骨valuesrating指定用评分列填充交叉格某个用户没评过某个景点就留下 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, indexitems, columnsitems) 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_n5): 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(dtypefloat) # 按预测分从高到低排序取前 top_n 个 top pd.Series(scores).sort_values(ascendingFalse).head(top_n) return top # 调用示例给用户 1 推荐 5 个景点 recommendations recommend_top_n(1, rating_matrix, sim_matrix, top_n5) print(recommendations)逻辑说明候选池是用户没有评过分的所有物品这一步保证了推荐结果不会推荐用户已经去过的景点。positive_mask sims 0把负相似的物品剔除掉只保留有正向参考价值的物品。最终排序用的sort_values(ascendingFalse)取预测分数最高的前 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_col0) sim_matrix pd.read_csv(sim_matrix.csv, index_col0) app.route(/recommend, methods[GET]) def recommend(): user_id request.args.get(user_id, typeint) top_n request.args.get(top_n, default5, typeint) 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(debugTrue, port5000)参数说明user_id是必传参数top_n默认 5typeint会让 Flask 自动做类型转换传非数字会返回 400 而不是 500这个小细节答辩时能加分。把rating_matrix和sim_matrix放在函数外加载是因为每次请求都读 CSV 会拖慢响应而且矩阵属于静态数据不需要热更新。启动后访问http://127.0.0.1:5000/recommend?user_id1top_n5就能看到 JSON 结果用 curl 验证更直观curl http://127.0.0.1:5000/recommend?user_id1top_n54.2 冷启动场景基于景点标签的内容推荐兜底协同过滤有一个先天缺陷新用户没有评分记录无法算相似度。毕设里不能回避这个问题因为答辩老师必然会问「一个新用户注册后你给他推荐什么」。常见做法是补一条「基于内容」的推荐路给每个景点打标签比如「历史人文」「自然风光」「亲子」「美食街区」新用户注册时勾选感兴趣的主题系统按标签匹配景点。下面给出简化版实现def content_based_fallback(user_id, top_n): # 取该用户偏好标签实际项目中存在用户表里 prefer_tags [历史人文, 自然风光] # 景点表带 tags 字段多个标签用逗号分隔 attractions pd.read_csv(attractions.csv, encodingutf-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, ascendingFalse).head(top_n) top_scores top[tag_hit] / len(prefer_tags) return pd.Series(top_scores.values, indextop[item_id], namescore)逻辑说明tag_hit计算的是景点标签与用户偏好标签的交集大小排序后取前 N 个返回。这里的评分逻辑没有用到真正的评分数据所以只是兜底方案。实际毕设中建议把这个函数挂在/recommend接口的冷启动分支上即user_id合法但无评分时触发不要让用户看到空推荐列表。4.3 说明文档与论文结构怎么对应源码毕设交付物里的说明文档和论文核心不是写代码怎么实现而是写「系统怎么设计、为什么这么设计」。我在整理自己项目时总结了这几组对应关系。论文章节对应源码目录关键内容提示第 3 章 系统设计data/和utils/三表结构设计、评分矩阵构建流程、相似度矩阵预计算策略第 4 章 系统实现recommend/和app.py协同过滤函数、Flask 接口定义、冷启动兜底逻辑第 5 章 系统测试evaluate.py和test/留一法评估、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(indexitem_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(indexitem_id)模拟了「用户还没去过这个景点」的状态再走一遍和第 3 章相同的加权求和逻辑拿到预测值后与真实值true_rating做差值。coverage_items用来累计至少被成功预测过一次的物品 ID覆盖率能直接反映推荐结果有没有卡在少数头部景点上。跑完如果 MAE 在 0.8 以下说明系统的预测精度基本过关。5.2 参数调试技巧k 值与相似度阈值的配合留一法跑出来的指标不是一次就合格的需要调参。协同过滤两个直接影响结果的参数是「相似度阈值」第 2 章提到的共同评分下限和「最终推荐条数 k」。前者控制相似度可信度后者控制推荐列表的多样性。我常用的调参顺序是固定 k逐步提高相似度阈值观察 MAE 的下降趋势找到拐点即可。相似度阈值MAE 变化趋势覆盖率变化趋势调参结论1 个共同评分偏低但噪声大不稳定覆盖率最高不适合答辩展示波动大3 个共同评分明显下降趋于稳定覆盖率下降约 10%比较理想的起点5 个共同评分变化不明显可能回升覆盖率继续下降提升有限收益变低调参时的验证技巧是把recommend_top_n里top_n分别跑 3、5、8再把指标贴进论文的测试章节形成对比表格。真正拉高 MAE 的往往不是算法本身而是评分矩阵里存在「用户只评过一个景点」的极端行这类行会在留一法中被跳过但如果跳过的样本太多评估本身就失真了。我一般会先把评分少于 5 条的用户过滤掉再跑评估得到的数据才是系统正常状态下的真实水平。用一句概括这个验证思路评估脚本和推荐逻辑共用一个预测核心函数调哪里、效果怎么样一眼就能看明白。本文还有配套的精品资源点击获取
返回列表