☰
Python旅游推荐数据分析可视化毕业设计实战:从数据清洗到ECharts大屏
2026/10/1 17:35:12 网站建设 项目流程

简介:这份资源是面向计算机相关专业毕设学生与Python实战学习者的旅游推荐数据分析可视化项目,采用python+Django+mysql技术栈,并引入协同过滤算法实现个性化推荐,可直接作为毕业设计、课程设计或期末大作业使用。压缩包共422个文件,约9.36MB,以174个js、59个css、40个json、27个py、18个html等为主,涵盖前端页面样式、后端业务逻辑、数据配置与模板渲染,另附sql数据库脚本、docx部署说明及md文档,结构完整便于二次开发。项目功能覆盖用户登录注册、个人信息管理、景区浏览、评分收藏与智能推荐,管理员端支持景区信息及分类的增删改查、用户资料与行为记录管理。已有82人学习,适合需要完整赛题方案、可运行源码与部署参考的读者快速上手并完成答辩准备。

1. 旅游推荐数据分析可视化:从一份毕业设计源码能跑出什么

很多同学拿到「基于 Python 的旅游推荐数据分析可视化」这个题目时,第一反应是去搜免费 Python 源码大全,下载一个压缩包,解压,然后卡在第一步——跑不起来。我见过太多这样的场景:源码里requirements.txt列了三十多个包,Python 版本不对,数据库连不上,前端页面白屏,最后只能截图交差。这个方向本身是有价值的,它把 Python 数据分析、推荐算法、ECharts 可视化大屏串成了一条完整的链路,既能体现数据处理能力,又能展示工程落地。适合谁?适合计算机相关专业做毕业设计的学生,也适合想入门数据分析项目实战的开发者。核心要解决的问题就三个:数据从哪来、推荐怎么算、图表怎么画。把这三件事拆开,每一步都能复现,这个项目就不只是交差,而是能写进简历的东西。

2. 数据层:旅游数据从哪来、怎么存、怎么清洗

2.1 数据来源的三种现实选择

做旅游推荐,数据是地基。常见做法有三种:第一种是公开数据集,比如携程、去哪儿等平台的历史评论数据,或者一些数据竞赛平台上的旅游景点数据集,字段通常包含景点名称、城市、评分、评论数、门票价格、标签等;第二种是自己写爬虫抓取,用requests+BeautifulSoup或Scrapy,目标站点选结构清晰的旅游攻略页面,注意控制频率、遵守 robots 协议;第三种是模拟生成,用Faker库造一批看起来合理的数据,适合时间紧、只演示流程的情况。我一般会建议:如果导师不要求真实数据,用公开数据集最稳,省去反爬和清洗的麻烦;如果要求真实,就选一个页面结构简单的站点,抓个几千条就够用了。

数据存哪?小规模用 SQLite 就够了,零配置,一个文件搞定,Python 内置支持。规模大一点或者想练手 MySQL,就装一个本地 MySQL,用pymysql或SQLAlchemy连接。Redis 在这个项目里不是必须的,但如果你想做热门景点缓存或者推荐结果缓存,可以加一个 Redis 可视化客户端来观察键值,这算加分项。

2.2 建表与数据清洗的实操代码

假设我们抓到的原始数据是一份 CSV,字段有:景点名称、城市、评分、评论数、价格、标签、简介。先建表,再清洗。下面这段代码可以直接抄:

import pandas as pd import sqlite3 # 读取原始 CSV,注意编码,中文数据常见 utf-8 或 gbk df = pd.read_csv("travel_raw.csv", encoding="utf-8") # 查看缺失情况 print(df.isnull().sum()) # 清洗:评分转为数值,缺失值用中位数填充 df["评分"] = pd.to_numeric(df["评分"], errors="coerce") df["评分"].fillna(df["评分"].median(), inplace=True) # 评论数转为整数,缺失填 0 df["评论数"] = pd.to_numeric(df["评论数"], errors="coerce").fillna(0).astype(int) # 价格去掉“元”字,转为浮点数 df["价格"] = df["价格"].astype(str).str.replace("元", "", regex=False) df["价格"] = pd.to_numeric(df["价格"], errors="coerce") df["价格"].fillna(df["价格"].median(), inplace=True) # 标签列拆分,取第一个标签作为主分类 df["主标签"] = df["标签"].astype(str).str.split(",").str[0] # 去重:按景点名称去重,保留评论数最多的那条 df = df.sort_values("评论数", ascending=False).drop_duplicates(subset=["景点名称"]) # 写入 SQLite conn = sqlite3.connect("travel.db") df.to_sql("scenic", conn, if_exists="replace", index=False) conn.close() print("清洗完成,共", len(df), "条记录")

