☰
Python旅游网站数据可视化项目详解:pandas清洗到Flask部署
2026/10/12 19:42:52 网站建设 项目流程

简介:该项目是基于Python的旅游网站数据分析及可视化期末大作业源码,适合高校学生作为课程设计参考,也适合想学习数据可视化完整流程的开发者。压缩包共74个文件,约13.62MB,其中Python脚本与Jupyter Notebook负责数据读取和清洗,HTML文件对应各类可视化图表,CSS与JS辅助页面布局和交互,CSV提供原始旅游数据,docx说明文档帮助快速理清项目结构。项目以2020年五一旅游数据为分析对象,覆盖景点热度、门票销售、地区分布等维度,用地图、词云、漏斗图、水球图、条形图等形式展示结果,并整合进可浏览的Web页面。除常规统计外,还包含景区评价率、景点星级对比、热门类型词云等专项分析,分析视角丰富。整体代码经过严格调试,确保可直接运行,已有616人学习,对零基础用户非常友好,可作为期末大作业或入门数据分析实战的优质参考。

1. 基于Python的旅游网站数据分析及可视化:这份95分大作业到底拆出了什么

期末大作业拿高分和写好代码往往是两回事,这份基于Python的旅游网站数据分析及可视化资源是我见过把“数据分析+可视化+网页展示”串得最完整的一套。它用的不是虚拟数据集,而是去哪儿2020年五一旅游数据的真实CSV,从pandas清洗到pyecharts出图,再到Flask包装成可点击的网页站点,整条链路是闭环的。更关键的是,项目里不只给了单张图表HTML,而是把“全国热门景点漏斗图+各省景点评价率水球图+分区域星级数量地图+词云图”做成了完整仪表盘,这正是课程设计评审最吃的一套。

我拆这份资源时的第一判断是:它适合两类人。一类是期末要交数据可视化大作业、想要一套能直接复现且结构完整的参考;另一类是刚学完pandas和pyecharts基础、想看看真实项目里这些库怎么配合使用的初学者。当然,它不是零基础教程,需要你有Python环境和基本的pandas读写经验。接下来我会按实际拆包顺序,把数据读取、可视化参数、Flask整合和踩坑记录完整过一遍。

2. 数据读取与清洗:先用pandas把去哪儿CSV扒干净

2.1 CSV实际结构:字段比想象中多,别直接to_csv覆盖

压缩包里的核心数据文件是去哪儿2020年五一旅游数据.csv。我解压后先没有急着画图,而是用pandas做了字段摸底——这一步省了后面大量返工。文件里包含景点名称、所在省份、城市、景点等级(5A/4A等)、门票价格、评分、评价人数、适合人群(亲子/情侣等)、热度排名等列,其中“评分”和“评价人数”后续做水球图和漏斗图都要反复用。

import pandas as pd df = pd.read_csv('去哪儿2020年五一旅游数据.csv', encoding='utf-8') print(df.shape) print(df.columns.tolist()) print(df.head(3)) print(df.dtypes)

第一轮跑的时候我发现df.dtypes里面“门票价格”是object类型,原因是原始数据里混了“免费”“待定”这类字符串。评分列也存在空值,处理方式是先把非数值的票价统一映射成0,再转float。如果你直接df.to_csv('clean.csv')覆盖原始文件,后面要用原始字段时就得重新解压,所以我会单独导出一个清洗后的副本文件。

df['门票价格'] = pd.to_numeric(df['门票价格'], errors='coerce').fillna(0) df['评分'] = pd.to_numeric(df['评分'], errors='coerce') df['评价人数'] = pd.to_numeric(df['评价人数'], errors='coerce') df = df.dropna(subset=['景点名称', '省份']) df.to_csv('去哪儿2020年五一旅游数据_clean.csv', index=False, encoding='utf-8-sig') print(df.isnull().sum())

