旅游推荐系统实战:Python数据分析与可视化完整链路
发布时间:2026/9/15 4:08:22 作者:尧图编辑部 阅读量:1,286

简介面向高校计算机类毕业生与希望积累真实项目经验的 Python 学习者基于 DjangoMySQL协同过滤算法的旅游推荐数据分析可视化项目源码包也适配课程设计、期末大作业等场景。项目实现了用户登录注册、个人信息管理、景区浏览、评分收藏与推荐以及管理员对景区信息、分类和用户信息的管理等功能前后端功能划分明确操作流程完整逻辑清晰。资源共 422 个文件以 js、css、html 等前端页面样式文件为主辅以 py 后端代码、json 配置、jpg 图片、csv 数据及 sql 数据库脚本压缩包大小约 9.36MB目录结构清晰便于按模块阅读和二次开发。包内另含部署说明文档覆盖环境配置与启动步骤可帮助使用者在本地快速跑通项目。目前已有 81 人学习下载适合需要完整线上项目参考、并希望据此快速搭建毕业设计的 Python/Django 初学者。1. 旅游推荐数据分析可视化毕业设计怎么把数据变成可交付的东西很多人做旅游推荐系统调一个协同过滤模型输出Top10景点就结束了。但放到答辩现场老师更想看到的是“你的数据从哪来、清洗后长什么样、为什么推荐这个而不是那个”。这个标题把三个关键词合在一起基于Python的推荐算法、数据分析、可视化。它的价值不在算法多深而在把一条完整的数据链路跑通——从景点数据清洗到推荐结果计算再到页面上的地图和图表。适合正在做毕业设计或想补齐数据展示能力的人。接下来会按数据清洗、推荐算法、可视化部署的顺序把每个环节可复现的命令和代码给出来。2. 数据清洗与数据分析旅游景点数据怎么变成推荐可用的特征2.1 数据源选型与字段设计数据是推荐系统的地基。做旅游推荐常见的数据来源有两种一是爬取公开的景点评论和评分二是用模拟生成的测试数据。毕业设计不推荐一上来就爬虫因为反爬、验证码会耗掉大量时间。我一般会给实训学生一个CSV结构字段包括景点ID、名称、城市、类型自然风光、历史古迹、主题乐园等、评分、门票价格、游玩时间、评论关键词。这样的数据既能支撑基于内容的推荐也方便做分组统计。有趣的是很多研究在做基于内容的推荐时只关注评分和标签而忘了地理位置。旅游推荐和电影推荐最大的区别在于“距离成本”。所以在字段设计时加一个“所属区域”字段可以复用城市级筛选。数据规模不用大100个景点、5000条评论足以跑通全流程。2.1.1 字段说明字段名类型含义推荐中的用途spot_idint景点唯一ID主键spot_namestr景点名称展示citystr所属城市区域筛选categorystr景点类型特征ratingfloat平均评分排序权重comment_tagsstr评论关键词逗号分隔文本特征recommended_timestr建议游玩时长过滤条件2.2 用pandas完成数据清洗和聚合统计拿着原始CSV不能直接进模型。常见问题包括评分列被读成字符串、评论关键词有空值、城市名有空格。用pandas清洗是标准做法。下面的代码可以当作模板import pandas as pd df pd.read_csv(travel_data.csv, encodingutf-8-sig) # 去除前后空格统一字符串格式 df[city] df[city].str.strip() df[category] df[category].str.strip() # 评分转数值解析不了的行置NaN df[rating] pd.to_numeric(df[rating], errorscoerce) # 删除关键字段缺失的行 df df.dropna(subset[spot_name, rating]) # 评论关键词空值用“一般”补齐 df[comment_tags] df[comment_tags].fillna(一般) # 按城市和类型统计数量观察数据分布 city_stat df.groupby(city).size().reset_index(name景点数) category_stat df.groupby(category)[rating].mean().reset_index() print(city_stat.head())这段代码做了五件事去空格、评分转数值、删缺失、补关键词、分组聚合。注意errorscoerce是必须的因为爬下来的评分可能混入“暂无”这类文本直接astype(float)会报错。utf-8-sig是为了让Excel另存的CSV能正常读取中文字段。分组统计的结果是后面可视化“城市景点数”“分类平均评分”两张图表的数据基础。如果你发现某个城市景点数异常少比如只有1条别急着删先看是不是城市字段有别名比如“北京市”和“北京”同时存在。这是数据分析里最常见的脏数据坑。2.3 数据分析指标与可视化前的数据准备清洗后做聚合是为了回答几个具体问题哪个城市景点最多、哪类景点平均评分最高、价格区间分布如何。这些图表标题会直接出现在可视化页面上所以指标口径要提前定好。推荐一个实用的做法把清洗逻辑封装成函数然后输出一份“干净数据集”的副本。后续所有图表和推荐模块都读取这份副本而不是反复重跑原始数据。也就是把数据处理和业务展示解耦不然每次改动一个过滤条件所有图表都要跟着重新算。def clean_data(path: str) - pd.DataFrame: # 完整清洗流程返回干净的DataFrame df pd.read_csv(path, encodingutf-8-sig) df[rating] pd.to_numeric(df[rating], errorscoerce) df df.dropna(subset[rating]) return df clean_df clean_data(travel_data.csv) clean_df.to_csv(clean_travel_data.csv, indexFalse, encodingutf-8-sig)注意to_csv时indexFalse很重要。如果不设读出来的数据会多出一列Unnamed索引后续按spot_id做分组会失败这也是一个常见疏漏。到这里数据层面已经可以把“城市景点数量”“分类评分均值”输送给可视化模块同时把comment_tags文本保留给推荐算法使用。提示在毕业设计说明书里数据清洗部分建议附上清洗前后行数对比表。例如原始2007行清洗后1985行缺失22条。答辩时这个数字比“我用了pandas”有说服力得多。3. 推荐算法落地基于内容的旅游推荐怎么做才不“空泛”3.1 为什么毕业设计选基于内容而不是协同过滤协同过滤看着高级但在100个景点的数据量下效果很差并且冷启动阶段新景点没有历史评分就无法推荐。更现实的原因是毕业设计答辩时老师一定会问“你的推荐依据是什么”基于内容可以很清楚地说“因为用户选了历史古迹所以推荐其他历史古迹中相似度最高的”。这种可解释性对毕业设计非常加分。基于内容的推荐核心逻辑是“给用户看过/喜欢的对象算特征找出库里最相似的”。旅游场景里特征既可以是景点类型、城市也可以是评论关键词的文本向量。用一个经典的文本相似度方法就能完成。3.2 用TF-IDF构建景点特征向量把每个景点的“类型 城市 评论关键词”拼成一个文本然后做分词、向量化、计算余弦相似度。这样推荐结果在语义上更合理。比如用户看过“故宫”评论关键词是“历史、宏伟”那么“颐和园”“天坛”这种历史古迹且关键词相近的景点被排到前面。代码示例import jieba from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity # 拼接文本特征 def build_full_text(row): return .join([row[category], row[city], str(row[comment_tags])]) # 分词处理使用空格连接 def tokenize(text): return .join(jieba.cut(text)) # 计算整个数据集的相似度矩阵 def build_similarity_matrix(df): df[content] df.apply(build_full_text, axis1) df[content_tokenized] df[content].apply(tokenize) vectorizer TfidfVectorizer() tfidf_matrix vectorizer.fit_transform(df[content_tokenized]) return cosine_similarity(tfidf_matrix, tfidf_matrix) def recommend(spot_id, df, similarity_matrix, top_k5): idx df.index[df[spot_id] spot_id].tolist()[0] scores list(enumerate(similarity_matrix[idx])) scores sorted(scores, keylambda x: x[1], reverseTrue) scores [s for s in scores if s[0] ! idx][:top_k] result df.iloc[[s[0] for s in scores]][[spot_name, city, category]].copy() result[similarity] [round(s[1], 4) for s in scores] return result说明逻辑build_full_text把结构化字段拼成一句话jieba.cut负责中文分词。TfidfVectorizer在合并后的语料上计算每个词的TF-IDF权重这样“故宫”出现次数多的景点会被认为更有代表性。cosine_similarity计算两两景点的相似度矩阵中sim[i][j]就是第i个景点和第j个景点的相似度。参数说明top_k5控制推荐数量答辩时可以改成10看召回更多。TfidfVectorizer默认会过滤单字词因为单个汉字对旅游文本没有区分度。可以显式设置token_patternr(?u)\b\w\b配合分词后的空格文本这样就不会因为默认的英文单词正则而丢掉中文。3.3 调参和评估注意点调参是让推荐结果“看起来好用”的关键。常见问题是新景点没有评论关键词导致特征向量全零相似度永远是0。解决办法是在拼接文本时给缺失关键词一个通用兜底row[comment_tags] if row[comment_tags] else 热门景点。评估上没有标注数据时不要硬上准确率。可以做一个简单的人工验证把相似度分数取阈值比如0.3以下的结果直接不显示这样可以过滤掉“看起来完全不搭”的推荐。在答辩时这个阈值可以当作“模型可调节参数”讲比单纯跑一个黑盒分数更有说服力。表推荐效果快速验证验证点检查方式常见异常是否重复推荐推荐结果中spot_id唯一相似矩阵对角元素没去除是否推荐了不同城市打印推荐列表的city字段文本特征没包含city冷启动景点给一个无评论关键词的景点调用结果全为0相似度4. 可视化大屏与交互界面把分析结果变成可展示的页面4.1 可视化工具选型matplotlib、pyecharts和ECharts分析了半天数据最终要给老师看。matplotlib适合做论文插图但不适合做网页交互。pyecharts封装了ECharts能在Python里直接生成HTML片段适合快速出图。如果你的项目要求“可视化大屏”我建议直接使用ECharts的JavaScript库因为大屏适配和轮播效果全在前端控制后端只需要提供JSON数据。选型逻辑图表数量少、只需要导出静态图时用matplotlib页面需要悬浮提示、点击联动时用ECharts。如果你用的是Flask模板前两者都能嵌入但ECharts的联动控制最方便。4.2 Flask ECharts 搭建最小的可视化页面用Flask做后端一个/api/stats接口提供统计数据一个/api/recommend/spot_id接口提供推荐结果页面用原生HTMLECharts渲染。这是最小可运行的方案不需要前端工程化。from flask import Flask, jsonify, request, render_template import pandas as pd from recommender import build_similarity_matrix, recommend app Flask(__name__) # 加载清洗后的数据及相似度矩阵 df pd.read_csv(clean_travel_data.csv, encodingutf-8-sig) matrix build_similarity_matrix(df) app.route(/) def index(): # 渲染可视化页面 return render_template(index.html) app.route(/api/stats) def stats(): # 城市景点数量统计 city_count df.groupby(city).size().reset_index(namecount) return jsonify({ cities: city_count[city].tolist(), counts: city_count[count].tolist() }) app.route(/api/recommend/int:spot_id) def get_recommend(spot_id): rec_df recommend(spot_id, df, matrix, top_k5) return jsonify(rec_df.to_dict(orientrecords)) if __name__ __main__: app.run(debugTrue)这段代码把后端拆成了三个接口首页渲染静态页面、统计接口返回柱状图数据、推荐接口按景点ID返回推荐列表。前端页面里用fetch(/api/stats)拿数据然后用echarts.init生成图表。注意fetch请求的是相对路径不要在HTML里写http://localhost:5000/api/stats否则部署到服务器上还得改代码。这是个很常见的低级错误。4.2.1 前端页面片段!DOCTYPE html html langzh-CN head meta charsetUTF-8 title旅游推荐数据分析可视化/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idcity_chart stylewidth:600px;height:400px;/div select idspot_select option value1故宫/option option value2颐和园/option /select div idrecommend_box/div script fetch(/api/stats) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(city_chart)); chart.setOption({ xAxis: { data: data.cities }, yAxis: {}, series: [{ type: bar, data: data.counts }] }); }); document.getElementById(spot_select).addEventListener(change, function() { const id this.value; fetch(/api/recommend/ id) .then(res res.json()) .then(data { const box document.getElementById(recommend_box); box.innerHTML data.map(item p${item.spot_name}${item.city}- ${item.similarity}/p ).join(); }); }); /script /body /html这个页面完成两个动作页面加载时拉取统计数据画柱状图下拉框改变时拉取对应景点的推荐结果。下拉框的选项最好由后端接口动态生成不要像这里写死。否则新增景点后页面要改代码。4.3 推荐结果联动与图表刷新有了第一版页面要把它变成“可视化大屏”还需要增加联动效果。一个常见的做法是点击柱状图上的“北京市”下方筛选出该城市的景点列表再点某一个景点查看推荐结果。ECharts的点击事件正好可以做这件事。chart.on(click, function(params) { const city params.name; fetch(/api/spots?city encodeURIComponent(city)) .then(res res.json()) .then(data { // 刷新景点下拉框 const select document.getElementById(spot_select); select.innerHTML data.map(item option value${item.spot_id}${item.spot_name}/option ).join(); }); });这里的思路是“从统计到明细再到推荐”的下钻式交互比单独一个推荐按钮更像一个数据产品。每次新增一个接口时注意对参数做encodeURIComponent处理避免中文城市名拼接乱码。5. 部署说明里的坑与验证技巧5.1 requirements.txt 与本地一键启动毕业设计交付的包里必须包含依赖清单。在项目目录下执行pip freeze requirements.txt会把当前环境所有包打进去这不推荐因为会把非项目依赖也带进去。更好的做法是手写一份关键依赖pandas2.2.3 flask3.0.3 scikit-learn1.5.2 jieba0.42.1 matplotlib3.9.2版本号以你实测能跑通的为准。启动说明有三步就够安装Python 3.10及以上pip install -r requirements.txt然后python app.py。我见过很多部署问题出在版本冲突比如scikit-learn和Python版本不兼容所以要求一律用虚拟环境python -m venv venv source venv/bin/activate # Windows下执行 venv\Scripts\activate pip install -r requirements.txt python app.pyWindows和macOS上激活虚拟环境的命令不同部署说明里最好把两个平台都列出来这是模板数据生成时最容易被忽略的一步。5.2 数据不显示时先查哪几个地方页面白屏或图表空白先从浏览器开发者工具的Network面板看/api/stats的HTTP状态码。如果是200检查返回的JSON字段是不是cities和counts别把下划线拼错如果是500回终端看Flask的Traceback绝大多数是KeyError或ValueError说明前后端字段名不一致。这个排查动作能解决七成以上的显示问题。第二类问题是端口被占用。app.run(debugTrue)默认绑定5000端口如果被其他服务占用改成app.run(host0.0.0.0, port8899, debugFalse)。注意debugTrue会开启调试器交给老师演示前建议关掉避免页面报错时出现可交互的调试点。第三类问题是中文乱码。后端返回的数据在浏览器里显示成乱码通常是因为旧版本Flask的JSON配置没有关闭ASCII转义。Flask 2.x之后默认启用UTF-8如果你还在用老代码可以加上app.config[JSON_AS_ASCII]False但在Flask 3.x里这个配置会被移除所以最稳妥的方案是升级到Flask 3并把所有模板文件保存为UTF-8编码。5.3 用点击日志验证推荐效果并准备答辩素材毕业设计答辩最怕老师随机点一个景点推荐列表不好看。与其临场紧张不如给系统加一个轻量的点击日志。每次前端发起/api/recommend/id请求时后端把景点ID、推荐结果、相似度最大值写入CSVimport time, json def log_recommendation(spot_id, result): log_path recommend_log.csv with open(log_path, a, encodingutf-8) as f: f.write(f{time.time()},{spot_id},{len(result)},{result[0][similarity] if result else 0}\n)这个日志有三个用处第一证明推荐模块真的被调用过第二通过相似度最大值分布判断阈值设得是否合理第三如果老师问“系统怎么优化”你可以说“收集用户点击行为后用贝叶斯平滑算法补全新景点的评分”。日志模块不给老师看也可以但它是你扩展“推荐系统上线后持续优化”这个答辩问题的实物支撑。最后一步验证打开recommend_log.csv确认每次点击下拉框都新增了一行记录。如果某一行相似度最大值低于0.3就回去检查这个景点的comment_tags是否为空、city是否参与了文本拼接。这样从CSV数据清洗到页面展示和推荐结果的完整链路就可以在答辩前逐项核对完毕。本文还有配套的精品资源点击获取