简介:这份资源是面向计算机相关专业毕设学生与Python项目实战学习者的旅游推荐数据分析可视化系统,采用Python+Django+MySQL技术栈,并引入协同过滤算法实现个性化推荐,可直接作为毕业设计、课程设计或期末大作业使用。压缩包共422个文件,约9.36MB,以174个js、59个css、40个json、27个py、18个html等为主,涵盖前端页面样式、后端业务逻辑、数据配置与模板渲染,另附sql数据库脚本、docx部署说明及md文档,便于快速搭建运行环境。系统功能完整,用户端支持登录注册、个人信息管理、景区浏览、评分收藏与智能推荐;管理员端可对景区信息及类型进行分类管理,并查看和维护用户资料与行为记录。项目经过严格调试,确保可运行,已有82人学习下载,适合需要完整赛题方案、清晰目录结构与排错思路的读者参考实践。
1. 旅游推荐数据分析可视化:从一份毕业设计源码到能跑起来的完整链路
很多同学拿到「基于 Python 的旅游推荐数据分析可视化」这个题目时,第一反应是去搜免费 Python 源码大全,下载一个压缩包,解压,然后发现跑不起来——缺库、缺数据、路径写死、编码报错,最后只能对着别人的截图改改论文。这个标题真正要解决的问题不是「推荐算法有多深」,而是把数据采集、清洗、推荐计算、可视化展示串成一条能在本地复现的链路。它适合两类人:一是正在做计算机毕业设计、需要一套可演示可答辩系统的同学;二是想用 Python 数据分析与可视化练手、但不想从零造轮子的初学者。核心词就三个:Python、旅游推荐、数据分析可视化。下面按「数据怎么来、推荐怎么算、图怎么画、坑怎么躲」的顺序拆开讲,每一步都给出可抄的代码和参数说明。
2. 数据从哪来:旅游数据集的获取、清洗与字段设计
2.1 三种常见数据来源与选型理由
旅游推荐系统的数据通常分三块:景点/酒店/餐厅的基础信息、用户行为数据(评分、浏览、收藏)、以及评论文本。毕业设计场景下,最稳的做法是用公开数据集打底,再补少量爬虫数据。常见做法是:
- 公开数据集:如景区评分、酒店评论类 CSV,字段规整,适合直接进推荐流程。
- 爬虫补充:用 requests + BeautifulSoup 抓取景点名称、评分、地址、简介,注意控制频率,别把人家站点抓崩。
- 模拟用户行为:自己用脚本生成 user_id、item_id、rating 三列,方便演示协同过滤。
选型理由很直接:公开数据集保证字段完整,爬虫补充保证数据量够画图,模拟行为保证推荐算法有输入。三者拼起来,系统才像那么回事。
2.2 清洗脚本:去重、缺失值、类型转换
拿到数据后第一件事不是画图,是清洗。下面这段脚本处理景点表,逻辑是:读 CSV → 去重 → 填充缺失评分 → 统一城市字段 → 输出干净文件。
import pandas as pd import numpy as np # 读取原始景点数据,注意 encoding 参数,中文数据常见 gbk 或 utf-8-sig df = pd.read_csv("raw_spots.csv", encoding="utf-8-sig") # 1. 按景点名称去重,保留第一条 df = df.drop_duplicates(subset=["spot_name"], keep="first") # 2. 评分缺失用中位数填充,避免均值被极端值拉偏 df["score"] = df["score"].fillna(df["score"].median()) # 3. 城市字段统一去掉空格,并转成字符串 df["city"] = df["city"].astype(str).str.strip() # 4. 价格字段转数值,无法转换的置为 NaN 后删除 df["price"] = pd.to_numeric(df["price"], errors="coerce") df = df.dropna(subset=["price"]) # 5. 输出清洗后数据 df.to_csv("clean_spots.csv", index=False, encoding="utf-8-sig") print("清洗完成,剩余记录数:", len(df))参数说明:encoding="utf-8-sig"是为了兼容 Excel 打开时不乱码;errors="coerce"把无法转数值的脏数据变成 NaN,再统一删掉;keep="first"表示重复项只留第一条。失败时先看列名是否和脚本一致,再看编码,最后看有没有空文件。
2.3 用户行为表的设计与生成
推荐算法需要用户-物品交互矩阵。毕业设计里如果没有真实用户数据,可以用脚本模拟。下面生成 200 个用户对 50 个景点的评分,评分范围 1 到 5。
import pandas as pd import numpy as np np.random.seed(42) # 固定随机种子,保证每次生成结果一致 user_ids = [f"U{i:03d}" for i in range(1, 201)] spot_ids = [f"S{i:03d}" for i in range(1, 51)] records = [] for u in user_ids: # 每个用户随机选 10 到 30 个景点评分 n = np.random.randint(10, 31) chosen = np.random.choice(spot_ids, size=n, replace=False) for s in chosen: records.append([u, s, np.random.randint(1, 6)]) behavior = pd.DataFrame(records, columns=["user_id", "spot_id", "rating"]) behavior.to_csv("user_behavior.csv", index=False, encoding="utf-8-sig") print(behavior.head())这段代码的关键参数是np.random.seed(42),固定种子后每次跑出来的数据一样,方便调试和写论文截图。replace=False保证一个用户不会对同一个景点重复评分。生成后可以用behavior.describe()看评分分布,如果全是 3 分,说明随机范围有问题。
3. 推荐算法怎么选:协同过滤与基于内容的落地对比
3.1 协同过滤:用户相似度计算与推荐生成
协同过滤的核心是「相似的人喜欢相似的东西」。用 pandas 构建用户-景点评分矩阵,再算用户余弦相似度,最后给目标用户推荐他没看过但相似用户高分的景点。
import pandas as pd from sklearn.metrics.pairwise import cosine_similarity behavior = pd.read_csv("user_behavior.csv") # 构建用户-景点评分矩阵,缺失值填 0 matrix = behavior.pivot_table(index="user_id", columns="spot_id", values="rating").fillna(0) # 计算用户之间的余弦相似度 user_sim = cosine_similarity(matrix) user_sim_df = pd.DataFrame(user_sim, index=matrix.index, columns=matrix.index) def recommend(user_id, top_n=5): # 取该用户与其他用户的相似度,排除自己 sim_scores = user_sim_df[user_id].drop(user_id) # 取最相似的 10 个用户 similar_users = sim_scores.sort_values(ascending=False).head(10).index # 找这些相似用户评分高、但目标用户没评分的景点 candidate = behavior[behavior["user_id"].isin(similar_users)] candidate = candidate[~candidate["spot_id"].isin( behavior[behavior["user_id"] == user_id]["spot_id"])] # 按平均评分排序 rec = candidate.groupby("spot_id")["rating"].mean().sort_values(ascending=False).head(top_n) return rec print(recommend("U001"))参数说明:fillna(0)表示未评分视为 0 分,这是协同过滤的常见处理;head(10)控制相似用户数量,太大引入噪声,太小推荐不准;top_n=5是最终推荐个数。如果推荐结果为空,说明该用户已经评过所有候选景点,或者相似用户太少,可以调大相似用户数量。
3.2 基于内容的推荐:用景点标签做相似度
协同过滤有冷启动问题——新用户没评分就推不了。这时候用基于内容的推荐兜底:把景点名称、城市、标签拼成文本,用 TF-IDF 向量化,再算景点之间的相似度。
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import linear_kernel spots = pd.read_csv("clean_spots.csv") # 把城市和标签拼成特征文本 spots["content"] = spots["city"].astype(str) + " " + spots["tags"].astype(str) tfidf = TfidfVectorizer(stop_words=None) tfidf_matrix = tfidf.fit_transform(spots["content"]) # 计算景点之间的余弦相似度 cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix) def content_recommend(spot_name, top_n=5): idx = spots[spots["spot_name"] == spot_name].index[0] sim_scores = list(enumerate(cosine_sim[idx])) sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True) sim_scores = sim_scores[1:top_n+1] spot_indices = [i[0] for i in sim_scores] return spots["spot_name"].iloc[spot_indices] print(content_recommend("西湖"))这里TfidfVectorizer把文本转成向量,linear_kernel算相似度比 cosine_similarity 更快。注意stop_words=None是因为中文停用词需要自己准备,毕业设计里不处理也能跑。如果报错说某个景点找不到,先检查名称是否有多余空格。
3.3 两种算法的融合与切换策略
实际系统里不会只用一种。常见做法是:用户有评分记录时走协同过滤,没有时走基于内容。下面这个判断逻辑可以直接用。
def hybrid_recommend(user_id, spot_name=None, top_n=5): user_records = behavior[behavior["user_id"] == user_id] if len(user_records) >= 5: return recommend(user_id, top_n) elif spot_name: return content_recommend(spot_name, top_n) else: # 兜底:返回评分最高的热门景点 return behavior.groupby("spot_id")["rating"].mean().sort_values(ascending=False).head(top_n) print(hybrid_recommend("U001", "西湖"))阈值>= 5是经验值,评分少于 5 条时协同过滤不稳定。这个融合策略在答辩时很好讲:既解决了冷启动,又保留了协同过滤的个性化。
4. 可视化怎么做:从 ECharts 大屏到 Python 静态图
4.1 用 Pyecharts 生成可交互的 HTML 图表
毕业设计演示时,静态图不如可交互图表有冲击力。Pyecharts 能直接输出 HTML,浏览器打开就能点。下面画一个景点评分分布柱状图。
from pyecharts.charts import Bar from pyecharts import options as opts import pandas as pd spots = pd.read_csv("clean_spots.csv") # 按城市统计平均评分 city_score = spots.groupby("city")["score"].mean().sort_values(ascending=False).head(10) bar = ( Bar() .add_xaxis(city_score.index.tolist()) .add_yaxis("平均评分", city_score.values.round(2).tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="城市景点平均评分 Top10"), xaxis_opts=opts.AxisOpts(name="城市"), yaxis_opts=opts.AxisOpts(name="评分"), ) ) bar.render("city_score.html") print("图表已生成:city_score.html")参数说明:head(10)只取前 10 个城市,避免横轴太挤;round(2)保留两位小数;render输出 HTML 文件。如果中文显示乱码,在set_global_opts里加title_opts的字体设置,或者检查 HTML 的 meta 编码。
4.2 用 Matplotlib 画推荐结果对比图
论文里通常需要静态图。下面画协同过滤和基于内容推荐的评分对比。
import matplotlib.pyplot as plt import pandas as pd # 假设已有两种算法的推荐结果 cf_rec = recommend("U001", top_n=5) content_rec = content_recommend("西湖", top_n=5) fig, axes = plt.subplots(1, 2, figsize=(12, 5)) axes[0].barh(cf_rec.index, cf_rec.values, color="#4C72B0") axes[0].set_title("协同过滤推荐评分") axes[0].set_xlabel("预测评分") axes[1].barh(content_rec.index, [1]*len(content_rec), color="#DD8452") axes[1].set_title("基于内容推荐景点") axes[1].set_xlabel("相似度") plt.tight_layout() plt.savefig("recommend_compare.png", dpi=150) print("对比图已保存")figsize=(12,5)控制画布大小,dpi=150保证论文打印清晰。barh是横向柱状图,适合景点名称较长的情况。如果保存的图空白,检查plt.show()是否在savefig之前调用了。
4.3 可视化大屏的布局思路与数据接口
如果要做可视化大屏,常见结构是:顶部标题栏、左侧城市筛选、中间地图或柱状图、右侧推荐列表。数据接口可以用 Flask 暴露 JSON,前端用 ECharts 异步加载。
from flask import Flask, jsonify import pandas as pd app = Flask(__name__) @app.route("/api/city_score") def city_score(): spots = pd.read_csv("clean_spots.csv") data = spots.groupby("city")["score"].mean().round(2).to_dict() return jsonify(data) if __name__ == "__main__": app.run(debug=True, port=5000)port=5000是 Flask 默认端口,debug=True方便改代码自动重启。前端用fetch("/api/city_score")拿数据后传给 ECharts 的setOption。注意跨域问题,本地开发可以在 Flask 加 CORS 头,或者前端和后台同端口部署。
5. 避坑与排查:毕业设计里最容易翻车的 5 个地方
5.1 编码报错:UnicodeDecodeError
现象:读 CSV 时抛UnicodeDecodeError: 'utf-8' codec can't decode byte。原因:原始文件是 GBK 编码,脚本用 UTF-8 读。解决:先试encoding="gbk",再试encoding="utf-8-sig",或者用chardet检测编码。
5.2 路径写死:换台电脑就跑不了
现象:代码里写C:/Users/xxx/Desktop/data.csv,换电脑后 FileNotFoundError。原因:绝对路径依赖本机目录。解决:统一用相对路径,把数据文件和脚本放同一目录,或者用os.path.dirname(__file__)拼路径。
5.3 库版本冲突:sklearn 报错 API 变了
现象:ImportError: cannot import name 'cosine_similarity'或参数名变了。原因:不同版本 sklearn 接口有差异。解决:在requirements.txt里锁定版本,比如scikit-learn==1.3.0,部署时用pip install -r requirements.txt。
5.4 推荐结果为空:冷启动没处理
现象:新用户调用recommend返回空列表。原因:该用户没有评分记录,协同过滤算不出相似用户。解决:加融合逻辑,没记录时走基于内容或热门兜底,参考 3.3 节的hybrid_recommend。
5.5 可视化中文乱码:Matplotlib 显示方块
现象:图表里中文变成方框。原因:Matplotlib 默认字体不支持中文。解决:在画图前加两行配置。
import matplotlib matplotlib.rcParams["font.sans-serif"] = ["SimHei"] matplotlib.rcParams["axes.unicode_minus"] = FalseSimHei是 Windows 自带黑体,Mac 可以换Arial Unicode MS。axes.unicode_minus=False解决负号显示异常。
6. 让系统更像一个产品:部署、答辩演示与可扩展方向
把代码跑通只是第一步,答辩时老师会问「你怎么部署的」「能不能现场演示」。我一般会准备一个requirements.txt和一个启动脚本,确保换电脑也能三行命令跑起来。
# 1. 创建虚拟环境 python -m venv venv # 2. 激活环境(Windows) venv\Scripts\activate # 3. 安装依赖 pip install -r requirements.txt # 4. 启动 Flask 服务 python app.pyrequirements.txt里至少写:pandas、numpy、scikit-learn、pyecharts、flask、matplotlib。版本号建议锁定,避免答辩当天装不上。
演示顺序也有讲究:先打开可视化大屏展示数据概览,再输入一个用户 ID 展示推荐结果,最后打开代码讲清洗和算法逻辑。这样老师能看到「数据→算法→展示」的完整闭环。
可扩展方向有三个:一是把推荐结果存进 Redis,加快重复查询;二是用大模型智能体做旅游推荐对话,把推荐结果转成自然语言;三是把可视化大屏换成 Vue + ECharts 前后端分离,更像企业项目。但毕业设计时间有限,先把当前链路跑稳,再挑一个方向做深。
我自己踩过最深的坑是数据清洗没做干净,导致推荐结果全是同一个景点,答辩时被问「为什么推荐这么集中」,当场翻车。后来养成习惯:每次跑完推荐先看结果分布,如果 Top5 全是同一个城市,就回去查数据。希望帮到你。
本文还有配套的精品资源,点击获取