ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

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用encodingutf-8会让列名多出一个\ufeffencodingutf-8-sig能自动把 BOM 剥掉遇到从网页直接下载的 GBK 文件再改用encodinggbk重试。不要凭感觉猜先读进来看结构import pandas as pd df pd.read_csv(tourism_spot.csv, encodingutf-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], keepfirst)duplicated(subset[spot_name]).sum()用布尔和的变形快速给出重复行数drop_duplicates的keepfirst保留最早出现的一条适合多来源拼接的景点主表。如果同一景点在不同城市都有记录可以先看city列再决定要不要把城市加进subset不要把整行去重当作默认操作。提示文件名最好不要带中文路径某些 Windows 环境下 pandas 读中文路径会报 FileNotFoundError但文件明明就在那里。这个坑排查起来很费时间。2.2 数值列的“脏值”清理类型转换与缺失值补全评分的“4.8分”和评论数的“1.2万”是两种典型脏值直接astype(float)会抛异常。推荐两条转换路径统一格式后用pd.to_numeric(errorscoerce)无法解析的值置为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(分, , regexFalse) .pipe(pd.to_numeric, errorscoerce) ) df[ticket_price] ( df[ticket_price].astype(str) .str.replace(免费, 0, regexFalse) .pipe(pd.to_numeric, errorscoerce) .fillna(0) )parse_wan先统一小写再判断以“万”还是“w”结尾避免同一列里“1.2万”和“1.2w”共存时统计偏差。comment_count在转换后直接astype(int)前提是数据不存在小数如果来源流量值可能出现小数保留 float 更安全。score用str.replace(分, , regexFalse)去掉后缀regexFalse避免把“分”字当正则处理。ticket_price把“免费”当 0其他解析不了的落到NaN再fillna(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_indexFalse) .agg( 景点数(spot_name, count), 平均评分(score, mean), 评论总数(comment_count, sum), 票价中位数(ticket_price, median), ) ) agg_df[平均评分] agg_df[平均评分].round(2) agg_df agg_df.sort_values(评论总数, ascendingFalse)as_indexFalse让省份名留在普通列之后转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.possegimport 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展示票价或评分的 TOP10WordCloud承接第 3 章词频结果。三种图表对应三种数据类型接口源数据都从agg_df、top100取不需要再回到原始明细表做多维关联。写地图前注意 pyecharts 版本差异。网上能搜到很多旧教程0.5.x 时代调用方式是Map(中国地图).add(, data, maptypechina)新版 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_pairmap_data, maptypechina, is_map_symbol_showFalse, ) .set_global_opts( title_optsopts.TitleOpts(title全国热门景点数量分布), visualmap_optsopts.VisualMapOpts( min_int(agg_df[景点数].min()), max_int(agg_df[景点数].max()), is_piecewiseTrue, range_color[#e8f5e9, #66bb6a, #2e7d32] ), ) )data_pair接收的是“省份、数值”两两成对的列表顺序不要颠倒。is_map_symbol_showFalse去掉地图上的散点标记让区块颜色成为主视觉。visualmap_opts里is_piecewiseTrue表示按区间分段显示比连续色带更容易读出“哪些省份落入同一档位”。如果地图整体灰掉或者某个区域不上色先回 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_gap50%, ) .set_global_opts( title_optsopts.TitleOpts(title热门景点票价中位数 TOP10), yaxis_optsopts.AxisOpts(name元), xaxis_optsopts.AxisOpts( axislabel_optsopts.LabelOpts(rotate45, interval0) ), ) .set_series_opts( label_optsopts.LabelOpts(is_showTrue, positiontop) ) )category_gap50%控制柱间间距值越大柱越细适合省名较长的横向空间。rotate45让 X 轴标签斜排解决长省名重叠interval0强制每根柱子都显示标签避免 pyecharts 默认跳过部分标签。positiontop把数值放到柱顶信息层次比放在柱子中间更清晰。4.3 Page 的拖拽布局而不是把所有图塞进 Grid把多图合版排在一页时先分清两类容器。Grid适合同坐标系图表的排列组合例如柱状图和折线图共用一套 X/Y 轴时用Grid叠加把地图、词云这类不带数值坐标系的图硬塞进Grid会出现大片空白、覆盖或者坐标轴标题错位。要排“地图 柱状图 词云”这种混合多图用Page更合理page Page(layoutPage.DraggablePageLayout) page.add(map_chart, bar_chart, wordcloud_chart) page.render(tourism_dashboard.html)Page(layoutPage.DraggablePageLayout)渲染出的 HTML 支持在浏览器里直接拖拽每张图表调整位置。拖拽完成后点击页面上的“保存布局配置”会生成一个chart_config.json随后回到脚本执行page.save_resize_html( tourism_dashboard.html, cfg_filechart_config.json, desttourism_dashboard_final.html )save_resize_html读取拖拽后的配置并按配置重排图表输出文件可独立发布。这个流程比在代码里手工估GridOpts(pos_top5%, pos_left10%)高效得多也间接解决了一部分可视化大屏适配问题图表比例由手动调整落定不用反复猜测像素值。提示若目标环境不允许交互式操作退回Grid时用grid_optsopts.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 文件本身已经包含完整交互图表不需要额外启动服务直接用浏览器打开即可放在本地或发给需求方看板。本文还有配套的精品资源点击获取
返回列表