逻辑说明:pd.to_numeric的errors='coerce'是核心参数,它把无法解析的字符串转成NaN,再用fillna(0)补票价;但评分和评价人数我故意不fillna,因为后续画水球图时要按省份分组再算均值,提前补0会把省份均值拉低。导出时用utf-8-sig而不是utf-8,是为了让Excel直接打开不乱码。清洗思路就是:能转数值的转数值,不能转的先保留空值,等分组聚合时用mean()自动跳过NaN。

2.2 分组聚合与排序:水球图数据就是从这里来的

项目里有一堆“省份-景点评价率水球图.html”,这个“评价率”不是原始CSV里现成的字段,而是聚合计算的产物。常见的做法是对省份分组,算各景点评价人数的合计和均值,再映射成百分比。我习惯把这一步单独放在picture.py里,跟画图逻辑分开,方便以后换数据集。

import pandas as pd df = pd.read_csv('去哪儿2020年五一旅游数据_clean.csv', encoding='utf-8-sig') province_group = df.groupby('省份').agg( 景点数量=('景点名称', 'count'), 平均评分=('评分', 'mean'), 总评价人数=('评价人数', 'sum') ).reset_index() province_group['评价率'] = (province_group['总评价人数'] / province_group['总评价人数'].sum() * 100).round(2) province_group = province_group.sort_values('评价率', ascending=False) print(province_group.head(10))

逻辑说明:先用groupby('省份')聚合出三列统计量,再在聚合结果上计算评价率占比。这里有个容易翻车的点——如果你在groupby里同时写了mean和sum,pandas会对数值列自动做两次聚合,得到MultiIndex列,后面用province_group['评价率']会定位失败。我的处理习惯是分两步:先聚合、后算新列,代码可读性和健壮性都好。排序时用ascending=False降序,因为水球图默认按输入顺序从上往下渲染,降序后最大的省份在第一屏展示。

3. 核心可视化实现:pyecharts水球图、漏斗图、词云图的参数与选型

3.1 水球图:评价率展示利器,参数集中在series里

项目里“广东省-景点评价率水球图.html”“四川省-景点评价率水球图.html”这类文件,全部由同一个函数模板生成。我拆了function.py后确认,它们用的都是pyecharts的Liquid(水球图)。这类图在课程答辩里视觉效果非常突出,波浪动画能让评审一眼看到省份的评价占比。

from pyecharts.charts import Liquid from pyecharts import options as opts liquid = Liquid() liquid.add( "评价率", [0.826], center=["50%", "50%"], shape="circle", color=["#00BFFF", "#0066CC"], is_outline_show=True, outline_gap=5, background_color="white" ) liquid.set_global_opts(title_opts=opts.TitleOpts(title="广东省-景点评价率")) liquid.render('广东省-景点评价率水球图.html')

参数说明:add的第一个参数是系列名称;第二个是数值列表,取值0到1,项目里会把刚才算出的评价率除以100再传入。center控制水球在整个画布的位置,shape支持circle/roundRect/diamond,课程设计里用circle最稳,评审不会因为图形猎奇而质疑合理性。color列表表示波浪的两层渐变颜色,深蓝加浅蓝最清晰。is_outline_show=True保留外圈轮廓,否则水球边缘会显得模糊。

水球图的坑在于:add里的数值必须是float且不能超过1,超过1会出现波浪溢出甚至不渲染。我见过有人直接传82.6这个百分比数值,结果图形变成全屏蓝色一坨,这就是把评价率换算这步漏掉了。

3.2 漏斗图与词云图:十大热门景点和类型分布怎么画

“2020年五一期间全国最受欢迎的十大景点-漏斗图.html”用的是Funnel,数据来自CSV里热度排名Top10的景点及对应评价人数。漏斗图天然表达“从海量候选到最终Top10”的筛选感,在期末答辩里配合讲述“数据怎么一步步收敛到结论”特别好用。

from pyecharts.charts import Funnel from pyecharts import options as opts data = [("广州长隆", 23800), ("北京故宫", 21500), ("上海迪士尼", 19800)] funnel = Funnel() funnel.add( "热门景点", data, sort="descending", gap=4, label_opts=opts.LabelOpts(position="inside", formatter="{b}: {c}") ) funnel.set_global_opts(title_opts=opts.TitleOpts(title="2020年五一全国最受欢迎十大景点")) funnel.render('funnel_label_inside.html')

