news 2026/9/12 12:15:05

pandas+jieba+pyecharts:300行代码实现全国旅游景点数据分析与可视化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
pandas+jieba+pyecharts:300行代码实现全国旅游景点数据分析与可视化

简介:全国热门旅游景点数据分析与可视化,是一份适合Python数据分析初学者的实战项目,内置约300行可运行代码。项目贯穿数据导入、清洗、聚合到可视化的完整链路:利用pandas完成Excel数据读取、缺失值处理、类型转换、布尔筛选和groupby分组统计;借助pyecharts绘制柱状图、地图等交互图表,并进行坐标轴、标题与图例配置;通过jieba分词对景点评论文本提取关键词,为词云展示和趋势分析打下基础。资源包共6个文件、约502KB,包含两个Jupyter Notebook源码、一份Excel原始数据、一个已渲染的HTML可视化报告及停用词文本,结构紧凑、对照方便。目前已有592人学习下载。运行案例既能掌握数据预处理、特征分析与结果展示的完整思路,也可替换数据源迁移到其他景区分析场景,适合高校学生、转行人员及旅游行业从业者快速上手。

1. 把全国热门旅游景点数据分析打进 300 行:pandas 处理、jieba 分词、pyecharts 出图的配合方式

旅游数据的体量不大,多数 CSV 只有几千行,难的不是跑不动,而是没法直接画。真实会遇到的景点表里,评分列混着“4.8分”、评论数写着“1.2万”、省份一会儿是“广西”一会儿是“广西壮族自治区”。这个标题里的约 300 行代码,做的正是把这类原始数据拉直成“省份 × 指标”宽表,再把用户评论文本切成词频,最后落到地图、柱状图和词云的一整条流水线。pandas 负责清洗与聚合,jieba 负责从评论里抽词,pyecharts 负责把结果渲染成可交互 HTML。适合的场景很明确:刚拿到一份多字段旅游景点表,要做一份能对外展示的全国概览,又不想引入太重的大数据组件。下面按读取、清洗、聚合、分词、出图、排版的次序往下拆。

2. pandas 数据处理:从景点原始表到省份聚合指标的完整管线

2.1 read_csv 的编码、类型与重复项初检

文件读入的第一道门槛是编码。很多从 Excel 导出的表格带 BOM,用encoding='utf-8'会让列名多出一个\ufeffencoding='utf-8-sig'能自动把 BOM 剥掉;遇到从网页直接下载的 GBK 文件,再改用encoding='gbk'重试。不要凭感觉猜,先读进来看结构:

import pandas as pd df = pd.read_csv('tourism_spot.csv', encoding='utf-8-sig') print(df.shape) print(df.dtypes) print(df.head(3))

df.shape给出行列数,df.dtypes是下一步定位问题的重点。评分、评论数、门票价这三列,如果打印出来是object,说明列里混进了文本,必须先转类型再聚合。初检同时做一次按景点名的重复检查:

print('重复景点行数:', df.duplicated(subset=['spot_name']).sum()) df = df.drop_duplicates(subset=['spot_name'], keep='first')

duplicated(subset=['spot_name']).sum()用布尔和的变形快速给出重复行数;drop_duplicateskeep='first'保留最早出现的一条,适合多来源拼接的景点主表。如果同一景点在不同城市都有记录,可以先看city列再决定要不要把城市加进subset,不要把整行去重当作默认操作。

提示:文件名最好不要带中文路径,某些 Windows 环境下 pandas 读中文路径会报 FileNotFoundError,但文件明明就在那里。这个坑排查起来很费时间。

2.2 数值列的“脏值”清理:类型转换与缺失值补全

评分的“4.8分”和评论数的“1.2万”是两种典型脏值,直接astype(float)会抛异常。推荐两条转换路径:统一格式后用pd.to_numeric(errors='coerce'),无法解析的值置为NaN,不会中断后续流程:

def parse_wan(s): if isinstance(s, str): s = s.strip().lower() if s.endswith('万') or s.endswith('w'): return float(s[:-1]) * 10000 return float(s) return s df['comment_count'] = df['comment_count'].map(parse_wan).astype(int) df['score'] = ( df['score'].astype(str) .str.replace('分', '', regex=False) .pipe(pd.to_numeric, errors='coerce') ) df['ticket_price'] = ( df['ticket_price'].astype(str) .str.replace('免费', '0', regex=False) .pipe(pd.to_numeric, errors='coerce') .fillna(0) )

parse_wan先统一小写,再判断以“万”还是“w”结尾,避免同一列里“1.2万”和“1.2w”共存时统计偏差。comment_count在转换后直接astype(int),前提是数据不存在小数;如果来源流量值可能出现小数,保留 float 更安全。scorestr.replace('分', '', regex=False)去掉后缀,regex=False避免把“分”字当正则处理。ticket_price把“免费”当 0,其他解析不了的落到NaNfillna(0)