逻辑说明:pd.to_numeric的errors="coerce"参数会把无法转换的值变成 NaN,避免直接报错。fillna用中位数而不是均值,是因为旅游数据里价格和评分常有极端值,中位数更稳。去重时按评论数排序再保留,是为了留下信息量最大的记录。参数方面,encoding要根据实际文件调整,如果报UnicodeDecodeError,换成gbk或gb18030再试。

2.3 数据质量检查的三个关键指标

清洗完不是就完了,得验证。我一般看三个指标:一是缺失率,每个字段缺失超过 30% 就要考虑是否保留该字段;二是异常值,比如评分出现 100 分、价格为负数,这些要过滤掉;三是分布合理性,用df.describe()看价格和评分的分位数,如果 75% 分位数和最大值差距过大,说明有极端值拉偏了。下面这段检查代码可以放在清洗之后:

# 缺失率检查 missing_rate = df.isnull().sum() / len(df) print("缺失率超过 30% 的字段:") print(missing_rate[missing_rate > 0.3]) # 异常值过滤 df = df[(df["评分"] >= 0) & (df["评分"] <= 5)] df = df[df["价格"] >= 0] # 分布查看 print(df[["评分", "评论数", "价格"]].describe())

这一步做完,数据层就算稳了。很多毕业设计翻车就翻在数据没洗干净,后面推荐算出来的结果全是错的,图表也难看。

3. 推荐算法:协同过滤和基于内容的推荐怎么选、怎么调

3.1 两种推荐思路的适用场景

旅游推荐常见的算法就两类:协同过滤和基于内容的推荐。协同过滤需要用户-物品交互数据,比如用户对景点的评分或浏览记录。如果你手头只有景点信息,没有用户行为,那就只能用基于内容的推荐,靠景点标签、城市、价格区间做相似度匹配。我一般会建议:如果数据里有用户 ID 和评分,优先做协同过滤,效果更直观;如果没有,就用基于内容的推荐,用 TF-IDF 把标签向量化,再算余弦相似度。

协同过滤又分 UserCF 和 ItemCF。UserCF 是找相似用户,推荐他们喜欢的景点;ItemCF 是找相似景点,推荐给看过当前景点的人。旅游场景下 ItemCF 更稳定,因为景点之间的相似性比用户之间的相似性更容易解释。下面重点讲 ItemCF 的实现。

3.2 ItemCF 的完整实现与参数调优

假设我们有用户-景点评分矩阵,用surprise库或者手写都可以。手写更可控,适合毕业设计展示原理。核心步骤:构建共现矩阵、计算相似度、生成推荐列表。

import pandas as pd import numpy as np from sklearn.metrics.pairwise import cosine_similarity # 假设 ratings 是 DataFrame,列:user_id, scenic_id, rating # 构建用户-景点评分矩阵 matrix = ratings.pivot_table(index="user_id", columns="scenic_id", values="rating").fillna(0) # 计算景点之间的余弦相似度 sim = cosine_similarity(matrix.T) sim_df = pd.DataFrame(sim, index=matrix.columns, columns=matrix.columns) # 给某个用户推荐:找他已经评过分的景点,加权相似度求和 def recommend(user_id, top_n=10): user_ratings = matrix.loc[user_id] rated = user_ratings[user_ratings > 0].index.tolist() scores = {} for scenic in rated: similar = sim_df[scenic].drop(index=rated, errors="ignore") for s, score in similar.items(): scores[s] = scores.get(s, 0) + score * user_ratings[scenic] # 排序取前 top_n return sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n] print(recommend(1))

逻辑说明:pivot_table把长表转成矩阵,缺失值填 0 表示未评分。cosine_similarity计算列与列之间的相似度,即景点与景点的相似度。推荐时用用户已有评分加权相似度,评分越高的景点对推荐结果影响越大。参数方面,top_n控制推荐数量,一般 10 到 20 比较合适;相似度阈值可以加一个过滤,比如只保留相似度大于 0.2 的,避免噪声。

如果数据稀疏,可以先用TruncatedSVD降维再算相似度,效果会好一些。另外,冷启动问题在旅游场景很常见,新用户没有评分,这时候就退化成基于内容的推荐,用热门景点或高评分景点兜底。