sort="descending"让漏斗从上到下按数值递减排列,这是默认也是最优的展示顺序。gap=4控制每层之间的间距,太小会黏在一起,太大则梯形感消失。formatter="{b}: {c}"表示标签显示名称+数值,答辩时不用在图上找数据。

词云图部分对应 “2020年五一全国热门景点类型-词云图.html” 和 wordcloud_diamond.html,用的是WordCloud系列。这里有个细节:pyecharts词云默认需要传入(词, 权重)二元组列表,但中文分词在pyecharts内部不做,需要从CSV的“景点类型”列提取关键词并统计频次。

from pyecharts.charts import WordCloud from collections import Counter type_list = df['景点类型'].dropna().str.split('、').sum() type_counter = Counter(type_list) wc = WordCloud() wc.add("景点类型", type_counter.most_common(30), word_size_range=[20, 100], shape="diamond") wc.set_global_opts(title_opts=opts.TitleOpts(title="2020年五一全国热门景点类型")) wc.render('wordcloud_diamond.html')

逻辑说明:str.split('、')是因为原CSV里一个景点可能有多个类型标签,用顿号分隔。sum()把嵌套列表摊平成单层列表,Counter统计频次,most_common(30)只保留前30个高频词,避免低频词铺满画面。word_size_range控制字号范围,20到100是比较稳妥的选择;shape="diamond"让词云整体轮廓呈菱形,纯装饰性参数。

3.3 地图与条形图:分区域星级数量背后的Geo/Map选型

项目里“华东地区景点不同星级数量.html”“华南地区景点不同星级数量.html”这类按区域拆分的图,以及“2020年各地五一全国热门景点-条形图.jpg”,说明作者做了“区域维度+星级维度”的交叉分析。实现方式是在各省数据基础上增加区域映射列(华东/华南/华北等),再按区域过滤。

region_map = { '广东': '华南', '广西': '华南', '海南': '华南', '江苏': '华东', '浙江': '华东', '上海': '华东', '安徽': '华东', '福建': '华东' } df['区域'] = df['省份'].map(region_map) for region in df['区域'].dropna().unique(): region_df = df[df['区域'] == region] star_counts = region_df['景点等级'].value_counts().sort_index() bar = Bar() bar.add_xaxis(star_counts.index.tolist()) bar.add_yaxis("景点数量", star_counts.values.tolist()) bar.set_global_opts(title_opts=opts.TitleOpts(title=f"{region}景点不同星级数量")) bar.render(f'{region}地区景点不同星级数量.html')

逻辑说明:先用字典映射生成“区域”列,再循环每个区域单独画条形图。这里用value_counts().sort_index()让星级按5A、4A、3A的自然顺序排列,而不是按数量排序。如果你用sort_values()排序,条形图会变成从多到少,答辩时要解释排序逻辑反而费劲。区域映射字典在最前面集中维护,以后换数据或者调整省份归属,只改一处即可。

4. Flask整合:把散落的HTML图表变成可点击的完整站点

4.1 app.py路由设计:index/zone/sightseeing/rate四页对应关系

资源里的app.py加上templates目录下的 index.html、zone.html、sightseeing.html、rate.html,构成了完整的Flask站点。routes设计很有代表性:首页是全局总览,zone是分区域统计,sightseeing是热门景点排行,rate是评价率专题。每个路由渲染一个模板,模板里通过iframe或链接把之前生成的HTML图表嵌进去。

from flask import Flask, render_template app = Flask(__name__) @app.route('/') def index(): return render_template('index.html') @app.route('/zone') def zone(): return render_template('zone.html') @app.route('/sightseeing') def sightseeing(): return render_template('sightseeing.html') @app.route('/rate') def rate(): return render_template('rate.html') if __name__ == '__main__': app.run(debug=True, port=5001)