缺失值要区分对待:景点名和省份缺失意味着这一行没有分析价值,直接删除;评分缺失则用省份均值回填,保留它在地区统计里的权重:

df = df.dropna(subset=['spot_name', 'province']) df['score'] = df['score'].fillna( df.groupby('province')['score'].transform('mean') )

groupby('province')['score'].transform('mean')返回与df等长的序列,fillna用同省份的平均分填空缺,比全局均值更贴合地区差异。这里不直接对score赋值,是因为transform会保持索引对齐,逐行填空时不容易错位。

2.3 groupby 聚合与省份名规范化的先后顺序

省份名先规范化再做聚合,否则“广西”和“广西壮族自治区”会变成两行,地图上该上色的区域永远缺一块。别名表按项目里常用写法维护:

province_alias = { '广西壮族自治区': '广西', '广西省': '广西', '内蒙古自治区': '内蒙古', '内蒙': '内蒙古', '新疆维吾尔自治区': '新疆', '新疆省': '新疆', '西藏自治区': '西藏', '西藏省': '西藏', '宁夏回族自治区': '宁夏', '宁夏省': '宁夏', } df['province'] = df['province'].astype(str).str.strip().replace(province_alias)

astype(str).str.strip()去除因手工录入残留的首尾空格;replace传入 dict 时只对完全匹配的键做替换,不会误伤“黑龙江”这类本身完整的名称。归一之后在groupby里同时算多个指标:

agg_df = ( df.groupby('province', as_index=False) .agg( 景点数=('spot_name', 'count'), 平均评分=('score', 'mean'), 评论总数=('comment_count', 'sum'), 票价中位数=('ticket_price', 'median'), ) ) agg_df['平均评分'] = agg_df['平均评分'].round(2) agg_df = agg_df.sort_values('评论总数', ascending=False)

as_index=False让省份名留在普通列,之后转data_pair会更顺手。agg里的关键字参数写法是 pandas 较新的风格,旧式agg({'score': 'mean'})在部分版本会告警,建议直接改成agg(新列名=(旧列名, 函数))的结构。排序放在聚合之后做,既不污染汇总结果,又能直接拿前 10 行供柱状图使用。

字段原表列聚合函数在图表里的角色
景点数spot_namecount地图分层着色
平均评分scoremean柱状图对比维度
评论总数comment_countsum热度排序依据
票价中位数ticket_pricemedian可比价分析

这张宽表是后面所有图表的公共输入。到这里,pandas 阶段干的活就完成了从“清洗杂乱的原始记录”到“可消费的分析数据集”的过渡。

3. jieba 分词:把旅游评论切成可计数的偏好词

3.1 为什么是词频而不是情感分

景点评论的常规分析里,动不动先上一个情感分析模型,但面对“分布在哪儿、人们在讨论什么”这类问题,词频是更朴素的答案。高频词直接反映游客感知:“排队”“人多”“免费”“夜景”出现的次数,比一个 0 到 1 的情感分好解释得多,也方便在不同省份之间对比。分词的目标是产出(词, 次数)的二元组列表,这个结构与 pyecharts 的WordCloud输入完全对齐,后面的可视化不需要再做一次 reshape。

3.2 精确模式的分词与自定义词典

import jieba from collections import Counter jieba.setLogLevel(20) jieba.load_userdict('travel_dict.txt') reviews = df['review'].dropna().tolist() corpus = ' '.join(reviews) words = jieba.lcut(corpus)

setLogLevel(20)关闭 WARNING 以下的日志,否则每次跑都会被加载词库的进度刷屏。lcut返回列表,cut返回生成器;这里数据量不大,用lcut更直接。load_userdict加载的自定义词典,最佳用途是把“南锣鼓巷”“稻城亚丁”这类景点专名锁成完整词条,而不是被切碎;词典的文本格式为每行一个词,可附带词频和词性,示例:

南锣鼓巷 5 n 稻城亚丁 5 n 夜爬泰山 3 v

词频和词性不写也能生效,但写上之后对posseg词性过滤有用。如果没有自定义词典,jieba.cut很可能把“南锣鼓巷”拆成“南锣/鼓巷”,词频统计会把景区名拆得没意义,这也是分词结果里出现大量单字词的主要来源。

停用词的选取是分词之后的第一道过滤。用集合装停用词,再用列表推导式过滤:

stopwords = { '景点', '我们', '一个', '可以', '非常', '然后', '还有', '地方', '这里', '不过', '因为', '所以', '自己', '什么', '就是' } filtered = [ w for w in words if len(w.strip()) > 1 and w not in stopwords and not w.isdigit() ] freq = Counter(filtered) top100 = freq.most_common(100)

