ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python旅游网站数据分析与可视化:从数据清洗到答辩高分指南

Python旅游网站数据分析与可视化:从数据清洗到答辩高分指南

简介:一份基于Python的旅游网站数据分析及可视化完整项目,以2020年五一旅游数据为案例,面向需要完成数据采集、清洗、分析与可视化大作业的高校学生。项目源码评审分超过95分,经过严格调试可运行,适合数据分析初学者参考。压缩包共74个文件、大小13.62MB,包含3个Python脚本、3个ipynb分析笔记、36个html可视化页面、1个csv旅游数据集,并附docx说明文档及js/css静态资源,覆盖数据处理、图表生成到页面展示的完整流程,已有616人学习下载。通过该项目可掌握从数据清洗到可视化展示的完整分析流程,项目目录按数据、脚本、模板、静态资源分层,便于逐模块学习与二次开发;内置词云图、水球图、漏斗图、地图等多种可视化图表,其中景点评价率水球图、热门景点词云图、门票销售漏斗图等案例均可迁移至其他城市或领域,快速搭建同类旅游数据分析系统,无论用于课程设计、期末答辩还是个人作品集,都能获得直接可用的源码与参考。

1. 期末大作业选题:旅游网站数据分析与可视化到底在考什么

“基于Python的旅游网站数据分析及可视化”这类期末大作业,听起来像是个标准模板——数据好找、工具成熟、可视化库一抓一大把,可实际交上去能拿95分的很少。原因在于,这门课考核的不是你会不会调plot,而是你有没有把一个数据分析项目完整走通:从数据获取、清洗、分析,到图表呈现,最后落成一份能说服人的业务结论。我见过太多人卡在数据阶段就花了三天,或者画出一堆图却完全答不上“为什么用折线不用柱状”。这门题适合刚学完 Python 基础、想找一份数据分析项目实践练手、并且期末想冲高分的人。接下来我把完整套路拆给你,照着做,至少能避开八成翻车点。另外,拿到的.zip先别急着解压跑代码,看一遍 README 和目录结构能省不少时间。

2. 数据准备阶段:从抓包到清洗,三条路径与一套 Pandas 模板

2.1 先看包里有什么:README、数据集与代码的目录结构

拿到一个.zip,第一件事不是双击解压然后试图直接运行,而是先看压缩包里的文件清单。期末大作业的压缩包通常会有这几类东西:README 说明、数据文件(CSV 或 JSON)、脚本或 Notebook、报告目录。README 里一般会写清楚数据来源、字段含义、Python 版本要求;如果作者没写,你就得自己做“考古”——用 Excel 打开数据文件先看列名,再找有没有requirements.txt。

这一步的意义在于确定后续所有分析都围绕同一套字段展开,避免代码写到一半发现列名对不上。我一般会在项目根目录建一个数据说明.txt,把字段名、类型、时间格式、哪些列有空值记下来,后面写清洗和分析代码时直接对照,能减少接近三分之一的无意义返工。如果你发现压缩包里只有代码没有数据,别慌,看下一节,数据来源不只一条路。

2.2 数据获取的三条路径:公开接口、开源数据集与“结构化模拟”

如果 ZIP 里没给现成数据,你需要自己解决数据来源。期末大作业场景下,三条路我都走过,经验如下:

第一是抓包找公开接口。主流旅游网站的景点页面、点评列表,前端几乎都是异步加载 JSON,打开浏览器开发者工具切到 Network 面板,刷新页面就能看到一批 XHR 请求,里面往往直接返回结构化的 JSON 数据。用requests拉下来再用pandas解析就行。但爬虫要考虑请求频率、登录态、反爬策略,时间成本高,适合数据量小、结构清晰的场景。

第二是去 GitHub 找公开旅游数据集。很多开源项目整理了景点评分、游客量、门票价格这类结构化数据,字段质量和真实业务几乎一致,优势是省时间,缺点是需要花一点时间筛选字段口径。