debug=True开发阶段开着方便改模板热重载,但最后交作业或演示时我一般关掉,否则浏览器会弹出调试器界面,印象分直接扣。port=5001是自定义端口,因为5000端口在macOS上经常被AirPlay占用,启动直接报OSError: [Errno 48] Address already in use。

4.2 templates模板组织:iframe嵌入与静态资源路径

templates目录里除了四个页面模板,还有project.html,同时static目录下放了bootstrap.min.css、jquery.min.js、dashboard.css这套前端资源。模板的写法很直接:图表HTML文件放在项目根目录,页面里用iframe指向相对路径。

<!-- templates/rate.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>景点评价率看板</title> <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}"> </head> <body> <nav class="navbar navbar-inverse"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="/">旅游网站数据分析</a> </div> </div> </nav> <div class="container-fluid"> <div class="row"> <div class="col-sm-6"> <iframe src="/广东省-景点评价率水球图.html" style="width:100%;height:500px;border:none;"></iframe> </div> <div class="col-sm-6"> <iframe src="/四川省-景点评价率水球图.html" style="width:100%;height:500px;border:none;"></iframe> </div> </div> </div> </body> </html>

这里最容易翻车的是路径。Flask默认只把templates和static两个目录作为资源根目录,但项目里生成的图表HTML在根目录,所以iframe要用/广东省-景点评价率水球图.html这种绝对路径访问。如果你把图表HTML也放进templates,iframe的src就要写成templates/xxx.html,但Flask默认禁止直接访问templates下的文件,会报404。我的习惯是:图表文件一律放根目录,模板文件只做页面骨架。url_for('static', filename='css/bootstrap.min.css')是模板里引用静态资源的规范写法,别硬编码static/css/...,万一以后部署到子路径下面就会失联。

4.3 render.html与test.html:调试入口和最终展示入口

压缩包里还有个render.html,它不是图表文件,而是把多个图表用iframe拼成一个大看板的入口页。实际答辩时,直接打开render.html就能在浏览器里一次性展示所有图表,不需要启动Flask。test.html则是作者调试模板时的临时文件,作用不大但别急着删,里面可能保留了某个图表HTML的直接预览链接,排查“为什么Flask页面里图不显示”时能用来对照:直接双击打开这张图表HTML,如果正常显示,问题就在Flask路由;如果也不显示,问题就在pyecharts这边。

5. 避坑与排查:运行报错、中文乱码与看不见的图表

5.1 pyecharts版本不匹配:render输出空白页

现象:直接运行python picture.py,控制台不报错,生成的HTML文件用浏览器打开是一张空白页,或者只有title文字没有图。

原因:绝大多数情况是pyecharts版本混乱。项目里用到Liquid、Funnel、WordCloud这类图表,在pyecharts v1.x和v2.x的API差异很大。v0.5时代用add的写法完全不同,v1.9之后改为opts链式调用。如果环境里安装的是未来版本(比如2.0),部分图表组件的shape参数被改名或废弃,render时不会报错,但JavaScript渲染层会静默失败。

解决:先pip show pyecharts确认版本,建议直接锁使用与项目配套的版本范围:pip install pyecharts==1.9.1。我拆完这套资源后统一用1.9.1重跑了一遍,所有图表HTML都能正常render。升级到2.x也不是不行,但Liquid的outline_gap、Funnel的sort这些参数位置有调整,没必要在期末阶段冒这个险。

提示:把pyecharts==1.9.1写进requirements.txt,换机器时pip install -r requirements.txt一次到位。

5.2 中文乱码:图表的标题和图例变成方框

现象:图表能显示,但标题、省份名、景点名全部是方块或问号,英文正常。

原因:pyecharts底层渲染用的字体是浏览器默认字体,HTML文件没有声明中文字体栈,或者matplotlib生成的jpg图片里没有中文字体配置。项目里2020年各地五一全国热门景点-词云图.jpg这类图片是用picture.py配合matplotlib画的,matplotlib默认字体DejaVu Sans不支持中文。

解决:pyecharts部分比较简单,在任何模板HTML的head里加一段:

<style> body, text, div { font-family: "Microsoft YaHei", "PingFang SC", sans-serif; } </style>