3.3 基于内容的推荐:TF-IDF 加余弦相似度

没有用户行为数据时,基于内容的推荐是唯一选择。把每个景点的标签、城市、简介拼成一个文档,用 TF-IDF 向量化,再算相似度。

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import linear_kernel # 拼接特征文本 df["特征"] = df["城市"] + " " + df["主标签"] + " " + df["简介"].fillna("") # TF-IDF 向量化 tfidf = TfidfVectorizer(stop_words=None, max_features=5000) tfidf_matrix = tfidf.fit_transform(df["特征"]) # 计算余弦相似度 cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix) # 根据景点名称推荐相似景点 indices = pd.Series(df.index, index=df["景点名称"]).drop_duplicates() def content_recommend(name, top_n=10): idx = indices[name] sim_scores = list(enumerate(cosine_sim[idx])) sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)[1:top_n+1] scenic_indices = [i[0] for i in sim_scores] return df["景点名称"].iloc[scenic_indices] print(content_recommend("西湖"))

逻辑说明:TfidfVectorizer的max_features控制词汇表大小,5000 对于几千条数据够用了。linear_kernel比cosine_similarity快,因为 TF-IDF 向量已经归一化。indices用来把景点名称映射到行号,注意drop_duplicates防止同名景点报错。参数方面,stop_words中文需要自己提供停用词表,或者用jieba分词后再传入。

4. 可视化:用 ECharts 和 Flask 搭一个能看的数据大屏

4.1 后端 Flask 接口设计

可视化大屏的数据不能写死在前端,得从后端接口拿。用 Flask 写几个简单的 API,返回 JSON 给 ECharts 用。常见接口:热门城市 Top10、评分分布、价格区间分布、推荐结果列表。

from flask import Flask, jsonify, render_template import sqlite3 import pandas as pd app = Flask(__name__) def query_db(sql): conn = sqlite3.connect("travel.db") df = pd.read_sql(sql, conn) conn.close() return df @app.route("/") def index(): return render_template("dashboard.html") @app.route("/api/city_top10") def city_top10(): df = query_db("SELECT 城市, COUNT(*) as 数量 FROM scenic GROUP BY 城市 ORDER BY 数量 DESC LIMIT 10") return jsonify(df.to_dict(orient="records")) @app.route("/api/price_dist") def price_dist(): df = query_db("SELECT 价格 FROM scenic") bins = [0, 50, 100, 200, 500, 1000, 99999] labels = ["0-50", "50-100", "100-200", "200-500", "500-1000", "1000+"] df["区间"] = pd.cut(df["价格"], bins=bins, labels=labels) result = df.groupby("区间").size().reset_index(name="数量") return jsonify(result.to_dict(orient="records")) if __name__ == "__main__": app.run(debug=True, port=5000)

逻辑说明:query_db封装了连接和查询,避免重复代码。pd.cut做分箱,把连续价格转成区间。jsonify把 DataFrame 转成 JSON 数组,前端直接消费。参数方面,port默认 5000,如果被占用改成 5001;debug=True开发时用,部署时关掉。

4.2 ECharts 大屏的四个核心图表

前端用 ECharts,从 CDN 引入,不用 npm 打包,简单直接。四个图表:柱状图展示热门城市、饼图展示价格区间、折线图展示评分趋势、表格展示推荐结果。

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>旅游数据大屏</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> </head> <body> <div id="cityChart" style="width: 600px; height: 400px;"></div> <div id="priceChart" style="width: 600px; height: 400px;"></div> <script> // 热门城市柱状图 fetch("/api/city_top10").then(r => r.json()).then(data => { const chart = echarts.init(document.getElementById("cityChart")); chart.setOption({ title: { text: "热门城市 Top10" }, xAxis: { type: "category", data: data.map(d => d.城市) }, yAxis: { type: "value" }, series: [{ type: "bar", data: data.map(d => d.数量) }] }); }); // 价格区间饼图 fetch("/api/price_dist").then(r => r.json()).then(data => { const chart = echarts.init(document.getElementById("priceChart")); chart.setOption({ title: { text: "价格区间分布" }, series: [{ type: "pie", data: data.map(d => ({ name: d.区间, value: d.数量 })) }] }); }); </script> </body> </html>

逻辑说明:fetch请求后端接口,拿到 JSON 后直接喂给 ECharts 的setOption。map做数据转换,把对象数组转成 ECharts 需要的格式。参数方面,echarts.init的容器必须有明确宽高,否则图表不显示。CDN 地址用 jsdelivr,国内访问一般没问题,如果加载慢可以换成本地文件。

