ARTICLE DETAIL

资讯详情

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

Python智能旅游推荐系统毕设:协同过滤算法与Flask接口实战

Python智能旅游推荐系统毕设:协同过滤算法与Flask接口实战 简介一套基于Python的智能旅游推荐系统毕业设计项目面向计算机相关专业学生及Python开发者可同时用于毕业设计、课程设计与实战练手。项目采用前后端分离结构前端以HTML、Vue、CSS为主包含丰富的JS交互脚本与SVG图标资源后端基于Python 3.7配合MySQL数据库及SQL脚本提供完整的数据库初始化数据系统功能覆盖旅游线路推荐、景点信息展示、用户管理等模块界面简洁美观流程操作便捷具备较高的实际应用价值。压缩包共797个文件约20.19MB涵盖164个JS脚本、53个Vue组件、60张JPG/PNG图片、45个Python源文件、2个SQL数据库脚本以及安装与运行批处理文件内含安装.bat、运行.bat等辅助脚本可直接配置环境运行目录结构清晰便于快速定位源码、前端页面、数据库和配置文档。项目已经严格调试可直接运行已有132人学习下载通过这套项目可掌握旅游推荐系统的完整业务逻辑、前后端数据交互方式及Python Web项目从配置到部署的全流程同时可借助附带数据库脚本和Navicat可视化工具快速还原环境适合作为毕业设计答辩或作品集素材。1. 智能旅游推荐系统在毕设里到底做什么做毕设选“基于Python的智能旅游推荐系统”的人十个里有八个不是奔着旅游去的而是这套题能把大学四年学的东西串成一条完整链路爬数据或造数据、MySQL建表、pandas清洗、协同过滤算法、Flask写接口、前端展示。老师看到的是一个从数据库到推荐结果再到网页演示的闭环你自己也能在答辩时讲清楚每个环节。这个系统核心解决的其实是典型的“信息过载”问题用户面对几百上千个景点不知道去哪系统根据他之前的浏览、收藏和评分行为帮他过滤掉不合适的选项直接给出TopN推荐。适合Python基础已经过关、但缺少一个完整项目经验的同学也适合想快速把协同过滤从公式变成能跑代码的人。它不追求算法多前沿重要的是把“用户-景点-评分”这条数据链路做扎实。2. 推荐算法选型为什么协同过滤是这套题的默认主干2.1 先看用户行为形态再决定UserCF还是ItemCF智能旅游推荐系统里最常见的做法是协同过滤核心思想只有一句话让相似的人或相似的东西互相背书。UserCF基于用户的协同过滤找的是“和你口味相似的人”把这些人去过而你没去过的地方推给你。ItemCF基于物品的协同过滤找的是“和你喜欢过的景点相似的景点”比如你收藏过西湖系统把西溪湿地、灵隐寺这一类同样主打自然人文的景点推给你。选型有一个很朴素的判断标准用户数量大、物品内容更新快、用户兴趣容易变化的场景用UserCF比如新闻App物品集合相对稳定、用户兴趣比较长久的场景用ItemCF比如图书、旅游景点。毕设里用户往往只有几百个评分几千条景点表撑死几百行这种情况下ItemCF有两个肉眼可见的好处景点相似度矩阵可以离线算好接口只需要做查表响应快演示不卡顿。推荐结果可解释性强能说出“因为你去过XX所以推荐XX”答辩时特别好讲。所以我一般会默认把ItemCF当主干UserCF留作对比实验。对比实验在毕业论文里是个加分项哪怕最终效果差不多至少说明你两种方法都推演过。2.2 基于内容的推荐专门补冷启动这个硬伤协同过滤有一个天生短板新注册的用户没有任何行为记录系统不知道该给他推什么新录入的景点没有任何人评分它就永远沉在数据库底部。这个现象业内叫冷启动。旅游推荐系统尤其怕冷启动因为用户第一次打开页面时通常只有一个“当前城市”的信息连登录都没有这时候你不可能指望协同过滤。常见的做法是加一层基于内容的推荐核心思路是用景点本身的属性算相似度而不是用用户行为。属性可以取城市、主题类型、门票区间、游玩时长这几个字段。城市必须单独对待因为旅游和买书不一样推荐一个从北京跑到三亚的景点很可能没有实际意义——用户根本没有那个出行计划。一个最朴素的特征相似度计算可以写成这样。# 基于内容的景点相似度只取城市和主题做加权 def content_similarity(a, b): sim 0.0 if a[city] b[city]: sim 0.5 if a[theme] b[theme]: sim 0.3 if abs(a[price] - b[price]) / max(a[price], b[price], 1) 0.2: sim 0.2 return sim这里把城市权重提到0.5是因为在旅游推荐里地域属性是硬约束主题和价格只是软偏好。如果反过来城市权重太低相似度矩阵里就会混进去大量跨省但同主题的景点推荐结果看起来“很智能”实际上用户根本用不上。基于内容的推荐不用动用户行为表数据直接从景点表读属性就能算非常适合做冷启动兜底。它的缺点是推荐结果容易同质化翻来覆去都是同一类地方所以通常是和协同过滤混合使用而不是替代。2.3 混合推荐加权融合比纯算法更耐看毕设答辩时老师最常问的问题之一就是“你这个推荐和淘宝首页的有啥区别”你如果回答“我用了某个纯算法”其实很难站住脚。电商推荐系统里没有哪个是只靠一种算法打天下的都是混合策略。针对旅游推荐系统我常用的方案是加权融合协同过滤得分占0.7内容相似度得分占0.3两者产生冲突时以协同过滤为主内容相似度负责把冷门但属性接近的景点补进来。如果用户行为太少连协同过滤都算不出可靠结果就直接降级到“当前城市的热门榜”。这个降级策略不是随便想的它对应一个关键工程问题算法的置信度。行为数据只有三五条时算出来的相似度全是噪声与其硬推不如让热门榜顶上。热门榜本身也是一个推荐策略而且是最稳的底线。混合推荐的排序逻辑可以理解为两轮召回阶段先把城市不匹配的景点剔除再用协同过滤得分和内容相似度加权算总分最后按总分截断取TopN。3. 数据库设计与旅游数据准备数据造不好算法全是空中楼阁3.1 三张核心表怎么建才不会被答辩老师挑毛病旅游推荐系统的数据模型不复杂核心就是三张表用户表、景点表、评分表。很多毕设翻车就翻在评分表上要么没有唯一键导致同一用户对同一景点重复评分要么外键随意设置导致造数据时各种报错。先看一份可以直接落地的建表SQL。CREATE DATABASE travel_rec CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE travel_rec; CREATE TABLE user ( uid INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, gender TINYINT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sight ( sid INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, city VARCHAR(50) NOT NULL, theme VARCHAR(50), price DECIMAL(10,2) DEFAULT 0, longitude DECIMAL(10,6), latitude DECIMAL(10,6), rating_cache DECIMAL(3,1) DEFAULT 0.0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE rating ( rid INT PRIMARY KEY AUTO_INCREMENT, uid INT NOT NULL, sid INT NOT NULL, score TINYINT NOT NULL COMMENT 1-5分, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_uid_sid(uid, sid), CONSTRAINT fk_rating_user FOREIGN KEY (uid) REFERENCES user(uid), CONSTRAINT fk_rating_sight FOREIGN KEY (sid) REFERENCES sight(sid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE INDEX idx_rating_sid ON rating(sid); CREATE INDEX idx_rating_uid ON rating(uid);几个容易被忽略的设计点UNIQUE KEY uk_uid_sid(uid, sid)非常关键。它从数据库层面保证同一个用户对一个景点只产生一条评分后面不管数据导入多少次、脚本多跑几遍都不会把矩阵撑出重复值。score TINYINT用1到5的整数而不是浮点数。评分是用户的离散情绪表达10分制看着精细实际上用户根本分不清7分和8分的区别反而加大造数据难度。景点表里的rating_cache是冗余字段存景点平均分。推荐排序或展示热门榜时直接查这个字段不用每次实时聚合AVG(score)几百条数据虽然无所谓但这个习惯对后续扩展有价值。外键建议只在 rating 表上设fk_rating_user和fk_rating_sight用户表和景点表之间不要乱加外键。因为真实业务里修改主键的情况很少外键一旦参与导入批量写数据的顺序就会被约束容易报错。如果你在vscode里配置Python环境记得装pymysql和sqlalchemy这两个库连接MySQL时能省掉很多手工操作。3.2 造数据是门玄学怎么生成“像真人”的评分记录多数毕设项目拿不到真实的旅游平台数据第一反应是写几个for循环随机生成评分。这里有个坑如果按均匀分布随机打1到5分造出来的评分矩阵“太平均了”协同过滤跑出来的推荐结果几乎等于瞎蒙因为用户之间的差异太小。真实场景里有几条统计规律可以模拟热门景点获得的评分多且分数偏高冷门景点评分少且分数方差大大部分用户只评过几个景点只有少数重度用户会评几十个。这种“幂律分布”特征才是推荐算法能抓到规律的前提。import random import pandas as pd random.seed(42) # 假设sight表里有500个景点前50个设为热门 all_sights list(range(1, 501)) hot_sights set(random.sample(all_sights, 50)) rows [] for uid in range(1, 201): # 用power分布模拟用户活跃度多数用户评3-20条少数评很多 n_ratings int(random.power(4) * 20) 3 rated_sights random.sample(all_sights, min(n_ratings, len(all_sights))) for sid in rated_sights: # 热门景点基准分给4.0冷门给2.8再加高斯噪声 base 4.0 if sid in hot_sights else 2.8 score int(min(5, max(1, round(base random.gauss(0, 1.2))))) rows.append((uid, sid, score, uid 1000000)) ratings pd.DataFrame(rows, columns[uid, sid, score, timestamp]) ratings.to_csv(ratings.csv, indexFalse, encodingutf-8-sig)重点看random.power(4)这个参数。幂律分布的指数调得越高数据长尾越明显指数为4时大部分用户落在3到10条评分的区间少数用户能评到20条以上。这个比例和真实推荐场景比较接近。如果直接把random.randint(1, 30)拿来用造出来的数据会均匀得像个假数据集算法跑出来不如直接按热度排序答辩时经不起追问。热门景点基准分给4.0而不是5.0也有讲究。真实的用户永远不会把好评全部给到满分总有人因为天气、排队、预期落差给热门景点打低分。加上random.gauss(0, 1.2)的噪声之后热门景点的平均分落在3.8到4.2之间冷门景点平均分在2.5到3.2之间这样的区分度才能让算法学到“热门的就是更受欢迎”这个信号。3.3 把CSV灌进MySQL用Python比Navicat粘贴更省心数据文件有了接下来要导入MySQL。很多同学习惯打开Navicat手动导入但CSV字段类型、编码、主键冲突的问题会在图形界面里被吞掉报错信息也不直观。我更建议直接在Python里完成导入顺带可以做一次简单的数据校验。import pandas as pd from sqlalchemy import create_engine ratings pd.read_csv(ratings.csv, encodingutf-8-sig) ratings.columns [uid, sid, score, timestamp] engine create_engine( mysqlpymysql://root:123456localhost:3306/travel_rec?charsetutf8mb4, pool_size5, pool_recycle3600 ) with engine.begin() as conn: ratings.to_sql(rating, conn, if_existsappend, indexFalse, chunksize1000)这里有几个参数值得说明encodingutf-8-sig是为了处理Windows下CSV文件常见的BOM头。用普通的utf-8读取时第一列列名会变成带不可见前缀的字符串导入后字段名对不上报错很难排查。mysqlpymysql://是SQLAlchemy连接MySQL的标准驱动写法。URL里的charsetutf8mb4必须显式声明否则即使数据库建表时用了utf8mb4连接层的字符集可能还是latin1中文导入后就变成乱码。if_existsappend表示追加写入。如果反复跑这个脚本会触发前面建表时设的唯一键约束重复数据会被拒绝而不是变成脏数据。这也是我把唯一键放在最前面的原因。chunksize1000让pandas分批写入避免一次性拼一个超大的INSERT语句把数据库连接拖死。几千行数据可能感觉不到差异但如果你把评分规模扩到几万条就会明白这个参数的价值。导入完成后顺手检查一下数据量。SELECT COUNT(*) FROM rating; SELECT uid, COUNT(*) AS cnt FROM rating GROUP BY uid ORDER BY cnt DESC LIMIT 10;第二句SQL是检查“用户评分数量分布”是否还有长尾特征。如果所有用户都评了差不多的数量说明造数脚本的幂律设计失败了趁早回去改数据别等算法跑完才发现推荐结果全是热门榜。4. 核心代码落地相似度计算、推荐生成与Web接口4.1 把评分表变成算法能吃的矩阵推荐算法不直接认SQL表它需要的是一个二维矩阵行是用户列是景点单元格是评分没有评分的位置填0。pandas里用pivot_table一行就能完成。import pandas as pd from scipy.sparse import csr_matrix from sqlalchemy import create_engine engine create_engine(mysqlpymysql://root:123456localhost:3306/travel_rec?charsetutf8mb4) df pd.read_sql(SELECT uid, sid, score FROM rating, engine) # 行用户列景点 rating_matrix df.pivot_table(indexuid, columnssid, valuesscore).fillna(0) sparse_matrix csr_matrix(rating_matrix.values)pivot_table执行完后rating_matrix.index是全量用户IDrating_matrix.columns是全量景点ID。后面所有推荐结果都要用这两个索引来还原成真实的UID和景点SID不要自己另建一套ID映射否则接口返回时容易对不上号。这里我特意把fillna(0)和csr_matrix都写出来是因为它们各有各的用途。fillna(0)方便你print出来直观检查数据比如确认某一行几个非零值csr_matrix才是真正送去算相似度的对象它只存储非零元素几百个用户几百个景点可能看不出差别但当你加数据到几万条时稠密矩阵的内存开销会是稀疏矩阵的几十倍。4.2 用余弦相似度计算景点之间的关系景点相似度有多种算法皮尔逊相关系数、余弦相似度、杰卡德相似系数都有人用。在评分数据稀疏的场景下余弦相似度是最稳的选择因为它只关心两个景点在共同被评过的用户上方向是否一致不关心评分绝对高低。实现上用矩阵乘法代替双重循环速度完全够用。import numpy as np def cosine_similarity(item_matrix): # item_matrix: shape (n_sights, n_users) item_matrix item_matrix.T norm np.sqrt(np.sum(item_matrix ** 2, axis1)).reshape(-1, 1) norm[norm 0] 1e-6 normalized item_matrix / norm return normalized.dot(normalized.T)这段代码里有个细节值得注意把原始矩阵转置成“每一行是一个景点”再计算。为什么要转置因为协同过滤最终要的是“景点相似度矩阵”它的第 i 行第 j 列表示景点 i 和景点 j 的相似度。如果你拿用户评分矩阵直接算得到的是用户相似度方向就反了。norm[norm 0] 1e-6是防御性写法。当某个景点没有任何人评分时它的行全是0除零会产生NaN而NaN会污染整个矩阵——一个NaN经过矩阵乘法扩散出去所有和它有关的相似度全部变成NaN后续排序直接崩。给它一个极小的归一化值至少保证结果是一个有限数。4.3 生成推荐列表加权求和加上城市过滤相似度矩阵算完之后推荐生成就是一个查表和排序的过程遍历用户已经评分的景点找出每个景点最相似的邻居景点用“相似度 × 用户对该景点的评分”作为邻居景点的得分累加后排序取TopN。from collections import defaultdict def recommend(uid, rating_matrix, sim_matrix, top_n10): if uid not in rating_matrix.index: return [] # 交给上层兜底热门榜 user_ratings rating_matrix.loc[uid] rated_sights user_ratings[user_ratings 0].index.tolist() if len(rated_sights) 5: return [] # 行为太少协同过滤结果不可信 scores defaultdict(float) for s1 in rated_sights: s1_idx rating_matrix.columns.get_loc(s1) # 取该景点最相似的30个邻居 neighbor_idx np.argsort(sim_matrix[s1_idx])[::-1][:30] for s2_idx in neighbor_idx: s2 rating_matrix.columns[s2_idx] if s2 in rated_sights: continue sim sim_matrix[s1_idx][s2_idx] if sim 0.1: scores[s2] sim * user_ratings[s1] # 城市过滤在排序后执行实际项目中在这里按city字段排除 ranked sorted(scores.items(), keylambda x: x[1], reverseTrue) return [sid for sid, _ in ranked[:top_n]]几个参数是用教训换来的邻居数量取30不是取全部。全部邻居参与计算会把大量相似度极低的景点噪声累加进分数里TopN结果反而被拉偏。30这个值在几百个景点的数据集上表现比较稳定你要是数据量大可以提到50。sim 0.1是过滤阈值。相似度低于0.1的邻居基本就是噪声参与加权只会让推荐结果变平庸。行为少于5条直接返回空列表。这个策略配合后续接口层的热门榜兜底才能保证每个用户打开页面都有内容可看。4.4 用Flask把推荐结果包装成交互接口算法跑通后还差最后一步让用户在浏览器里触发推荐。最常见的方式是用Flask提供两个接口一个渲染页面一个返回JSON数据。from flask import Flask, request, jsonify, render_template app Flask(__name__) # 启动时把相似度矩阵算好接口内不做重计算 SIM_MATRIX cosine_similarity(sparse_matrix) app.route(/) def index(): return render_template(index.html) app.route(/api/recommend/int:uid) def recommend_api(uid): recs recommend(uid, rating_matrix, SIM_MATRIX) if not recs: # 冷启动兜底返回当前城市热门景点 recs get_hot_sights_by_city(city杭州, top_n10) return jsonify({ uid: uid, sights: [{sid: sid, name: sight_name_map[sid]} for sid in recs] }) app.run(debugTrue, port5000)注意SIM_MATRIX是模块导入时一次性算好的而不是在每次请求里重新算。一个几百乘几百的相似度矩阵双循环计算也就几十毫秒但如果以后数据量变大每次请求都重算会直接把接口拖垮。把重计算放到启动阶段是这类小型推荐项目最实用的性能优化没有之一。返回JSON时还有一个隐藏坑sim_matrix里的元素是numpy类型jsonify序列化np.int64或np.float64会直接抛TypeError。所以我在组装返回结构时用int()包了一层避免序列化踩坑。4.5 前端展示透明度比炫酷更重要推荐系统的前端不需要很复杂但要把“推荐理由”展示出来。最简单的做法是推荐列表里加一行小字因为你去过西湖、灵隐寺所以为你推荐西溪湿地。这个设计不仅对用户有说服力更重要的是答辩时有话可讲。老师问“你这个推荐为什么靠谱”你指着界面的推荐理由说这是ItemCF的产物系统根据你历史评分景点的相似邻居加权计算得出。可解释性直接体现“我懂算法原理”比一个只会吐景点列表的黑匣子强太多。5. 避坑旅游推荐系统从开发到答辩的常见问题与排查5.1 相似度矩阵全是0推荐列表永远为空现象控制台打印sim_matrix全部是0调用推荐接口返回空列表前端什么都没有。原因最常见的是评分矩阵过于稀疏。如果200个用户只产生了800条评分每个用户平均只评了4个景点景点两两之间几乎没有被同一个人评过余弦相似度算出来没有共同非零项自然全是0。解决要么把造数脚本的用户评分数量下限提高保证每个用户至少评10个景点要么在推荐函数里加一个最少评分阈值我上面代码里len(rated_sights) 5就是干这个的。数据稀疏不是靠算法能硬扛的先把数据密度提上来再谈算法效果。5.2 中文景点名导入MySQL后全部变成问号现象sight表里插入“西湖”查询出来是???前端页面显示乱码。原因三层字符集有一层没对齐。数据库建库时用了utf8mb4但连接串没加charsetutf8mb4pymysql默认用latin1发送数据或者CSV文件读取用了encodingutf-8但文件本身带BOM头。解决连接串显式加上?charsetutf8mb4CSV读取统一用utf-8-sig建库语句用CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。这三层对齐后中文基本不会再出问题。如果还乱码检查MySQL服务端默认字符集在my.cnf里设置character-set-serverutf8mb4。5.3 推荐结果同质化严重来来回回都是同一类景点现象用户收藏过一个自然风光景点推荐列表里全是自然风光同一个城市里扎堆出现看着不智能。原因ItemCF天然存在“马太效应”相似度高的景点总是更容易被推荐多样性和用户真实需求被忽略了。旅游场景尤其特殊用户出游一次可能会同时考虑人文、美食、购物、亲子单一类型的推荐不符合实际。解决在排序后加一个多样性重排。常见做法是对结果按主题做配额比如Top10里最多只允许4个同主题景点优先级从高到低依次排。更简单的一种是轮盘策略每取一个景点检查与已选景点平均相似度超过阈值就跳过换下一个。5.4 Flask接口报错 TypeError: Object of type int64 is not JSON serializable现象浏览器访问/api/recommend/1直接白屏控制台报TypeError。原因numpy会把pandas里的整数变成np.int64把小数变成np.float64。Flask的jsonify不认识这些类型序列化时直接抛异常。解决组装返回数据时显式转换类型。sights [{ sid: int(sid), name: str(sight_name_map[sid]), score: float(scores.get(sid, 0.0)) } for sid in recs]5.5 导入评分数据时外键约束报错 IntegrityError现象运行ratings.to_sql()时报IntegrityError: (pymysql.err.IntegrityError) Cannot add or update a child row。原因造数脚本生成的uid或sid在 user 表和 sight 表里不存在。很多同学先跑造数脚本生成评分再手动往用户景点表里灌数据两边ID范围对不上外键直接拒绝写入。解决严格按“先导user、再导sight、最后导rating”的顺序操作。造数脚本里用random.sample(range(1, 501), n)时先确认sight表的主键最大值和最小值正好覆盖这个范围。我习惯在造数前先执行一条SELECT MAX(sid), MIN(sid) FROM sight;把范围查出来再写脚本。6. 把系统从“能跑”做到“能答辩”离线评估与冷启动技巧推荐系统最容易被答辩老师抓到的问题就是你自己怎么知道推荐结果好不好如果答不上来说明系统缺少评估环节。毕设阶段不需要上在线A/B测试用离线评估就够了方法叫留一法把每个用户的最后一条评分藏起来用剩下的数据给这个用户推荐再看被藏起来的那条评分有没有出现在推荐列表里。def evaluate(rating_matrix, sim_matrix, top_n10): hit 0 total 0 for uid in rating_matrix.index: user_ratings rating_matrix.loc[uid] rated user_ratings[user_ratings 0] if len(rated) 2: continue # 藏起最后一条评分 test_sid rated.index[-1] train rating_matrix.copy() train.loc[uid, test_sid] 0 recs recommend(uid, train, sim_matrix, top_ntop_n) if test_sid in recs: hit 1 total 1 return hit / max(total, 1)配合Precision10推荐列表里有多少是用户真正感兴趣的和Recall10用户感兴趣的景点有多少被推荐出来两个指标就能说明系统有效性。毕设项目里精度达到0.1到0.2之间已经算及格不用追求夸张的数字重点是你敢把评估过程讲清楚。最后一个实用技巧冷启动状态下先用“当前城市热门榜”扛住首页。用户一进入系统根据IP或手动选择的城市返回热门景点等用户产生三五个评分行为后再切换到协同过滤推荐。这个策略简单、有效、还容易演示比硬上一个复杂模型实在得多。我每一版改完相似度阈值或邻居数量都会重跑一遍留一法评估再手动打开页面用几个测试账号点一遍。推荐系统这东西有点玄学光看离线指标不许真实页面里有没有内容才是最后的底线。希望这些步骤能帮你把这个项目做成一个站得住脚的毕设也希望你答辩顺利。本文还有配套的精品资源点击获取
返回列表