
做毕业设计选方向的时候很多人在“算法研究、应用开发、数据分析”这三座大山之间反复横跳。选纯算法怕数学不过关选纯增删改查又怕答辩没亮点。今天拆解的这套《Python 旅游景点智能推荐系统》可以说是一个比较取巧的组合方案用 Django 做 Web 框架用协同过滤推荐算法做核心亮点再叠加数据分析可视化大屏作为展示层。既能让评委第一眼看到“推荐系统”这种有技术含量关键词又能在演示环节用大屏和数据图表撑住场面是一个人能独立完成、工期可控、答辩上限还比较高的毕业设计选题。这套系统到底怎么搭、协同过滤在 Django 里怎么落地、可视化数据从哪来、哪些环节最容易翻车我按实际做项目的顺序给你完整拆一遍。1. 整体设计与思路拆解1.1 为什么旅游景点推荐适合做毕业设计选旅游这个领域不是因为它简单而是因为它的数据特征和推荐算法的匹配度很高。景点有明确的属性维度城市、分类、门票价格、评分、评论量用户也有明确的交互维度收藏、评分、浏览记录。有用户、有物品、有行为这不就是协同过滤最标准的数据形态。更关键的是旅游数据的“可解释性”非常好。推荐出来的景点评委一眼就能看出“因为用户去过杭州所以推荐了乌镇”这种逻辑是合理的。相比之下你要是做个电影推荐评委对调参结果也很难直观判断好坏但旅游景点推荐的效果普通人扫一眼就能感知到这对答辩演示是天然的优势。另外这个题目天然带“大数据分析”属性。景点热度排行、城市分布、价格区间占比、游客评分分布这些数据拿出来就能做成漂亮的可视化大屏。评委不会关心你的数据是爬来的还是模拟生成的他们关心的是你有没有“数据思维”——能不能从一堆数据里提取出有业务含义的结论。这一点恰恰是很多只做增删改查的毕设项目最缺的东西。1.2 技术选型背后的取舍逻辑技术栈看似平平无奇但每一层都是经过权衡的。后端用 Django 而不用 Flask核心原因有三个。第一Django 自带 ORM、Admin 后台、用户认证体系这些基础能力能省掉大量重复代码开发效率明显更高。第二Django 的 ORM 抽象层对你做数据分析和推荐算法很友好可以直接用QuerySet快速查出来用户行为数据转成矩阵不用像 Flask 那样自己拼 SQL。第三答辩时评委问“你的系统安全性怎么保证”Django 自带的 CSRF 防护、XSS 过滤、用户认证机制可以直接拿出来讲这些都是实打实的安全加分项。推荐算法选择协同过滤而不是深度学习或者大模型这一点很多同学可能会纠结。标题里虽然挂着“大模型”的热词但实际落地时你要冷静。毕业设计的核心目标是让项目完整跑通而不是在算力不足的情况下硬上大模型。协同过滤虽然经典但它包含完整的“相似度计算 → 最近邻选取 → 评分预测 → TopN 推荐”链路足够讲清楚推荐系统的核心思想。真正明智的做法是主体用协同过滤搞扎实在扩展方向里提出“未来可以引入大模型生成个性化推荐理由”的展望。这样既有落地成果又有前瞻性思考答辩效果反而更好。算法实现不依赖专门的推荐库。很多人会直接上surprise或者implicit这种现成库但我不建议在毕设里这么做。用numpy手写协同过滤核心逻辑代码量也就一百行上下但你在答辩时可以非常自信地说“这个算法的每一个公式我都清楚”一旦评委追问细节你也不会露怯。再说白一点答辩时让你现场推导协同过滤公式的场景是完全可能出现的用第三方库的话你连自己的代码都解释不清。1.3 系统模块的完整规划一套能打的旅游推荐系统至少要把下面这几个模块规划清楚一是用户模块负责注册、登录、个人信息管理。Django 自带的User模型直接扩展即可再加一个用户偏好表记录年龄、常居城市、偏好景点类型。二是景点数据模块包含景点基础信息的管理名称、城市、分类、门票价格、评分、评论数、经纬度、简介、图片地址。这些数据是后续推荐和可视化的数据底座。三是用户行为模块这是协同过滤的燃料。包括用户对景点的评分、收藏、浏览记录。如果是模拟数据要保证行为数据分布合理否则后面算法跑出来全是一堆诡异结果。四是推荐引擎模块协同过滤算法的核心实现包括用户相似度计算、最近邻查找、评分预测、推荐列表生成。这个模块要独立封装方便前端直接调用。五是数据分析与可视化模块对景点和用户行为数据做多维统计通过接口输出给前端用图表和大屏展示。包括热门景点排行、城市分布、类别占比、价格区间、评分分布、用户活跃趋势等。六是后台管理模块Django Admin 直接上管理员可以管理景点信息、查看用户数据、手动调整推荐结果。别小看后台这是系统完整性的重要拼图也方便你自己造数据测试。这个模块划分配置下来整个系统的边界就非常清晰了。后面写代码就像填空一样一个模块一个模块填进去就行。2. 协同过滤核心算法与数据分析详解2.1 协同过滤推荐算法的底层原理协同过滤的核心假设很简单跟你兴趣相似的用户喜欢的东西你大概率也会喜欢。它不需要任何景点内容特征只依赖用户的历史行为数据所以叫“协同”过滤。基于用户的协同过滤UserCF实现上分四步走。第一步搭建用户-景点评分矩阵行是用户列是景点矩阵元素是用户对景点的评分。第二步计算用户之间的相似度常用的是余弦相似度。第三步找到和目标用户最相似的 K 个用户作为“邻居”。第四步根据邻居的评分情况预测目标用户对未评分景点的得分取 TopN 作为推荐结果。相似度计算的数学公式长这样similarity(u, v) (u · v) / (||u|| × ||v||)每个用户对所有景点的评分构成一个向量余弦相似度衡量的是两个向量在方向上的接近程度。这样处理的好处是天然规避了用户评分尺度不同的问题有人喜欢打高分有人喜欢打低分但评分趋势一致的两个用户余弦相似度依然会比较高。在预测评分时还有个细节要注意最好做均值归一化。基本思路是先把用户向量中心化减去该用户的平均评分在相似度计算完成后再把平均值加回来。这样能降低用户评分习惯对预测结果的干扰。不做归一化的后果是一个平均只给 3 分的用户和一个平均给 5 分的用户即使兴趣完全一致相似度计算也可能不准。2.2 数据分析应该分析哪些维度数据分析这部分建议从以下六个维度入手每个维度拿出来都能做一张好看的图表而且都有明确的业务解释。第一个是景点热度分析统计每个景点的浏览量、收藏量、评论量算一个综合热度分用柱状图展示 Top10 热门景点。第二个是城市分布分析统计全国各城市收录的景点数量和热度用地图或者柱状图呈现“哪些城市是旅游热点”。第三个是景点类别分析把景点按自然风光、历史古迹、主题公园、博物馆等类别归类看类别占比饼图即可。第四个是价格区间分析按免费、0-50、50-100、100-200、200以上这五个区间统计景点数量直观反映景点定价结构。第五个是评分分布分析看评分集中在什么区间正态分布还是偏态分布可以顺便讲一讲“评分通胀”现象。第六个是用户行为分析统计注册用户数、活跃用户数、每日评分次数用折线图展示用户活跃趋势。这个六个维度串起来正好形成一条完整的数据分析逻辑链“资源端有什么城市/类别/价格→ 用户端怎么看评分/热度→ 平台端怎么优化推荐/运营”。答辩时顺着这条线讲评委会觉得你确实理解数据背后的业务意义。2.3 可视化大屏的选型与布局建议可视化部分首选 ECharts这是目前国内用得最多的前端图表库社区资料丰富、中文文档完善、地图组件开箱即用。用 CDN 方式引入即可不用折腾 npm 工程化对后端为主的同学非常友好。大屏布局建议采用经典的分栏结构。顶部一行放标题和核心指标卡比如“收录景点总数”“注册用户数”“平均评分”“热门城市”。中部区域左中右三栏左栏放热门景点 Top10 柱状图和类别占比饼图中间放全国景点城市分布地图右栏放价格区间分布图。底部一行放用户活跃趋势折线图和评分分布直方图。这个布局充分利用了屏幕空间信息层级也清楚一眼就能看到重点。需要注意的一个技术点ECharts 的地图需要 GeoJSON 数据。中国地图的 GeoJSON 可以找现成的静态资源答辩演示时如果没有外网环境一定要把 GeoJSON 文件提前下载到本地静态目录里。这个坑我见过太多人踩了演示现场打开页面地图一片空白非常尴尬。3. 实操过程与核心环节实现3.1 环境准备与 Django 项目搭建建议直接用 Python 3.10 或者 3.11太新的 Python 版本某些库可能还没有适配的预编译包。创建虚拟环境后激活然后安装依赖。python -m venv venv # Windows venv\Scripts\activate # macOS/Linux source venv/bin/activate pip install django4.2 numpy pandas mysqlclientDjango 版本建议锁死 4.2不要太新。Django 5.x 有一些行为变化网上很多教程还是基于 4.2 写的照着做不容易出问题。数据库这里给两个选择想省事就用默认的 SQLite数据量在万级以下完全够用想展示工程能力就上 MySQL但要多一步建库配置。创建项目和应用django-admin startproject tourism_project cd tourism_project python manage.py startapp recommend python manage.py startapp visualizationrecommend应用放模型、推荐算法和推荐相关的视图visualization应用专门处理数据统计和图表接口。把应用名注册到settings.py的INSTALLED_APPS里再配置好数据库连接迁移一下。python manage.py makemigrations python manage.py migrate python manage.py createsuperuser到这里骨架搭好了能启动、能进后台这只是万里长征第一步真正的硬骨头在模型设计和算法代码。3.2 数据模型设计与模拟数据生成模型设计直接决定后面代码好不好写。我建议按三张核心表展开。景点表Sight是数据底座字段要设计得够用但不冗余。初始版本可以这样# recommend/models.py from django.db import models class Sight(models.Model): name models.CharField(max_length128, verbose_name景点名称) city models.CharField(max_length64, db_indexTrue, verbose_name所在城市) category models.CharField(max_length32, verbose_name景点分类) price models.FloatField(default0, verbose_name门票价格) rating models.FloatField(default0, verbose_name评分) comment_count models.IntegerField(default0, verbose_name评论数) longitude models.FloatField(nullTrue, blankTrue, verbose_name经度) latitude models.FloatField(nullTrue, blankTrue, verbose_name纬度) description models.TextField(blankTrue, verbose_name景点简介) image_url models.URLField(blankTrue, verbose_name图片地址) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: ordering [-rating] verbose_name 景点信息 verbose_name_plural 景点信息 def __str__(self): return self.name用户评分表UserRating是推荐算法的燃料。这里的核心设计在于每个用户对每个景点只能有一条评分记录用联合唯一约束保证数据不脏。class UserRating(models.Model): user models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, verbose_name用户 ) sight models.ForeignKey( Sight, on_deletemodels.CASCADE, related_nameratings, verbose_name景点 ) score models.FloatField(verbose_name评分) created_at models.DateTimeField(auto_now_addTrue, verbose_name评分时间) class Meta: unique_together (user, sight) verbose_name 用户评分 verbose_name_plural 用户评分 def __str__(self): return f{self.user.username}-{self.sight.name}-{self.score}另外建议加一张收藏表UserCollect记录用户收藏景点的时间用于活跃趋势分析。字段大概就是 user、sight、created_at不需要太多花哨东西。模型建好之后最关键的一步是生成模拟数据。推荐算法能不能跑出“像样”的结果完全取决于造数质量。如果数据乱造一通用户之间没有兴趣分布规律协同过滤算出来的推荐结果就是随机的答辩时演示效果极差。生成模拟数据时要注意分布逻辑。比如定义 10 个兴趣人群每个群体对特定类别景点评分高4-5 分对无关类别评分低1-2 分再让一部分用户成为“活跃用户”评分记录多一部分用户是“沉默用户”评分记录少但也要有。用 Faker 库造用户名用随机数控制评分分布循环批量写入。有了这样有规律的数据协同过滤才能找到相似用户推荐结果才“看起来聪明”。3.3 协同过滤推荐算法核心代码实现算法模块我单独放到recommend/services.py里核心逻辑用numpy实现。第一步是把 QuerySet 转成评分矩阵。# recommend/services.py import numpy as np from .models import UserRating, Sight def build_user_item_matrix(ratings): 把用户评分数据转成用户-景点评分矩阵 user_ids sorted(set(r.user_id for r in ratings)) sight_ids sorted(set(r.sight_id for r in ratings)) user_index {uid: i for i, uid in enumerate(user_ids)} sight_index {sid: j for j, sid in enumerate(sight_ids)} matrix np.zeros((len(user_ids), len(sight_ids))) for r in ratings: i user_index[r.user_id] j sight_index[r.sight_id] matrix[i][j] r.score return matrix, user_ids, sight_ids, user_index, sight_index第二步计算用户相似度矩阵。这里用向量化的方式写余弦相似度比双重循环快了不止一个量级。先对评分矩阵做 L2 归一化然后一次矩阵乘法就是完整的相似度矩阵。def calc_user_similarity(matrix): 计算用户之间的余弦相似度矩阵 norm np.linalg.norm(matrix, axis1, keepdimsTrue) norm[norm 0] 1 normalized_matrix matrix / norm sim_matrix normalized_matrix normalized_matrix.T return sim_matrix第三步预测评分。给定一个目标用户遍历所有未评分的景点找到相似度最高的 K 个邻居加权平均预测分数。这里我加了一个小优化只有当邻居对该景点有过评分时才参与预测计算。def predict_score(active_idx, item_idx, sim_matrix, rating_matrix, k10): 预测用户对指定景点的评分 sims sim_matrix[active_idx].copy() sims[active_idx] 0 neighbor_indices np.argsort(-sims)[:k] numerator 0.0 denominator 0.0 for n_idx in neighbor_indices: if sims[n_idx] 0: continue if rating_matrix[n_idx][item_idx] 0: continue numerator sims[n_idx] * rating_matrix[n_idx][item_idx] denominator sims[n_idx] if denominator 0: return 0.0 return numerator / denominator第四步生成 TopN 推荐列表。遍历所有该用户没有评分的景点执行预测按预测分数倒序截取 N 个。为了防止每次都计算全量用户相似度影响性能可以预计算相似度矩阵传进来复用。整个推荐函数串起来长这样def recommend_for_user(user_id, top_n10, k10): ratings UserRating.objects.select_related(user, sight).all() if len(ratings) 10: return [] # 冷启动保护 matrix, user_ids, sight_ids, u_idx, s_idx build_user_item_matrix(ratings) if user_id not in u_idx: return [] sim_matrix calc_user_similarity(matrix) active_idx u_idx[user_id] predictions [] for sight_id in sight_ids: item_idx s_idx[sight_id] if matrix[active_idx][item_idx] ! 0: continue pred predict_score(active_idx, item_idx, sim_matrix, matrix, k) if pred 0: predictions.append((sight_id, pred)) predictions.sort(keylambda x: x[1], reverseTrue) return predictions[:top_n]这套代码虽然朴素但胜在干净、完整、讲得清。答辩时你完全可以手画一个流程图然后说清楚每一步的作用。这才是推荐系统该有的底层理解比跑一个调好的黑盒模型值钱多了。3.4 可视化大屏与数据分析接口实现可视化部分的核心思路是Django 后端做数据统计输出 JSON 接口前端 ECharts 拉接口渲染图表。在visualization/views.py里写统计逻辑。比如统计热门景点 Top10from django.http import JsonResponse from recommend.models import Sight, UserRating, UserCollect def hot_sights_api(request): sights Sight.objects.order_by(-comment_count, -rating)[:10] data [{ name: s.name, value: s.comment_count, } for s in sights] return JsonResponse({data: data})统计城市分布from django.db.models import Count def city_distribution_api(request): result ( Sight.objects .values(city) .annotate(countCount(id)) .order_by(-count) ) data [{name: item[city], value: item[count]} for item in result] return JsonResponse({data: data})前端大屏页面放在templates/visualization/dashboard.html里用 ECharts 的 CDN 引入库写完布局后直接在DOMContentLoaded里发请求。核心思路就一个先渲染页面骨架再fetch接口拿数据最后用拿到的数据setOption更新图表。这里有一个非常重要的实操细节ECharts 图表容器必须有明确的高度。很多人图表渲染不出来不用怀疑别的先看div高度是不是 0。大屏布局所有容器一定要用百分比高度或者固定像素高度尤其是body和html也要设置成height: 100%否则图表区域塌陷什么都看不到。另一个细节是接口数据格式要和 ECharts 对齐。ECharts 柱状图一般需要{ name: xxx, value: 20 }的数组格式饼图则需要data: [{ name: ..., value: ... }]。后端在组织 JSON 结构的时候就要想好前端要什么别把数据原样丢出去再在前端做复杂的二次转换那是自找麻烦。3.5 Django 视图层与推荐结果展示推荐结果要通过 Web 页面展示给用户。我的建议是做两个展示页面一个是登录后的“为你推荐”页面展示协同过滤的 TopN 推荐结果每张卡片显示景点名称、城市、分类、价格、评分、推荐理由另一个是“热门景点”页面作为未登录用户或者冷启动用户的兜底推荐。两个页面分别对应“个性化推荐”和“热门榜”商业模式上的解释是新用户没有行为数据时推热门有行为数据后逐渐转个性化。视图层代码大概长这样from django.shortcuts import render from django.contrib.auth.decorators import login_required from .services import recommend_for_user login_required def my_recommendation(request): user_id request.user.id recommendations recommend_for_user(user_id, top_n10) sights Sight.objects.filter(id__in[s[0] for s in recommendations]) sight_map {s.id: s for s in sights} result [] for sight_id, score in recommendations: s sight_map.get(sight_id) if s: result.append({ sight: s, predicted_score: round(score, 2), }) return render(request, recommend/list.html, { recommendations: result, is_empty: len(result) 0, })模板里用卡片布局展示。每张卡片展示景点图片、名称、城市标签、评分和“预测评分”标识。预测评分这个设计亮点在于它让“推荐算法”的存在感变得很强评委一眼就能看出这是基于算法算出来的推荐而不是简单的按评分排序。4. 常见问题与排查技巧实录4.1 冷启动问题怎么处理冷启动是推荐系统最著名的难搞问题毕设里要正确认识它并给出一个让评委认可的解决方案。新注册用户没有任何行为数据协同过滤在矩阵里找不到他自然无法推荐。两层方案兜底。第一层用户注册时增加“偏好选择”环节勾选感兴趣的景点类别和预算区间基于这些偏好做规则过滤初步推荐。第二层对没有任何行为的用户直接推荐全站热门景点也就是“Trending 榜”。等用户产生了评分或收藏行为后再切换到协同过滤个性化推荐。这个“从热门到个性化”的渐进策略既是工程实践也是答辩时可以重点讲的产品思维。4.2 评分矩阵稀疏导致推荐质量差造数造得再认真评分矩阵天然是稀疏的。一个用户撑死评分几十个景点而景点总数可能上千矩阵里 95% 以上都是 0。这会造成预测评分时找不到足够的共同评分用户分母为零的尴尬情况。应对思路有三个。一是过滤掉行为数据过少的用户比如评分少于 5 条的用户不参与相似度计算直接走热门推荐。这个过滤逻辑同时能提高计算速度。二是设置最低共同评分数两个用户至少对 3 个相同景点评过分才认为他们的相似度是可信的。三是对最终预测结果为 0 的情况做兜底拿全站评分均值填充并标记为“保守估计”。4.3 Django ORM 查询性能瓶颈当模拟数据量达到几万条时同步查询所有评分会开始变慢。主要优化手段是先select_related(user, sight)避免每次访问关联对象都触发额外 SQL 查询。另一个技巧是把用户行为数据提前缓存到cache或者内存里定期刷新避免每次请求都重新跑矩阵。推荐算法这类计算密集型逻辑不适合放在同步请求里频繁执行比较合理的架构是定期更新推荐结果用户拉取时直接读缓存。毕设阶段可以用最简单的方案——每次请求都计算但加上lru_cache装饰器缓存结果配合定时任务或者手动触发更新。这样逻辑上干净实现上也简单。4.4 前端图表常见翻车点做可视化大屏这一块最容易出问题的几个点我列一下碰到了直接对号入座。ECharts 图表不显示优先检查容器高度地图组件空白检查 GeoJSON 是否加载成功图表数据全是 0检查接口返回的字段名是否和前端data字段名一致日期和时间轴的格式化问题检查是不是把字符串传给了数值轴横坐标全部挤成一团大屏样式错乱基本是单位没有统一用百分比。还有一个特别注意ECharts CDN 资源尽量下载到本地静态目录。答辩现场不一定有校园网更不要指望现场网络有多快所有资源提前本地化这也是一种职业素养。4.5 常见问题速查表问题现象根本原因解决措施推荐结果全是随机热门用户相似度矩阵全为 0没有有效邻居检查评分数据是否有规律减少参与计算的最低评分条数预测评分出现 NaN分母为零没有邻居对目标景点评分预测函数里加零值判断返回 0 或用均值兜底推荐页面 500 报错推荐代码里访问了不存在的索引加用户索引判断用户无行为时直接返回空列表ECharts 图表空白容器高度为 0设置固定高度或百分比高度html/body 也设置 height地图不显示GeoJSON 没加载或跨域本地化 GeoJSON 文件用 JSONP 或 fetch 方式加载Django 后台中文乱码数据库字符集不是 utf8mb4MySQL 建库时用 utf8mb4或者直接用 SQLite数据造完推荐结果没区分度模拟数据太平均用户之间没有兴趣差异造数时引入用户群体偏好让不同人群评分有明显倾向4.6 闭坑建议答辩演示场景准备答辩演示是整个项目交付的最后一公里很多项目写得好但演示翻车核心原因是没做好场景预演。建议准备三套演示数据。第一套是小数据集100 个用户、100 个景点保证算法毫秒级响应用于演示流畅的交互流程。第二套是用户全景数据几千个用户、上千个景点用于展示可视化大屏的丰富图表。第三套是针对性测试数据专门为“冷启动”“相似用户推荐”设计。比如你提前创建一个“喜欢山水景点”的用户并评了一堆分答辩时现场登录这个用户看到推荐结果全是黄山、九寨沟、张家界这类景点评委一下子就理解了算法的逻辑。还有一个技巧故意准备一个没有评分的“新用户”现场演示系统如何从热门榜开始给他推荐然后解释冷启动策略。这种“主动暴露系统边界”的做法反而会让评委觉得你的设计是经过深思熟虑的。我自己在做这类毕设项目时的体会是算法堆得再高不如把一条核心链路做闭环。从用户注册、偏好选择、产生行为数据、算法计算、推荐结果展示再到数据大屏的全局统计这一整条链路只要通顺流畅项目就已经超过大多数把精力只花在界面上的同类作品了。推荐系统最迷人的地方不在于算法本身有多炫而在于你亲手设计了一条从数据到价值的转化路径。这个项目做完之后如果想继续扩展方向也很多引入时间因素做序列推荐、结合景点地理位置做 Context 感知推荐、甚至用大模型生成个性化推荐理由文本都是可以持续深挖的点。先把当前这版跑稳后面都是你的发挥空间。