len(w.strip()) > 1过滤单字词;w not in stopwords去掉高频但没信息量的虚词;not w.isdigit()排除“2024”“300”这类纯数字。三个条件用and连接,含义是“同时满足三个条件的词才保留”。评论正文里偶尔带换行符,词条可能粘着空白,所以对w.strip()的长度做判断,避免空白干扰字数计算。

3.3 词性过滤:从词云里挖出“动作词”

Counter.most_common(100)已经能满足WordCloud的最小输入要求,即列表元素为二元组。若想按词性收窄范围,用jieba.posseg

import jieba.posseg as pseg pairs = [(w.word, w.flag) for w in pseg.cut(corpus)] verbs = [w for w, flag in pairs if flag.startswith('v')] verbs_freq = Counter(verbs).most_common(50)

flag.startswith('v')判断动词,“打卡”“爬山”“露营”这类动作词正好落入该集合;名词过滤则把判断条件改成flag.startswith('n'),能拿到“山海、古镇、缆车”这类场景词。按需保留比跑完整词表再手工抓重点更可控。实际做分析时,建议保留两套词频结果:名词喂词云,动词留作后续人工解读,这样汇报时可以现场对比“游客去了哪”和“游客在做什么”。

4. pyecharts 可视化:地图、柱状图、词云与一屏排布

4.1 图表选型与数据接口

可视化阶段先想清楚三张图各回答什么问题。Map回答“全国各省热门景点数量分布”,Bar展示票价或评分的 TOP10,WordCloud承接第 3 章词频结果。三种图表对应三种数据类型接口,源数据都从agg_dftop100取,不需要再回到原始明细表做多维关联。

写地图前注意 pyecharts 版本差异。网上能搜到很多旧教程,0.5.x 时代调用方式是Map("中国地图").add("", data, maptype="china"),新版 v1/v2 的写法是链式调用。用下面命令先确认:

pip show pyecharts | grep Version

确认是 v1 或 v2 后,按下面的Map写法:

from pyecharts.charts import Map, Bar, WordCloud, Page from pyecharts import options as opts map_data = agg_df[['province', '景点数']].values.tolist() map_chart = ( Map() .add( series_name='热门景点数量', data_pair=map_data, maptype='china', is_map_symbol_show=False, ) .set_global_opts( title_opts=opts.TitleOpts(title='全国热门景点数量分布'), visualmap_opts=opts.VisualMapOpts( min_=int(agg_df['景点数'].min()), max_=int(agg_df['景点数'].max()), is_piecewise=True, range_color=['#e8f5e9', '#66bb6a', '#2e7d32'] ), ) )

data_pair接收的是“省份、数值”两两成对的列表,顺序不要颠倒。is_map_symbol_show=False去掉地图上的散点标记,让区块颜色成为主视觉。visualmap_optsis_piecewise=True表示按区间分段显示,比连续色带更容易读出“哪些省份落入同一档位”。如果地图整体灰掉或者某个区域不上色,先回 2.3 节查省份名映射,不要先怀疑绘图代码。

4.2 Bar 的 X 轴标签与坐标轴设置

柱状图用于排名对比,Y 轴刻度直接影响读图结论。对旅游评分这种取值范围基本落在 4 到 5 之间的数据,整体用AxisOpts里的min_固定起始值,能避免“把微小差距放大成夸张条长”的误导:

top10_price = agg_df.head(10) bar_chart = ( Bar() .add_xaxis(top10_price['province'].tolist()) .add_yaxis( '门票中位数', top10_price['票价中位数'].tolist(), category_gap='50%', ) .set_global_opts( title_opts=opts.TitleOpts(title='热门景点票价中位数 TOP10'), yaxis_opts=opts.AxisOpts(name='元'), xaxis_opts=opts.AxisOpts( axislabel_opts=opts.LabelOpts(rotate=45, interval=0) ), ) .set_series_opts( label_opts=opts.LabelOpts(is_show=True, position='top') ) )

category_gap='50%'控制柱间间距,值越大柱越细,适合省名较长的横向空间。rotate=45让 X 轴标签斜排,解决长省名重叠;interval=0强制每根柱子都显示标签,避免 pyecharts 默认跳过部分标签。position='top'把数值放到柱顶,信息层次比放在柱子中间更清晰。

4.3 Page 的拖拽布局,而不是把所有图塞进 Grid

把多图合版排在一页时,先分清两类容器。Grid适合同坐标系图表的排列组合,例如柱状图和折线图共用一套 X/Y 轴时用Grid叠加;把地图、词云这类不带数值坐标系的图硬塞进Grid,会出现大片空白、覆盖或者坐标轴标题错位。要排“地图 + 柱状图 + 词云”这种混合多图,用Page更合理:

page = Page(layout=Page.DraggablePageLayout) page.add(map_chart, bar_chart, wordcloud_chart) page.render('tourism_dashboard.html')