第三是老师只给了题目、没给数据的情况。常见做法是:按真实业务字段结构生成一份仿真样本数据,例如城市、景点、评分、评论数、门票价格、月份、游客量。答辩时明确说明这是按公开统计口径构造的演示数据,用于验证分析流程合理性。这不算造假,只要诚实标注,老师通常能接受。

# 路径一:请求公开 JSON 接口(注意遵守目标站点协议,仅限学习使用) import requests import pandas as pd # headers = {"User-Agent": "Mozilla/5.0"} # 基本浏览器标识 # resp = requests.get("https://api.example.com/scenic/list", headers=headers, timeout=10) # data = resp.json() # df = pd.json_normalize(data["data"]["list"]) # 更稳的路径二:直接读本地 CSV df = pd.read_csv("tourism_data.csv", encoding="utf-8") print(df.head()) print(df.dtypes) # 检查字段类型,是清洗的第一步

这段代码里,requests.get的timeout=10很关键,没有超时控制,遇到慢接口会一直挂住;pd.json_normalize能把嵌套 JSON 展平成表格,比手动循环json.loads省事得多。实际大作业里,我建议优先走 CSV 或 Excel 数据,因为评分和游客量字段可以直接用 Pandas 处理,不用处理嵌套结构。

2.3 数据清洗:去重、补缺失、时间格式化与异常值过滤

无论数据从哪来,清洗都是最耗时、也最能体现基本功的环节。四个动作是必须做的:按业务唯一键去重、用中位数或众数填充缺失值、把时间字段统一成datetime类型、过滤明显不合理的异常值。很多人跳过去重直接画图,结果趋势线出现双倍峰值,答辩时被问住,非常尴尬。

clean = df.copy() # 1. 去重:三个字段同时相同才算重复行 clean.drop_duplicates(subset=["city", "attraction", "month"], inplace=True) # 2. 缺失值:评分用中位数填充,价格缺省按 0 处理 clean["rating"] = clean["rating"].fillna(clean["rating"].median()) clean["price"] = clean["price"].fillna(0) # 3. 时间统一:字符串 "2024-01" 转成月度时间对象 clean["month"] = pd.to_datetime(clean["month"], format="%Y-%m") # 4. 异常值过滤:游客量为负或单月超千万的删掉 clean = clean[(clean["visitors"] > 0) & (clean["visitors"] < 1000)] # 清洗结果单独存一份,后续所有脚本都从这份读入 clean.to_csv("clean_tourism.csv", index=False, encoding="utf-8-sig") print(clean.info())

drop_duplicates里的subset参数指定了“哪几列一致才算重复”,不用它的话,只要有一列相同就会被删。fillna用中位数而不是均值,是因为评分字段往往有偏态分布,均值容易被极端值拉偏。to_datetime的format="%Y-%m"指定了解析格式,不写它也能猜,但遇到 “2024/01” 这类变体会解析失败。最后utf-8-sig是为了让 Excel 打开 CSV 时不乱码,具体原因在避坑章节细说。

3. 分析维度拆解:游客量、评分与价格的四个核心口径

3.1 月度游客量趋势:先聚合再画图,别拿明细直接上折线

拿到clean_tourism.csv之后,最容易犯的错是直接拿明细数据去画折线。每一行是一个景点的月度记录,几百条线叠在一起,画出来的图跟毛线团一样,答辩老师一眼就看出你没思路。正确做法是按月度汇总游客量,再做趋势分析。

trend = clean.groupby(clean["month"].dt.to_period("M"))["visitors"].sum().reset_index() trend["month"] = trend["month"].astype(str) # Period 对象转字符串方便画图 print(trend.head())

dt.to_period("M")把datetime截断到月份,groupby后sum()得到每月总游客量。注意reset_index()不能省,否则month会留在索引里,后面画图时x轴取不到。这里先不急着画,聚合结果还可以继续做环比计算:trend["visitors"].pct_change()能直接算月度环比增速,写报告时一句“暑期环比增长 42%”比放十张图都有说服力。