4.3 大屏布局与响应式适配

毕业设计的大屏通常要求 1920x1080 分辨率下好看,但答辩时可能用笔记本,所以得做响应式。简单做法:用 flex 布局,图表容器宽度用百分比,echarts.init后监听resize事件。

window.addEventListener("resize", () => { chart.resize(); });

如果要做成真正的数据大屏,可以加一个深色背景、标题栏、时间显示,用 CSS grid 把图表排成三列。注意 ECharts 的resize要每个实例都调用,或者用echarts.connect统一管理。

5. 避坑与排查:部署时最容易翻车的五个地方

5.1 现象:ModuleNotFoundError: No module named 'xxx'

原因:依赖没装全,或者 Python 环境不对。源码里的requirements.txt可能不完整,或者你用的 Python 版本和作者不一致。解决:先pip install -r requirements.txt,如果还报错,手动装缺失的包。建议用虚拟环境,python -m venv venv,激活后再装,避免污染全局。Python 版本尽量用 3.8 到 3.10,太新的版本有些包不兼容。

5.2 现象:Flask 启动后访问 500,日志显示 SQLite 找不到表

原因:数据库文件路径不对,或者表没建。sqlite3.connect("travel.db")是相对路径,如果从不同目录启动 Flask,就会找不到文件。解决:用绝对路径,os.path.join(os.path.dirname(__file__), "travel.db")。另外,确保清洗脚本先跑过,表已经存在。

5.3 现象:ECharts 图表空白,控制台报Cannot read property 'init' of undefined

原因:ECharts 的 CDN 没加载成功,或者echarts.init的容器 ID 写错了。解决:检查浏览器控制台 Network 面板,看 echarts.min.js 是否 200。如果 CDN 被墙,下载到本地放到static目录。容器 ID 要和getElementById一致,大小写敏感。

5.4 现象:推荐结果全是同一个景点

原因:相似度矩阵计算有问题,或者数据里某个景点出现次数太多,导致它和所有景点都相似。解决:检查cosine_similarity的输入矩阵,确保没有全零行。如果是基于内容的推荐,检查 TF-IDF 的max_features是否太小,导致特征区分度不够。可以加一个惩罚项,对热门景点降权。

5.5 现象:中文乱码,图表上显示方框

原因:Flask 返回的 JSON 默认ensure_ascii=True,中文被转义了。解决:app.config["JSON_AS_ASCII"] = False,或者用jsonify时加ensure_ascii=False。另外,HTML 的<meta charset="utf-8">要加上,数据库连接也可以指定charset="utf8"。

6. 进阶技巧:把推荐结果做成可解释的卡片

最后一章说一个让毕业设计加分的小技巧:推荐结果不要只给一个列表,做成带解释的卡片。比如「推荐西湖,因为你去过千岛湖,两者都是湖泊类景点,评分都在 4.5 以上」。实现方式是在推荐函数里同时返回相似度最高的那个已评分景点和相似原因。

def recommend_with_reason(user_id, top_n=5): user_ratings = matrix.loc[user_id] rated = user_ratings[user_ratings > 0].index.tolist() scores = {} reasons = {} for scenic in rated: similar = sim_df[scenic].drop(index=rated, errors="ignore") for s, score in similar.items(): scores[s] = scores.get(s, 0) + score * user_ratings[scenic] if s not in reasons or score > reasons[s][1]: reasons[s] = (scenic, score) result = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n] return [{"景点": s, "得分": round(v, 2), "因为你去过": reasons[s][0], "相似度": round(reasons[s][1], 2)} for s, v in result]

逻辑说明:reasons字典记录每个推荐景点最相似的已评分景点和相似度。返回时组装成字典列表,前端可以直接渲染成卡片。参数方面,top_n控制卡片数量,5 到 8 个比较合适,太多页面放不下。

验证方法:找几个测试用户,人工看推荐结果是否合理。如果推荐结果和已评分景点完全不相关,说明相似度计算有问题,回去检查 TF-IDF 或共现矩阵。我一般会抽 10 个用户,每个看前 5 个推荐,命中率超过 60% 就算及格。

最后说个血泪经验:毕业设计答辩时,老师最常问的是「你这个推荐和直接按评分排序有什么区别」。所以一定要准备一个对比实验,比如随机推荐、热门推荐、协同过滤推荐三组,算一下准确率或召回率,哪怕数据量小,有对比就有说服力。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询