Page(layout=Page.DraggablePageLayout)渲染出的 HTML 支持在浏览器里直接拖拽每张图表调整位置。拖拽完成后点击页面上的“保存布局配置”,会生成一个chart_config.json;随后回到脚本执行:

page.save_resize_html( 'tourism_dashboard.html', cfg_file='chart_config.json', dest='tourism_dashboard_final.html' )

save_resize_html读取拖拽后的配置并按配置重排图表,输出文件可独立发布。这个流程比在代码里手工估GridOpts(pos_top='5%', pos_left='10%')高效得多,也间接解决了一部分可视化大屏适配问题:图表比例由手动调整落定,不用反复猜测像素值。

提示:若目标环境不允许交互式操作,退回Grid时用grid_opts=opts.GridOpts(pos_top='...', pos_left='...')手动定位,但只适用于能共享坐标系的 Bar/Line 组合,跨类图仍优先Page

5. 数据—图表交叉验证与 HTML 报告导出的落点细节

5.1 聚合结果反向抽检,避免坐标轴自欺

地图和柱状图取数之后,最容易出错的不是绘图代码,而是“聚合时悄悄丢了行”。画图之前养成一个嵌入主流程的验证片段:

def check_agg(row, df_origin): province = row['province'] sub = df_origin[df_origin['province'] == province] assert len(sub) == row['景点数'], f"{province} 景点数不一致" assert abs(sub['comment_count'].sum() - row['评论总数']) < 1 return True

逐个省份循环调用该函数,断言失败时立刻回溯到清洗环节。聚合数对不上的原因通常集中在两处:一是drop_duplicates之后的行数没有同步到统计口径,二是省份别名映射在聚合之后才执行,导致groupby里出现两套表述。这个切片对照写法同样适用于其他维度的聚合验证。

5.2 导出最终 HTML 前的三件收尾

最终报告生成前,把下面三个固定动作跑完。地图着色异常时,先打印map_data的前 5 个元素,看省份字段是否与内置地图词表一致;如果出现“广西省”这类别名,回 2.3 节检查替换表的位置。词云里词条偏碎或单字过多,先回头扩展自定义词典,而不是放宽单字过滤条件;词频失真往往是分词阶段造成的,靠后期清洗只会越洗越没信息。Page.DraggablePageLayout生成的初始页面不是最终样式时,要等拖拽后保存布局配置,再执行save_resize_html;若跳过了保存配置,打开 HTML 会看到一份未经排版的初始页面。

把整个管线封装成main()后,约 300 行的结构就落到四个函数和一次调用上:

def main(): df = load_clean_data('tourism_spot.csv') agg_df = aggregate(df) top100 = comment_keywords(df, 'review') charts = make_charts(agg_df, top100) make_dashboard(charts) if __name__ == '__main__': main()

comment_keywords里把 jieba 自定义词典路径单独抽出,下次换语料时不用钻进代码内部找文件名。这份代码交付后,通常只要替换 CSV 路径和词典文件即可复用,避免把数据、字典、图表全混在一个文件里的维护成本。最后的 HTML 文件本身已经包含完整交互图表,不需要额外启动服务,直接用浏览器打开即可放在本地或发给需求方看板。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 12:12:34

OpenClaw全球餐饮IP的市场本地化策略与技术解析

1. 项目背景&#xff1a;OpenClaw的全球热度解析OpenClaw&#xff08;小龙虾&#xff09;作为中国本土爆红的餐饮IP&#xff0c;其成功模式正在引发全球餐饮行业的关注。这个起源于湖南长沙的麻辣小龙虾品牌&#xff0c;凭借独特的口味配方和社交媒体营销策略&#xff0c;在短短…

作者头像 李华
网站建设 2026/9/12 12:12:07

Simulink实现IEEE33节点配电网仿真与潮流计算

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 12:10:14

Redis源码剖析:从事件循环到命令执行的完整链路

动手读过Redis源码的人&#xff0c;多半是被一些"表象问题"勾过去的&#xff1a;为什么单线程还能扛住十万级QPS&#xff1f;为什么某条命令会把整个实例卡住&#xff1f;为什么明明连接数不高&#xff0c;输出缓冲却撑爆了内存&#xff1f;这些问题翻文档翻不出答案…

作者头像 李华
网站建设 2026/9/12 12:10:07

Spring Boot零食电商平台架构设计与实战

1. 项目概述与核心价值"味蕾探索"线上零食购物平台是一个典型的B2C电商系统&#xff0c;专为零食爱好者打造的全流程购物解决方案。这个基于Spring Boot的后台管理系统&#xff0c;实现了从商品展示、用户交互到订单处理的完整闭环。在当前零食电商年增长率超过25%的…

作者头像 李华
网站建设 2026/9/12 12:09:26

Vue 3组件库类型安全实践与原理详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华