3.2 热门目的地排名:评论数、游客量与评分的交叉解读

排名是大作业里的必做项,但只排一个指标太单薄。常见做法是同时算三份排名:按游客量、按评论数、按平均评分,然后把它们放在一起交叉解读。你会发现有意思的业务结论:游客量最高的城市,评分不一定最高;评论数很多但评分偏低的景点,很可能是服务没跟上人流。

top_visitors = clean.groupby("city")["visitors"].sum().sort_values(ascending=False).head(10) top_reviews = clean.groupby("city")["reviews"].sum().sort_values(ascending=False).head(10) top_rating = clean.groupby("city")["rating"].mean().sort_values(ascending=False).head(10) print(top_visitors) print(top_reviews) print(top_rating)

这三个groupby的差异点在于聚合函数:游客量和评论数用sum,评分用mean。评分聚合后要留意样本量,某个城市只有一条评论且打了 5 分,它就会排到第一,这不代表真实水平。稳妥做法是加一个过滤条件:只保留评论数超过一定阈值的城市再算均分,代码里加一句clean = clean[clean["reviews"] >= 50]就行。

3.3 门票价格与满意度:箱线图与分组均值

价格和满意度之间的关系,是最容易出彩的分析点。常见做法是把价格切成几个区间,对比各区间内的平均评分。这里有一个 Pandas 2.x 用户容易踩的坑:pd.cut分箱后groupby默认会忽略空箱,需要加observed=True。

bins = [0, 50, 100, 200, 500] labels = ["0-50", "50-100", "100-200", "200以上"] clean["price_band"] = pd.cut(clean["price"], bins=bins, labels=labels) band_stats = clean.groupby("price_band", observed=True)["rating"].mean().reset_index() print(band_stats)

pd.cut的bins定义了价格边界,labels是每个区间显示的名字。observed=True在 Pandas 2.0 之后是必须的,否则会出现一排 NaN 的警告。分析结论通常是中低价位景区的满意度反而高于高价景区,这背后的业务解释可以是“高价景区承载量过大,体验下降”。写报告时把这个结论放在图旁边,比单纯放一张箱线图更显思考深度。

3.4 评论数与游客量的相关性:先做正态检验再选相关系数

很多大作业喜欢算相关性,但直接df.corr()就算完,忽略了一个前提条件:Pearson 相关系数要求变量近似正态分布。评论数和游客量这类业务数据几乎都是右偏分布,用 Pearson 会低估相关强度,正确做法是先做正态性检验,再决定用什么系数。

from scipy import stats # 先检验评分字段是否正态 shapiro = stats.shapiro(clean["rating"].dropna()) if shapiro.pvalue > 0.05: corr = clean[["rating", "reviews"]].corr(method="pearson") else: corr = clean[["rating", "reviews"]].corr(method="spearman") print(corr)

stats.shapiro返回(统计量, p值),p 值大于 0.05 认为数据是正态的。业务数据大多落在 else 分支,所以method="spearman"是更常见的组合。另外注意,corr()只对数值列生效,如果city列混了进来会报错,所以要先取["rating", "reviews"]两列。报告中写清楚“评分与评论数呈弱正相关(Spearman r=0.32)”,其中方法名、系数、样本量三要素齐备,评分自然不会低。

4. 可视化落地:Matplotlib 静态图与 Pyecharts 大屏的搭配方案

4.1 Matplotlib 出论文级静态图:字体、配色与分辨率

期末大作业的推荐策略是:静态图放在报告正文里,交互图放在附录或演示 HTML 里。Matplotlib 负责前者,三个参数是隐形扣分点:中文字体、负号显示、保存分辨率。没有配置中文字体,所有标签都会变成方块,这在后面避坑章节会细讲。

import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # 中文字体 plt.rcParams["axes.unicode_minus"] = False # 负号正常显示 fig, ax = plt.subplots(figsize=(10, 5)) ax.plot(trend["month"], trend["visitors"], marker="o", linewidth=2) ax.set_title("月度游客量趋势(2024)") ax.set_xlabel("月份") ax.set_ylabel("游客量(万人次)") ax.tick_params(axis="x", rotation=45) # 月份过多时旋转标签防重叠 fig.tight_layout() fig.savefig("trend.png", dpi=300, bbox_inches="tight")