matplotlib部分要单独设置字体:

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False

axes.unicode_minus=False必须配,否则负号会显示成方块。完整跑一遍生成图片,再把图片文件名填进Flask模板的img标签里引用。

5.3 Flask页面打开慢或图片不加载:缺失静态资源与host配置

现象:Flask服务启动了,页面骨架正常,但图表区域一直转圈,或图片裂开。

原因:static目录里如果bootstrap.css、jquery.min.js缺失,页面布局会错乱但不至于图表不显示;真正影响图表的是pyecharts渲染生成的HTML里引用了CDN的echarts.min.js,如果断网或CDN被墙,图就永远出不来。

解决:把pyecharts的render产出HTML后,检查其中是否有https://assets.pyecharts.org/assets/echarts.min.js这类外部链接。离线答辩环境建议改成本地引用:

from pyecharts.globals import CurrentConfig, NotebookType CurrentConfig.ONLINE_HOST = 'http://127.0.0.1:5001/static/echarts.min.js'

把echarts.min.js下载后放在static目录下,所有render出来的HTML都会指向本地资源。另外Flask的app.run(host='0.0.0.0', port=5001)可以允许同一局域网其他电脑访问演示,但只在自己电脑演示时用默认127.0.0.1就行,host设成0.0.0.0反而会被防火墙弹窗干扰。

5.4 ipynb与script结果不一致:Notebook里显示图但py文件render后没有

现象:Jupyter Notebook里Liquid().render()生成的图能显示,但同样的代码放到.py文件里跑,生成的HTML打开却是空的。

原因:Notebook环境会自动加载pyecharts的render依赖,而且Notebook的显示内核拦截了输出。写成.py脚本后,没有if __name__ == '__main__':的入口保护,某些聚合代码可能在import时就被执行,数据还没读到就render了空图。

解决:把生成每个图的代码包在函数里,脚本底部统一调用:

def draw_liquid(province, rate): liquid = Liquid() liquid.add("评价率", [rate], shape="circle") liquid.render(f'{province}-景点评价率水球图.html') if __name__ == '__main__': draw_liquid('广东省', 0.826)

这样做还有一个好处:for循环里动态生成多个省份的水球图时,函数参数传值不容易出现闭包变量被覆盖的问题。

6. 进阶:换数据复现与自定义看板的小技巧

这套资源的真正价值在于它是一个“可换皮”的架子。CSV换成2023年任意旅游平台的数据,清洗脚本和画图函数几乎不用改,只要保证列名一致即可。我一般会写一个column_map做列名映射,把新数据的字段名对齐到“景点名称/省份/评分/评价人数/景点类型/门票价格”这六个核心列,这样picture.py里所有画图函数不用动一行代码。

column_map = { '景点': '景点名称', '所属省份': '省份', '星级': '景点等级', '文旅评分': '评分', '评论数': '评价人数', '分类': '景点类型' } df = df.rename(columns=column_map)

换数据后有个必查项:新数据的省份字段是不是用全称。去哪儿这份CSV里“广东”不带“省”字,Flask模板和区域映射字典都是按“广东”匹配的。如果新数据里是“广东省”,region_map匹配不上,区域列会变成NaN,区域条形图就全空了。处理方式:统一去除“省/市/自治区”后缀。

df['省份'] = df['省份'].str.replace('省|市|自治区|壮族|回族|维吾尔', '', regex=True)

最后分享一个答辩演示习惯:我会在render.html里按“地图总览 → 漏斗图Top10 → 水球图分省评价率 → 词云图类型分布 → 区域星级条形图”的顺序排列iframe,这其实是标准的数据分析叙事结构。演示时先开全局、再聚焦、再对比、最后收在分布规律上,评审跟着你的节奏走,不会突然追问细节。从那以后,我每次做数据可视化作业都会强制走一遍“数据清洗→核心图表→页面整合→断网模拟测试”的流程,确保换一台完全没有Python环境的电脑,也能用浏览器打开所有HTML完成演示。希望帮到你。

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

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

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

立即咨询