资讯详情

资讯详情

Python协同过滤旅游推荐与数据可视化系统设计实现

很多人选毕设题目容易走进两个极端要么追着大模型、深度强化学习这种高热词汇跑结果你连训练数据都凑不齐要么干脆做个增删改查的管理系统答辩时老师问一句你这里有什么算法含量就愣住了。我自己最后选的方向是Python旅游智能推荐与数据可视化系统技术上用Django做Web框架推荐引擎基于协同过滤推荐算法再把景点分析结果用图表和地图做成可视化大屏。这套组合做完以后你会发现它特别适合作为计算机毕业设计选题有产业场景旅游出行核心有算法协同过滤展示有可视化数据分析大屏技术栈还是Python生态里最成熟的Django——每一项都能在答辩时实打实讲出来不怕被追问。这个项目做下来本质是在回答一个问题一个用户面对成千上万的景点系统怎么用他在历史上的评分、收藏、浏览行为帮他筛出最可能感兴趣的旅行目的地解决这个问题需要的数据量不算大但处理链路很完整数据清洗、建模、算法计算、Web服务、前端可视化。这篇文章把所有环节拆开讲包括代码实现和踩坑过程想拿去做毕设或者课程项目的同学可以直接参考。1. 选题逻辑与需求拆解旅游推荐系统到底要建哪些模块1.1 为什么是Django而不是Flask或SpringBoot我在定技术栈的时候先排除了SpringBoot。不是说它不好而是一个毕设项目里我们通常只有两到四个月的时间大部分精力要花在算法和可视化上如果还被Java那套配置、依赖、部署流程拖住成本太高。用Python的好处是算法代码和Web代码可以写在同一个项目里共享数据模型不用跨语言调接口。Django和Flask之间我最终选了Django。Flask虽然轻量但很多能力需要你自己拼用户认证、ORM、Admin后台、表单校验Django全都自带。旅游推荐系统里要维护用户、景点、评分、收藏这些实体数据库模型多关系复杂Django的ORM和自带Admin能省掉一大半重复工作。做毕设还有一个隐性要求——好演示。Django自带的后台管理界面你往景点表里录数据、给用户调评分整个过程都能在答辩现场直接展示这个东西在Flask里要折腾好久才能做出同等效果。技术栈对比如下对比项DjangoFlaskSpringBoot学习成本中低全家桶低但组装累高配置多ORM与后台自带Admin后台需装扩展JPA额外配置与算法代码集成同语言直调同语言直调需REST接口中转适合毕设场景极好尚可偏企业级如果后续想在这个项目上继续扩展比如接一个智能行程规划模块Django的App结构也能很好地隔离业务我的建议是没有特殊理由就用Django。1.2 功能模块怎么划分才不会被老师说工作量不够推荐系统如果只做猜你喜欢一个接口内容会很单薄。我调研了旅游平台常见的功能形态结合毕设的展示需要把系统拆成了五个模块用户模块注册、登录、个人偏好标签比如自然风光人文古迹亲子游美食之旅。景点模块景点名称、所在城市、经度纬度、门票均价、开放时间、封面图、热度值。评分与行为模块用户对景点的评分1-5分、收藏记录、浏览记录。这是协同过滤算法最核心的输入。推荐模块基于协同过滤计算TopN景点推荐同时对未登录用户给出基于热门的默认推荐。数据可视化模块景点热度排行、城市景点数量分布、用户画像词云、出游热度趋势以及地域流向图。这里有一个容易被忽略的点行为数据模块一定不要省。很多毕设只做景点CRUD简单推荐做完之后算法没有数据喂推荐结果全靠随机。我把评分、收藏、浏览三种行为都做成了独立数据表这既方便算法读取也方便答辩时展示系统从行为采集到推荐输出的完整闭环。1.3 数据准备数据集选择、清洗与造数技巧这是一个真问题协同过滤需要用户-景点评分矩阵但网上能直接下载的旅游领域公开数据集不多。我当时有两条路一是找通用推荐数据集改造成旅游字段二是自己构造模拟数据。我的最终方案是混合基础景点数据用一份整理好的国内景点公开表格包含省份、城市、景点名、等级、票价大约800多条记录评分数据则按幂律分布模拟生成比如热门景点被评次数多冷门景点评次少每个用户评分数量从5到80不等。这样做出来的稀疏度在90%以上和真实场景差不多算法才能体现出价值。数据清洗是很多人会忽略的环节。原始景点表格里的问题通常是城市字段格式混乱拉萨市和拉萨都有、票价有字符串混进来免费、100、坐标缺失或明显错位。我只用pandas做三步清洗统一城市名票价字段解析成数值型且缺失值填0坐标字段删除掉明显超出中国范围的记录。清洗脚本独立成一个data_clean.py文件答辩的时候可以明确告诉老师数据曾经过预处理这也是数据分析能力的体现。2. 协同过滤推荐算法从相似度计算公式到Python完整实现2.1 先做选择基于用户协同过滤还是基于物品协同过滤协同过滤推荐算法有两种主流方向理解这两种方向是整篇代码的地基。基于用户的协同过滤UserCF的核心思路是找到和当前用户口味最相似的一群人把这群人喜欢的、而当前用户没见过的景点推荐给他。适合用户数量远比物品数量少、且用户兴趣变化较慢的场景。基于物品的协同过滤ItemCF反过来计算景点之间的相似度比如看过故宫的人通常也喜欢颐和园然后根据用户历史上喜欢的景点推荐相似的景点。适合用户多、物品相对少、用户兴趣相对固定的场景。我做的是旅游推荐物品景点是相对稳定的用户的行为会受季节性、流行度影响比较大理论上ItemCF在长期稳定性上更好但UserCF在用户数量不多时结果更加直观答辩也更好解释我找到了一群同样喜欢自然风光的驴友。考虑到毕设的数据量级用UserCF更容易做出推荐结果的解释性展示。因此我的主推荐算法定为基于用户的协同过滤同时保留ItemCF模块作为对比。2.2 基于用户的协同过滤核心Python代码实现我先构造评分矩阵。数据表结构简化成三列user_id、scenic_id、rating。用pandas的pivot_table转成矩阵行是用户列是景点空值填0。import pandas as pd import numpy as np from math import sqrt from collections import defaultdict def load_data(): # 从Django ORM或CSV读取评分记录 ratings_df pd.read_csv(data/ratings.csv) scenic_df pd.read_csv(data/scenics.csv) matrix ratings_df.pivot_table( indexuser_id, columnsscenic_id, valuesrating ).fillna(0) return ratings_df, scenic_df, matrix接下来是相似度计算。相似度度量我选了皮尔逊相关系数而不是余弦相似度原因是用户评分尺度不同有人习惯打3分有人习惯打5分余弦相似度会把这种尺度差异误判为兴趣差异皮尔逊通过减去用户平均分相当于先把每个用户的评分习惯归一化再比较形状更加合理。def pearson_similarity(a: pd.Series, b: pd.Series) - float: # 只取双方都有评分的景点 common (a 0) (b 0) if common.sum() 2: return 0.0 a_vals a[common] b_vals b[common] a_mean a_vals.mean() b_mean b_vals.mean() numerator ((a_vals - a_mean) * (b_vals - b_mean)).sum() denominator sqrt(((a_vals - a_mean) ** 2).sum() * ((b_vals - b_mean) ** 2).sum()) if denominator 0: return 0.0 return numerator / denominator然后对目标用户找到TopK相似用户再用他们评过分而目标用户没评过分的景点做加权评分预测。def recommend_for_user(matrix, user_id, top_k20, top_n10): if user_id not in matrix.index: return [] user_row matrix.loc[user_id] # 计算当前用户与所有其他用户的相似度 scores {} for other_id in matrix.index: if other_id user_id: continue sim pearson_similarity(user_row, matrix.loc[other_id]) if sim 0: scores[other_id] sim top_users sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_k] if not top_users: return [] # 预测目标用户对未评价景点的评分 pred defaultdict(float) weight_sum defaultdict(float) for other_id, sim in top_users: other_row matrix.loc[other_id] for scenic_id in other_row[other_row 0].index: if user_row[scenic_id] 0: continue pred[scenic_id] sim * other_row[scenic_id] weight_sum[scenic_id] sim ranked sorted(pred.items(), keylambda x: x[1] / max(weight_sum[x[0]], 1e-6), reverseTrue) return [scenic_id for scenic_id, score in ranked[:top_n]]注意这里的评分预测我用的是加权平均而不是简单加权和。因为不同相似用户的权重总量不一样如果不做归一化相似用户越多、评分越高的景点就越占便宜这个细节在答辩时主动讲出来会让老师觉得你是真的理解算法而不是背代码。2.3 协同过滤的经典坑冷启动、稀疏矩阵与热门偏置这节内容是我觉得比算法本身更值钱的部分。直接跑上面的代码你会发现几个问题。冷启动问题新用户没有评分记录皮尔逊相关系数根本算不出来。解决方案是分层推荐有行为数据的用户走协同过滤没有行为数据的用户走热门推荐——按热度值、评分人数、浏览量加权排序。我专门在推荐接口里写了这条降级链路如果user_id不在评分矩阵里或者相似用户为空就自动返回Top热门景点。稀疏矩阵问题当评分矩阵稀疏度超过95%时任意两个用户共同评分过的景点可能只有一两个算出来的相似度非常不可靠。我的处理办法是加惩罚系数共同评分景点越多相似度越可信。def adjusted_similarity(a: pd.Series, b: pd.Series, c: float 5.0) - float: common_cnt ((a 0) (b 0)).sum() base_sim pearson_similarity(a, b) if common_cnt 0: return 0.0 return base_sim * common_cnt / (common_cnt c)热门偏置问题如果只是按评分加权平均推荐热搜景点会出现频率极高推荐列表缺乏个性。我后面的做法是从结果里剔除用户所在城市的景点再加入一个信息流新颖度过滤——预测评分差不多的情况下优先推荐排名靠后、评分人数少的景点。这样做出来的列表更像个性的推荐而不是排行榜。我在实际测试中还碰到过一个隐藏问题同一用户给同一景点重复评分。数据表设计时如果没有唯一约束矩阵会出现行重复pivot_table会自己聚合但Django ORM的查询统计会被干扰。解决办法是在模型层设置UniqueConstraint(fields[user, scenic])或者提前在清洗脚本里按user_id、scenic_id去重保留最新评分。3. Django后端集成推荐引擎如何与Web层优雅解耦3.1 项目结构与数据模型设计推荐算法写完只是第一步更关键的是把它嵌进Django工程。很多人写毕设所有代码全堆在views.py里最后成了一团乱麻。我的项目结构是分层的推荐引擎独立成一个模块不直接依赖Django的ORMtrip_recommend/ ├── recommend/ │ ├── collaborative.py # 协同过滤算法 │ ├── popularity.py # 热度推荐 │ └── utils.py # 数据加载、缓存、时间工具 ├── scenics/ │ ├── models.py # 用户、景点、评分、收藏模型 │ ├── views.py # 视图接口 │ ├── serializers.py # DRF序列化器 │ └── urls.py ├── analysis/ # 可视化数据分析应用 │ ├── views.py # 可视化接口 │ └── charts.py # 图表数据聚合 └── config/ # Django配置文件为什么这么分层有两个好处。第一recommend模块可以在不启动Web服务的情况下用命令行单独测试写单元测试也方便第二答辩时如果老师问你的算法能不能给其他系统复用你可以直接说推荐引擎是独立组件与Web层解耦只需喂评分数据即可。数据模型上核心是用户在Django里的AbstractUser扩展加上景点和评分两个实体。模型代码大致如下from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): nickname models.CharField(max_length32, blankTrue) preference models.CharField(max_length128, blankTrue) # 逗号分隔标签 class Scenic(models.Model): name models.CharField(max_length128) city models.CharField(max_length32, db_indexTrue) province models.CharField(max_length32) longitude models.FloatField(nullTrue, blankTrue) latitude models.FloatField(nullTrue, blankTrue) price models.FloatField(default0) heat models.IntegerField(default0) # 热度值 cover models.URLField(max_length512, blankTrue) description models.TextField(blankTrue) class Rating(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameratings) scenic models.ForeignKey(Scenic, on_deletemodels.CASCADE, related_nameratings) score models.IntegerField(default5, choices[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)]) created_at models.DateTimeField(auto_now_addTrue) class Meta: constraints [ models.UniqueConstraint(fields[user, scenic], nameunique_user_scenic) ]后端接口我用Django REST Framework来写它配合Django自带的序列化器和认证机制非常顺手不需要额外学太多东西。3.2 推荐接口的设计与实现推荐接口最核心的地方在于使用哪个算法、给多少条结果、返回哪些字段、如何保证响应速度。我设计了一个统一的推荐视图参数从GET请求读取返回的JSON里不仅包含推荐景点列表还包含推荐理由来自哪些相似用户这是答辩展示时很加分的点。from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from recommend.collaborative import recommend_for_user, is_cold_user from recommend.popularity import get_hot_scenics class RecommendView(APIView): permission_classes [IsAuthenticated] def get(self, request): user request.user top_n int(request.query_params.get(top_n, 10)) if is_cold_user(user.id): items get_hot_scenics(top_n) else: items recommend_for_user(user.id, top_ntop_n) return Response({ user: user.id, strategy: collaborative if not is_cold_user(user.id) else popular, items: items })这里有个细节Django的request.user默认是同步取数据库的推荐算法本身要扫描评分矩阵如果每次都实时去查性能会很差。我是这样处理的把评分矩阵和景点信息做成全局缓存每五分钟或每次有新的评分写入时刷新一次。因为毕设的数据量有限全局缓存完全够用。如果数据量再大才需要考虑用Redis或者把推荐结果预先离线算好存表。3.3 数据库索引与评分写入的防重设计我在做性能测试时遇到过一个问题评分表的数据量只有几千条但页面加载很慢。后来用query.set_queryset分析发现是很多查询没有命中索引。这里有两个必须加的索引Rating表的user字段和scenic字段以及Scenic表的city字段。我在模型里都加了db_indexTrue之后查询从几十毫秒降到个位数毫秒。评分写入还有一层容易忽略的逻辑用户第二次给同一个景点评分应该更新而不是插入新记录。我的做法是在视图里先查一次存在就更新不存在就创建def rate_scenic(user, scenic_id, score): scenic Scenic.objects.get(idscenic_id) obj, created Rating.objects.update_or_create( useruser, scenicscenic, defaults{score: score} ) # 写入后刷新推荐缓存 refresh_recommend_cache() return obj, created这种细节在答辩时可以主动提用户不会给同一个景点打两次分但系统要处理重复点击提交的场景所以要用update_or_create来保证幂等性。4. 数据可视化层把景点热度、用户画像与地域流向搬上可视化大屏4.1 可视化技术选型Pyecharts还是ECharts可视化是旅游推荐系统最容易出视觉效果的部分也是老师最直观感受到数据分析能力的入口。我先在Pyecharts、ECharts、Highcharts三个方案里做了对比。Pyecharts的优点是纯Python生成图表不用写前端JSDjango模板里直接渲染HTML即可学习成本最低适合不太熟悉前端的同学。ECharts功能更强地图、词云、数据大屏组件丰富但需要你去写JavaScript和ajax请求前后端分离做起来更灵活。我的最终选择是混合方案地图类图表和热点大屏用ECharts因为它的地图支持和交互效果是碾压级的简单的柱状图、折线图用Pyecharts快速生成。这样答辩时可以展现出我既会Python生成图表也能写前端ECharts配置覆盖面更广。4.2 四张核心图表的数据聚合与展示方案可视化的核心不是画图而是画图前的数据聚合。我做了四个模块分别对应不同数据类型景点热度Top10排行。从Rating表按景点分组计算平均评分与评分人数评分人数就是热度再和景点热度值做加权生成横向柱状图。城市景点数量与用户地域分布。从Scenic表按城市统计数量和地图坐标关联生成地图气泡图。这里用ECharts的geo组件最合适数据格式是这样option { geo: { map: china, roam: true }, series: [{ type: effectScatter, coordinateSystem: geo, data: [ { name: 北京, value: [116.46, 39.92, 128] }, { name: 成都, value: [104.06, 30.67, 96] } ] }] };用户偏好词云。把用户注册时填写的preference字段和评分高的景点类型字段拆分统计用词云展示自然、人文、古镇、海滨、亲子、摄影等关键词。词云用Pyecharts的WordCloud组件非常快。出游时间热度趋势。按月份统计评分和收藏的行为量用折线图展示五一、十一、暑期这几个高峰。我做数据模拟时有意识地让6-8月和10月的数据量偏高这样画出来的折线图趋势非常漂亮讲解故事也自然。数据分析可视化部分我建议加一张用户-景点评分离散度散点图横轴是用户评分均值纵轴是评分方差。这张图可以从统计学角度说明不同用户的评分习惯差异也能解释为什么算法要用皮尔逊相关系数而不是余弦相似度——我在项目里真的画了这张图答辩现场很多老师就会顺着图意的逻辑往下问你提前准备这一层分析非常划算。4.3 Django与前端可视化交互的性能细节可视化页面如果同时渲染多张图表性能问题会很突出。我踩过的坑是第一次实现时四个图表同时由后端的views.py同步计算一个页面要等所有聚合查询跑完才返回耗时3秒以上。后来拆成异步接口首页先加载骨架map接口、ranking接口、trend接口分别独立请求前端拿到数据后分别渲染。另一个坑是ECharts地图在城市级别渲染时需要GeoJSON数据Django静态文件默认不会帮你把china.json处理得太顺。要记得关闭Debug模式时用collectstatic收集所有前端静态资源并且在模板里明确指定静态文件路径否则答辩现场打开只有空白页面。这个坑很多同学都遇过最好把STATICFILES_DIRS配置提前写好STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static]5. 部署排错、推荐效果评估与答辩加分点5.1 本地运行到线上部署的排查过程我整个项目调试过程里最折磨人的一次是Windows本地上跑得好好的一到服务器上就崩。排查链是这样的先看终端日志发现报错竟然是ModuleNotFoundError: No module named MySQLdb。原因是本地我用SQLite服务器上换成了MySQLDjango默认的MySQL驱动不受支持需要安装mysqlclient而mysqlclient在Python 3.10以上版本在Windows上很难编译。解决办法要么换用PyMySQL并在项目配置里加一行import pymysql; pymysql.install_as_MySQLdb()要么安装对应版本的mysqlclient预编译包。我选了PyMySQL方案配置文件就可以正常使用MySQL数据库。接着又遇到静态文件404。部署环境Debug关闭后Django默认不会由框架处理静态文件需要先执行python manage.py collectstatic把文件收集起来再交给Nginx托管。这一步不做ECharts的js全部加载不出来页面一片空白。这些问题的共性是本地跑通不等于部署跑通配置和环境变量之间的差异才是坑密集区。5.2 推荐效果怎么做离线评测而不是只说效果很好答辩时老师最常问的一句话是你怎么证明你的推荐算法是有效的?如果你回答我觉得挺准的基本就结束了。我做了三个指标的计算并且把结果做成了三张可视化图表放在附录里。准确率PrecisionN推荐的N个景点里有多少个被用户实际评分过4分以上。召回率RecallN用户所有高评分景点里有多少个出现在推荐列表中。覆盖率Coverage推荐结果覆盖了多少比例的景点这个指标可以说明系统不是永远只推热门。离线评测的做法是从评分数据里随机抽20%作为测试集剩下80%作为训练集训练协同过滤模型然后在测试集上计算指标。我当时得到的结果大约是这样的指标UserCFItemCF热门基线Precision50.420.380.25Recall50.310.270.17覆盖率36%44%8%表格在答辩PPT里直接放出来老师立刻知道你做了对照实验而且理解了不同指标的取舍。覆盖率这里有交易ItemCF覆盖率更高但Precision略低UserCF推荐更精准却更容易集中在热门景点上。你的毕设可以选UserCF做主线ItemCF做对比这就是完整的实验设计。5.3 答辩高频问题与大模型衔接的扩展思路我整理了半年里辅导学弟学妹时被问得最多的问题建议你提前准备答案。第一个问题是为什么不用深度学习或大模型标准的回答思路是结合数据量和场景毕设数据规模只有几千到几万条评分记录深度学习模型容易过拟合而且训练资源有限协同过滤算法可解释性强推荐结果可以追踪到哪些相似用户产生了影响这在旅游场景里更重要。同时可以补充如果后续数据量扩大模型可以迁移到WideDeep或基于GNN的推荐框架。至于大模型可以这样表述——大语言模型适合做行程规划的生成式对话比如把推荐结果组织成3日游路线但作为毕设主体传统推荐算法配合统计可视化已经足够体现完整性和算法深度引入外部大模型API会带来额外的成本和不可控性。这个说辞既承认了大模型的价值又解释了为什么没把它作为核心老师一般会认可。第二个问题是推荐结果为什么不全是个性化的要指出冷启动的存在并说明系统如何降级到热门推荐这正好引出你的分层设计。第三个问题是如果用户数量从1000涨到1000万你的算法还能跑吗这个问题的标准答案是基于用户的协同过滤时间复杂度是O(M×N)级别所以大数据量下要改成基于物品的协同过滤物品数远小于用户数时更划算或者用矩阵分解、LSH近似近邻搜索。提前把这段话记住比现场想好得多。最后我再分享两个实际做项目时特别有效的加分操作。第一个在系统里加一个推荐原因展示功能比如因为用户A、B与你兴趣相似他们喜欢西湖所以推荐给你。协同过滤的相似用户成员是现成的只要把TopK相似用户的名字在推荐接口里一并返回前端展示出来整个项目的智能感立刻提升一个档次。第二个把评分数据模拟脚本的随机种子固定下来保证每次运行demo演示时数据一致不会出现上一秒推荐故宫、下一秒推荐外滩的尴尬。这两个小改动成本很低但对答辩观感的作用非常大。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →