简介:一份聚焦全国热门旅游景点数据分析与可视化的Python项目,围绕pandas数据处理、pyecharts可视化与jieba中文分词展开,适合正在学习数据分析、希望借助真实业务场景练手的读者。项目涵盖数据读取、缺失值处理、布尔筛选、分组聚合等常用操作,同时生成柱状图、饼图、地图等交互式图表,并完成分词与词云展示,直观呈现旅游景点数据背后的分布规律。包内共6个文件,以Jupyter Notebook代码为主,另有HTML可视化结果、Excel原始数据表和停用词文本,压缩包约502KB,整体轻量且结构清晰。目前已有592人学习下载,可作为课程设计、毕业设计或个人项目参考。通过学习可掌握数据清洗到可视化呈现的完整流程,获得可直接运行与二次开发的示例代码,以及处理真实数据时的常见操作思路,节省从零搭建和调试的时间。
1. 旅游景点数据项目拆解:pandas 清洗、pyecharts 地图与 jieba 词频分析
拿到这份“全国热门旅游景点数据分析与可视化”项目压缩包时,我第一反应不是看代码,而是看文件名。旅游景点.ipynb、旅游景点.xlsx、旅游景点.html三个文件已经勾勒出完整链路:Excel 数据源 → Notebook 处理 → 静态 HTML 报表。真正让我感兴趣的,是它用约 300 行代码把 pandas 的数据清洗、pyecharts 的多图表联动和 jieba 的中文分词塞进了同一个流程,这恰好是很多入门者卡壳的组合。对于想从“会调库”走向“能完成一个数据作业”的人来说,这个项目最大的价值不是景点排名,而是它展示了一套路子:先用 pandas 把脏数据变成规整的 DataFrame,再用 pyecharts 按不同维度输出图表,最后用 jieba 把非结构化文本变成可直接排序的词频统计。我下面按实际运行顺序拆解每一段逻辑,包括我补上的必要假设和踩坑点。
2. pandas 数据清洗:从 Excel 读入到可分析的结构化数据
2.1 读取 Excel 与初步探查
项目里数据源是旅游景点.xlsx,最常见的读法就是pandas.read_excel()。但实际开发中,这张表往往不像教程里那么干净:列名带空格、数值列混入“约 5A”之类的字符串、缺失值被填成“暂无”。我的习惯是读取后立刻做三件事:看形状、看列名、看前几行。
import pandas as pd df = pd.read_excel("旅游景点.xlsx", sheet_name=0) print("形状:", df.shape) print("列名:", df.columns.tolist()) print(df.head(3).to_string())参数说明:sheet_name=0表示读取第一个工作表,如果你的 Excel 里有多个景区分类 sheet,可以改成sheet_name="5A景区"。读取后先打印列名而不是直接head(),能更快发现列名里隐藏的换行符或全角空格。to_string()是为了避免 Jupyter 里因列宽省略而看不到真实内容。
这个项目原始文件里可能有“景点名称、地区、评分、热度、门票、关键词”这六类字段。清洗的第一步就是统一列名,我一般用列表推导式去掉所有列名首尾空白,并替换中文括号。
df.columns = [col.strip().replace("(", "(").replace(")", ")") for col in df.columns]2.2 缺失值与重复值处理
旅游景点数据最常见的缺失是评分和门票。有人用dropna()一把删,但那样会损失地区维度。我建议分列处理:评分缺失率低则删除该行,门票缺失则可以填充为该地区同类景点的中位数。项目代码里用的是fillna(),注意参数inplace在 pandas 2.0 后不建议再用,直接赋值给新变量更安全。
df["评分"] = pd.to_numeric(df["评分"], errors="coerce") df = df.dropna(subset=["评分", "景点名称"]) df = df.drop_duplicates(subset=["景点名称"]) df["门票"] = pd.to_numeric(df["门票"], errors="coerce") df["门票"] = df.groupby("地区")["门票"].transform(lambda x: x.fillna(x.median()))逻辑说明:pd.to_numeric(errors="coerce")会把“约5A”或“免费”这样的非数字转成NaN,这是处理混合类型列的常用手段。groupby("地区").transform()的巧妙之处在于它不会改变行数,而是把每个缺失值替换成所在地区所有非缺失门票的中位数,比全表填充更贴合业务。drop_duplicates(subset=["景点名称"])保留第一次出现的记录,如果你的表有“八达岭长城”和“八达岭长城(旺季)”这种变体,建议先做归一化再删重。
2.3 类型转换与新增特征列
pandas 里astype()只能处理能安全转换的列,比如把地区代码转成字符串。对于“热度”这种可能是1.2万格式的文本,astype(float)会直接报错。我一般先写一个解析函数,再用apply()批量转换。
def parse_hot(val): if isinstance(val, str): val = val.replace("万", "0000").replace(".", "") try: return float(val) except (ValueError, TypeError): return 0.0 df["热度数值"] = df["热度"].apply(parse_hot) df["价格区间"] = pd.cut(df["门票"], bins=[0, 50, 150, 1000], labels=["免费/低价", "中价", "高价"])参数说明:pd.cut的bins边界我用的是 0、50、150、1000,含义是“50 元以下为低价,150 元以下为中价,其余为高价”。这里的边界值不是项目里写死的,你可以根据门票分布调整。cut返回的是有序分类变量,后面做分组聚合时会自动按区间排序,不会出现“高价、中价、低价”这种乱序。新增“热度数值”列是为了后续 pyecharts 排序时不用再处理字符串。
清洗完的数据,老手会顺手做一次df.describe()检查数值范围,新手只看head()容易忽略异常值。比如“评分”列出现 9.9 分,而 5A 景区通常最高 5 分,这时候就要判断原始数据是百分制还是十分制。
3. pyecharts 可视化:柱状图、饼图与地图的配置细节
3.1 柱状图展示 Top10 热度景点
pyecharts 的核心逻辑是“创建图表对象 → add 系列 → 设置全局配置 → 渲染”。很多教程只贴代码不讲参数,导致换数据源时不知道怎么改。我用的是 pyecharts 2.x 版本,注意Bar()在 2.x 中是Bar(init_opts=opts.InitOpts(...)),而不是老版的Bar("标题")。
from pyecharts.charts import Bar from pyecharts import options as opts top10 = df.nlargest(10, "热度数值") bar = ( Bar(init_opts=opts.InitOpts(width="900px", height="500px", theme="light")) .add_xaxis(top10["景点名称"].tolist()) .add_yaxis("热度指数", top10["热度数值"].round(1).tolist(), category_gap="30%") .set_global_opts( title_opts=opts.TitleOpts(title="全国热门景点热度 Top10", subtitle="基于清洗后数据"), xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=30)), yaxis_opts=opts.AxisOpts(name="热度"), datazoom_opts=[opts.DataZoomOpts(range_start=0, range_end=100)], ) ) bar.render("bar_top10.html")逻辑说明:nlargest(10, "热度数值")直接按数值列返回前 10 行,不用先排序再切片。category_gap="30%"控制柱间宽度占比,数字越小柱子越宽。datazoom_opts这里其实是给长列表准备的,Top10 用不上,但保留range_start/range_end可以防止后续扩展成 Top30 时 x 轴文字重叠。axislabel_opts=opts.LabelOpts(rotate=30)是中文长名称景点的标配,旋转 30 度比interval强制显示更省空间。
如果你在 Jupyter 里想直接看效果,把bar.render()换成bar.render_notebook(),但要先确认 pyecharts 版本是 2.x,且在 Notebok 里执行过init_notebook_mode()。后面生成 HTML 文件时,注意 pyecharts 默认引用 CDN 的 echarts.min.js,离线环境需要改成本地路径,这点我在第 5 章排错部分详细说。
3.2 饼图展示各地区景点数量占比
饼图适合看分布,不适合看精确排名。项目里如果要做“各省份 5A 景点数量占比”,用Pie()很合适。这里有个坑:pyecharts 的Pie要求数据格式是[(key, value), (key, value)]列表,而不是两个平行列表。
from pyecharts.charts import Pie province_count = df["地区"].value_counts().reset_index() province_count.columns = ["地区", "数量"] data_pair = [list(x) for x in zip(province_count["地区"], province_count["数量"])] pie = ( Pie(init_opts=opts.InitOpts(width="900px", height="500px")) .add( series_name="景点数", data_pair=data_pair, radius=["30%", "70%"], label_opts=opts.LabelOpts(formatter="{b}: {c} ({d}%)"), ) .set_global_opts( title_opts=opts.TitleOpts(title="热门景点地区分布"), legend_opts=opts.LegendOpts(orient="vertical", pos_left="left"), ) ) pie.render("pie_province.html")参数说明:radius=["30%", "70%"]把饼图变成环形,内圈 30% 留白,外圈 70%。formatter="{b}: {c} ({d}%)"里的{b}是名称,{c}是数值,{d}是百分比,这是 pyecharts 的自定义模板语法。legend_opts设为垂直靠左,是为了防止省份名太长时挤压饼图区域。如果你的地区列有“北京 市”这样的脏数据,先执行df["地区"] = df["地区"].str.replace("市", "")再做计数。
3.3 地图可视化:Map 的坐标与名称匹配问题
pyecharts 地图是这类项目里最容易翻车的部分。因为Map()内部使用 ECharts 的地图注册表,中国地图的省份名必须是“北京”“河北”这种简称,一旦你的数据里写的是“北京市”“河北省”,地图会直接空白。
from pyecharts.charts import Map df["省份"] = df["地区"].str.replace("省|市|自治区|壮族|回族|维吾尔", "", regex=True) province_heat = df.groupby("省份")["热度数值"].sum().nlargest(15) map_chart = ( Map(init_opts=opts.InitOpts(width="1000px", height="600px")) .add( series_name="热度总和", data_pair=[list(x) for x in zip(province_heat.index, province_heat.values)], maptype="china", is_map_symbol_show=False, itemstyle_opts=opts.ItemStyleOpts( area_color="#f0f0f0", border_color="#999", ), ) .set_global_opts( title_opts=opts.TitleOpts(title="省份热门景点热度分布"), visualmap_opts=opts.VisualMapOpts( min_=int(province_heat.min()), max_=int(province_heat.max()), is_piecewise=True, pos_left="right", ), ) ) map_chart.render("map_hot.html")逻辑说明:.str.replace("省|市|自治区|壮族|回族|维吾尔", "", regex=True)中用正则一次性去掉常见行政区划后缀。注意“广西壮族自治区”如果先去掉“自治区”再取前两个字会变成“广西”,所以正则里要把“壮族”也删掉。is_map_symbol_show=False去掉地图上的散点标记,否则每个省份会叠一个红色圆点,视觉上很乱。VisualMapOpts的is_piecewise=True会把连续色带切成几段,方便观察梯度,如果不切,颜色过渡会掩盖小省份的差异。
地图渲染后,检查生成 HTML 里是否包含china地图数据。pyecharts 2.x 打开地图页时会自动加载china.js,如果你用的是国内镜像网络或离线环境,可能只看到中国轮廓但省份是灰色的。解决方法在第 5 章给出一套替换 map 文件的办法。
4. jieba 分词与词频统计:把“景点印象”变成可视化指标
4.1 自定义词典与停用词过滤
项目里的stopwords.txt是停用词文件,比如“我们”“这个”“一个”“了”这类无意义词。jieba 默认不会自动过滤停用词,所以我们要读取停用词表,在分词后做集合过滤。另一个问题是景点评论里经常出现“必须”“非常”这类副词,对热度分析没有价值。
import jieba import re stopwords = set() with open("stopwords.txt", "r", encoding="utf-8") as f: for line in f: stopwords.add(line.strip()) # 加载自定义景点词,防止“八达岭长城”被切成“八达岭”和“长城” jieba.add_word("八达岭长城") jieba.add_word("故宫博物院") def clean_comment(text): if not isinstance(text, str): return "" text = re.sub(r"[^\u4e00-\u9fa5]", "", text) words = jieba.lcut(text) # 只保留长度 >= 2 的非停用词 return [w for w in words if w not in stopwords and len(w) >= 2]参数说明:re.sub(r"[^\u4e00-\u9fa5]", "", text)会把所有非中文字符替换为空,包括数字、英文、标点。这个操作要放在分词之前,否则“5A级景区”会被拆成“5”“A”“级”“景区”。len(w) >= 2过滤掉“的”“了”“是”这类单字词,但像“西安”本身是二字词,不会被误删。jieba.add_word可以动态增加词典,如果你不想改dict.txt,这是最轻量的方案。
4.2 词频统计与条形图输出
分词之后,最直观的输出是词频图。用collections.Counter统计后取前 20 个词,再画一个横向条形图。横向条形图在 pyecharts 里是Bar()加上yaxis_opts反向,或者直接用Bar().reversal_axis()。
from collections import Counter import pandas as pd df["印象词"] = df["景点印象"].apply(clean_comment) all_words = [w for words in df["印象词"] for w in words] word_freq = Counter(all_words).most_common(20) freq_df = pd.DataFrame(word_freq, columns=["关键词", "频次"]).sort_values("频次") from pyecharts.charts import Bar from pyecharts import options as opts bar_words = ( Bar(init_opts=opts.InitOpts(width="900px", height="600px")) .add_xaxis(freq_df["关键词"].tolist()) .add_yaxis("出现频次", freq_df["频次"].tolist()) .reversal_axis() .set_global_opts( title_opts=opts.TitleOpts(title="景点印象高频词 Top20"), xaxis_opts=opts.AxisOpts(name="频次"), yaxis_opts=opts.AxisOpts(name="关键词", axislabel_opts=opts.LabelOpts(font_size=12)), ) ) bar_words.render("word_freq_bar.html")逻辑说明:reversal_axis()会把 x 轴和 y 轴对调,让关键词从下往上排列,最热门的词显示在顶部。sort_values("频次")先升序排列,这样条形图底部是最低频次,顶部是最高频次,视觉上符合阅读习惯。这种文本分析不需要词云那么花哨,但条形图能让读者一眼看出“风景好”“人多”“性价比”这类高频印象,对于景区运营决策更实用。
如果你的数据里没有“景点印象”列,只有景点名称,那简单办法是把所有景点名称拼接起来做 jieba 分词,也能统计出“北京”“上海”“自然”这类地缘特征。但这种做法更适合做地域分析,不适合做游客情绪分析。
5. HTML 报表整合与离线部署技巧
5.1 生成多图表聚合页
pyecharts 默认每个图表独立渲染成一个 HTML,但项目交付时通常需要在一个页面里看全景。pyecharts 2.x 可以用Page()对象把多个图表拼成一页,参数layout=Page.SimplePageLayout支持上下滑动布局。
from pyecharts.charts import Page from pyecharts.globals import CurrentConfig CurrentConfig.ONLINE_HOST = "./assets/" # 本地化 echarts 资源 page = Page(layout=Page.SimplePageLayout) page.add(bar, pie, map_chart, bar_words) page.render("旅游景点数据分析报告.html")参数说明:CurrentConfig.ONLINE_HOST = "./assets/"是离线部署的关键,它告诉 pyecharts 生成 HTML 时去本地assets目录找 echarts 的echarts.min.js和地图china.js。你需要事先从 pyecharts 的在线资源或 node_modules 里把这两个文件拷到项目根目录下的assets文件夹。如果不设置这一行,生成的 HTML 会默认引用https://assets.pyecharts.org/assets/,在无外网的内网环境里,地图和图表都会白屏。
Page.SimplePageLayout是纵向流式布局,每个图表占完整宽度。如果你想要响应式栅格,可以用Page.DraggablePageLayout,但那需要用户在浏览器里拖动调整位置,过度工程设计不推荐。页面生成后,用浏览器打开旅游景点数据分析报告.html,检查每个图表标题、数据标签和地图省份是否正常。
5.2 排查地图空白与版本不兼容
地图空白是这类项目最高频的问题。先打开“开发者工具”的 Network 面板,看china.js是否返回 200。如果 404,说明本地 assets 目录路径不对。如果返回 200 但地图还是空白,检查控制台有没有Map china not exists报错,这通常是因为你使用了"china"字符串,但当前 echarts 版本没有注册该地图。
解决方法有两种。第一种是在 HTML 中显式引入地图,在render()之前执行from pyecharts.datasets import register_url然后注册本地地址,这种方法可用但麻烦。我一般用更直接的方式:打开生成的 HTML,在<script>区域里手动加入echarts.registerMap('china', response.data),其中response.data是从china.js导出的 JSON。另一种是检查 pyecharts 版本,如果pip show pyecharts显示 1.x,很多配置项不兼容,建议升级到2.x,并同步升级echarts依赖。
另一个排查点是 pandas 版本。项目里如果用了read_excel(),需要确认安装了openpyxl或xlrd库,否则会报ImportError: Missing optional dependency 'openpyxl'。运行前最好执行一次pip install pandas openpyxl pyecharts jieba把环境补齐。
5.3 用 Jupyter 复现时的目录陷阱
压缩包里包含.ipynb_checkpoints目录,这是 Jupyter 自动生成的备份。我在复现时发现一个坑:如果用户把旅游景点.ipynb和旅游景点.xlsx放在不同目录,代码里的相对路径pd.read_excel("旅游景点.xlsx")就会失败。更隐蔽的是,Windows 下用os.path拼接路径时,如果文件名带中文,某些老版本 pandas 在特定编码环境下会报编码错误。
我的做法是在 Notebook 第一格强制把工作目录切到脚本所在目录:
import os os.chdir(os.path.dirname(os.path.abspath("__file__")))注意:Jupyter 里没有__file__变量,这个写法会直接报错。正确方式是使用os.getcwd()查看当前目录,或者用pathlib.Path.cwd()。如果文件确实在别的路径,最简单的是把数据和 ipynb 放在同一层。对于.lnk快捷方式文件,它没有实际数据,可以直接忽略。
如果你要把这个项目改造成可交付的脚本,我建议把每张图的render()集中放到一个main()函数中,并加上if __name__ == "__main__":入口。这样既能支持命令行运行,也能在 Jupyter 中逐段调试。输出 HTML 文件时,统一用一个output_dir变量管理,避免散落在根目录。
本文还有配套的精品资源,点击获取