dpi=300是期刊配图的标准,期末报告用 150 也够,但 300 会让老师觉得你认真。bbox_inches="tight"能自动裁掉空白边,避免保存出来的图四周一大片白。tick_params(rotation=45)解决标签重叠问题,别靠plt.xticks(rotation=45)那种老写法,前者更清晰。如果SimHei在你的机器上不存在,换成Microsoft YaHei或Noto Sans CJK SC都行,关键是先确认系统里有这个字体文件。

4.2 Pyecharts 拼接可视化大屏:Page 布局与交互效果

Pyecharts 是当前数据分析大作业里做“可视化大屏”的主流选择,因为它生成 HTML,可以动态交互,不用像 Flask 那样自己搭前端。期末场景用Page组件把多张图拼成一个纵向可滚动的页面即可,不需要炫技。

from pyecharts.charts import Bar, Line, Page from pyecharts import options as opts bar = (Bar() .add_xaxis(top_visitors.index.tolist()) .add_yaxis("游客量", top_visitors.values.tolist()) .set_global_opts(title_opts=opts.TitleOpts(title="热门目的地 TOP10"))) line = (Line() .add_xaxis(trend["month"].tolist()) .add_yaxis("游客量", trend["visitors"].tolist(), is_smooth=True) .set_global_opts(title_opts=opts.TitleOpts(title="月度趋势"))) page = Page(layout=Page.SimplePageLayout) page.add(bar, line) page.render("analysis_report.html")

Page.SimplePageLayout会让图表纵向排列,适合报告展示。add_xaxis需要 Python 列表,所以前面分析结果里的index和values都要.tolist(),这是最常见的类型报错来源。中间章节里的图表关键还在于,Pyecharts 版本不同 API 相差很大,0.5.x 和 1.x 的写法完全不同,装的时候直接指定pyecharts==1.3.1能省下一堆报错。

4.3 图表选型表:折线、柱状、地图、玫瑰图各用在哪

大作业最怕图表“为用而用”。下面这张选型表是我做项目时的标准参考,你可以直接抄进报告附录:

分析目标推荐图表Pyecharts 类型注意点
月度趋势折线图Linex 轴必须是排序后的时间序列
城市排名柱状图Bar先排序再取前 N
地理分布地图Map需要额外地图资源,离线易空白
价格区间满意度箱线图Boxplot先做异常值过滤
评分分布玫瑰图Pie+rosetype="radius"适合展示占比而非精确值
评论数与游客量关系散点图Scatter日志换算后再画,避免长尾挤压

这里重点说Map:Pyecharts 自带的 1.x 版本地图数据需要在联网环境下载,离线跑大概率白屏。期末答辩现场如果网络不稳定,地图组件会直接变成空白块。稳妥方案是放弃地图,改用柱状图按城市排布;或者提前把地图 JS 文件下载到本地,在init_opts={"js_host": "./js/"}中指定本地路径。

5. 旅游网站数据分析避坑指南:环境、字体与数据不一致的 5 个现场

5.1 现象:cmd 里输 python 没反应,双击脚本直接闪退

这是最基础也最影响心态的坑。原因九成是安装时没勾选 “Add Python to PATH”,系统根本找不到python.exe。解决方法是重装并勾选,或者手动把C:\Python3x\和C:\Python3x\Scripts\加进系统环境变量。装了 Anaconda 的用户,优先用 “Anaconda Prompt” 运行脚本,里面的环境变量是配好的。血泪经验:不要在 Windows 自带 cmd 里试python失败后就放弃,去查一下环境变量配置,这属于 python 安装教程里反复强调但总有人忽略的一步。

5.2 现象:图表标题和坐标轴中文全是方块

Matplotlib 默认字体不含中文字形。如果你只设了font.sans-serif = ["SimHei"]但系统没有这个字体,还是方块。先执行fc-list :lang=zh(Linux/macOS)或检查C:\Windows\Fonts确认字体是否存在,没有就去下载Noto Sans CJK SC安装。装完还没用,则是 Matplotlib 字体缓存的问题,删除~/.cache/matplotlib重启 Python 即可。这个坑在 Linux 服务器上跑代码时尤其频繁,本地 Windows 一般不缺 SimHei。

5.3 现象:Pyecharts 渲染出来是空白 HTML,控制台报 404

两种原因。第一种是 Pyecharts 版本错位:用了 0.5.x 的旧代码跑在 1.x 的环境里,图表对象直接不是预期结构,HTML 里什么都不会显示。解决方法是统一钉版本:pip install pyecharts==1.3.1。第二种是资源加载问题:1.x 默认从 CDN 拉取 echarts 依赖,网络受限或离线环境下图表区域空白。解决方法是把echarts.min.js下载到项目js/目录,然后生成图表时传入init_opts={"js_host": "./js/", "theme": "white"}。

5.4 现象:CSV 用 Excel 打开中文全乱,Notepad 打开正常

原因很明确:Pandas 默认用utf-8写入 CSV,而 Excel 对无 BOM 的 UTF-8 识别不稳定。解决方法是写文件时指定encoding="utf-8-sig",这是 UTF-8 带 BOM 的别名。前面第 2 章的清洗代码里我已经用了这个参数,但如果你使用to_excel,则不存在这个问题,Excel 原生支持 xlsx 格式的中文。注意,尽量不要在清洗阶段用latin1或gbk编码,后续重新读入时会遇到一堆编码报错。

5.5 现象:答辩 PPT 里的数字和数据脚本跑出来的对不上

这个问题非常隐蔽。原因通常是数据源不止一个,或者脚本每次运行都重新生成了模拟数据。比如你报告里写“全年游客量 3200 万”,但答辩前重新运行脚本,因为模拟数据的随机种子没固定,数字变成了 3100 万,当场翻车。解决方法是:清洗完成后立刻存一份clean_tourism.csv快照,后续分析和画图全部从快照读入;如果数据是随机生成的,在顶部加一行np.random.seed(42)。另外,统计口径也要写清楚,比如“游客量”是包含所有月份还是只含旺季,避免口径不一致。

6. 答辩加分技巧:把“能跑”变成“有说服力”的三个习惯

6.1 把项目拆成三个脚本,答辩时能一键复现

不要把所有代码写在一个大 Notebook 里。按阶段拆成data_clean.py、analysis.py、visual.py三个文件,每个文件末尾留一个if __name__ == "__main__":入口。答辩时老师如果问“这个图怎么来的”,你可以当场运行python visual.py,几秒钟重现全部图表。这个习惯在期末大作业和后续工作中都受用。

6.2 用 Page 生成一个总 HTML,所有图表放一页

与其让老师在多个 Notebook 输出之间来回跳,不如用 Pyecharts 的Page把所有图表拼成一个analysis_report.html。打开就是完整可视化报告,滚动即可查看全部图表。如果你想更进一步,也可以使用Tab组件把图表按“趋势 / 排名 / 价格”分组放在不同标签页,交互感更强。

6.3 每张图标题直接写业务结论,不写“游客量趋势图”

图表标题是老师的阅读路径。把“游客量趋势图”改成“暑期游客量为全年峰值、较春季增长 42%”,把“热门目的地排名”改成“TOP10 城市游客量占全网 73%,头部效应明显”。这一改动不需要额外代码,只是把你的分析结论前置,效果立竿见影。数据会自己说话是假的,需要你替它说。

我做这类项目养成的习惯是:每次跑完分析,先在记事本上写三行“最核心的发现”,再回头决定画哪些图。图表永远为结论服务,而不是为了凑页数。希望这份思路能帮你把那份.zip变成一份真正能打的大作业,也希望你答辩顺利。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进