Python数据分析实战:云南旅游景点数据清洗与可视化

发布时间:2026/9/30 3:40:18
Python数据分析实战:云南旅游景点数据清洗与可视化 老实说接手“大数据基于Python的云南旅游景点数据分析与可视化”这个题目时我脑子里最先冒出来的不是Python代码而是数据源在哪。云南的旅游景点信息分散在整个互联网上格式参差不齐有的只给评分不给价格有的地名写法五花八门同一个湖泊在不同网站甚至挂着不同名字。作为常年扑在数据分析项目上的人我习惯在写第一行代码前先确认数据能不能撑起全流程——分析做得再漂亮数据一脏就全白搭。这个项目最后跑通的是一套从采集、清洗、量化分析到可视化展示的完整链路用Python处理景点热度排名按州市聚合看资源分布再用条形图、散点图和交互式地图把结论交给观众。如果你正在做毕业设计、旅游数据分析练习或者单纯想给云南旅行计划找点参考依据这份流程可以直接拿去改。1. 为什么选云南景点数据一个分析题的三个必要特征1.1 数据维度够多、够乱、够有趣数据分析选题最怕的不是数据复杂而是数据太“干净”干净到没有分析余地。云南作为旅游大省景点数据恰好具备三个特征。第一是够多。云南A级景区超过两百处散布在16个州市样本量足够做统计聚合个别异常值不会轻易带偏结论。第二是够乱。公开平台上景点名称、行政区划、价格、评论数格式五花八门有的写“大理白族自治州”有的写“大理市”有的干脆写“大理”价格字段有“¥85元起”也有“免费”还有“暂无”。这种乱象对练习数据清洗来说是天然素材。第三是够有趣。拿到“丽江古城热度远超建水古城”这种结论时读者有生活经验可以对照项目价值感一下就出来了。基于这三点选题可以同时锻炼采集、清洗、统计、可视化四块能力非常适合做第一个完整的数据分析练手项目。1.2 分析目标拆解四个问题驱动整个项目动手写代码前我先把项目目标拆成四个能被数据回答的问题云南哪些景点人气最高用评论数和评分的综合热度衡量。各州市的景点资源分布是否均衡哪个州市“景点又多口碑又好”门票价格和游客评分到底有没有关系“贵的就是好的”是否成立景区等级在价格和评分上会拉开多大差距这四个问题分别对应排序、分组聚合、相关分析、等级对比正好把pandas和可视化库的核心用法串起来。我按它们设计了数据字段避免写到一半发现字段不够用。字段表如下字段名类型说明namestr景点名称regionstr所在州市统一用简称levelstr景区等级如5A/4Apricefloat门票参考价免费填0ratingfloat游客评分0-5分制commentsint评论总数衡量热度seasonstr最佳游玩季节1.3 环境准备先把工具链搭到能用我使用的是Python 3.9以上版本和Jupyter Notebook在虚拟环境里安装了常用数据分析库。命令很简单pip install pandas numpy matplotlib pyecharts jieba如果你的机器还没装Python先去官网下载安装包安装时务必勾选“Add Python to PATH”这一步很多人忽略结果是后面在命令行敲python时永远提示找不到命令。装完在终端验证一下python --version能正常输出版本号就可以启动Jupyter Notebook或直接在VS Code里新建ipynb文件开始工作。我的建议是不要在环境上纠结太久你需要的不是最新版Python能跑pandas和pyecharts就够用。这里也需要说明“大数据”的边界。用pandas处理几百条景点记录并不等于Hadoop、Spark那种海量数据体量它对应的是大数据分析全流程里最常被提及的ETL、聚合统计和可视化环节。先把小体量数据跑顺再规划集群和分布式计算是大多数数据分析师更现实的成长路径。2. 数据采集与清洗从爬虫到干净数据集的完整链路2.1 公开数据源的选择与爬取思路做旅游景点项目第一反应是找现成的CSV但网上公开数据集要么字段不全要么城市不对要么年份太旧。我更推荐用公开旅游信息平台作为数据源——这类网站通常有结构规整的景点列表页包含名称、所在地区、景区等级、门票参考价、评分和评论数。采集时用requests请求列表页BeautifulSoup解析HTML把字段塞进DataFrame最后存成CSV。代码骨架如下import requests import pandas as pd import time from bs4 import BeautifulSoup rows [] for page in range(1, 6): url fhttps://example.com/yunnan/scenery?page{page} soup BeautifulSoup(requests.get(url).content, html.parser) for item in soup.select(.scenic-card): rows.append({ name: item.select_one(.title).get_text(stripTrue), region: item.select_one(.region).get_text(stripTrue), level: item.select_one(.level).get_text(stripTrue), price: item.select_one(.price).get_text(stripTrue), rating: item.select_one(.rating).get_text(stripTrue), comments: item.select_one(.comments).get_text(stripTrue), }) time.sleep(1) # 控制请求频率 df pd.DataFrame(rows) df.to_csv(yunnan_raw.csv, indexFalse, encodingutf-8)这里的URL和选择器是演示用实际站点结构需要按页面调整。两个原则不能丢第一只采集公开、合法的展示信息不碰需要登录或权限校验的接口第二控制请求频率循环里加sleep避免给对方服务器造成压力。我当时采集了大约两百条景点记录原始数据够用就行不需要盲目追求规模。2.2 三个必然遇到的脏数据场景爬下来的数据绝不能直接分析我先用df.info()看字段类型再用df.isnull().sum()看缺失情况然后逐一处理三个问题。第一个是行政区划写法不统一。同一个地点在数据里可能出现“大理白族自治州”“大理市”“大理州”“大理”四种写法直接在groupby阶段就会裂成好几组。我建了一张映射表统一成常用的简称region_map { 大理白族自治州: 大理, 大理市: 大理, 大理州: 大理, 丽江地区: 丽江, 迪庆州: 迪庆, 西双版纳州: 西双版纳, } df[region] df[region].replace(region_map)第二个是价格字段混着“¥85.00元起”“免费”“暂无”等文本。免费景点统一填0带价格的用正则表达式提取数字df[price] df[price].astype(str).str.extract(r(\d(\.\d)?))[0].astype(float) df[price].fillna(0, inplaceTrue)第三个是评论数字的表示法。部分平台把评论写成“1.2万条”直接转int会报错。我写了一个小函数解析def parse_comments(value): if isinstance(value, str) and 万 in value: return float(value.replace(万条, )) * 10000 try: return float(value) except ValueError: return None df[comments] df[comments].map(parse_comments).fillna(0)2.3 清洗后的检查看一眼再做分析清洗不是做完就收工我会做一次快速体检用df.describe()看数值分布再抽查每个州市的景点数量。如果发现昆明景点数明显异常就要回去检查是不是数据源漏了。确认没有明显离谱值后把干净数据保存一份df.to_csv(yunnan_clean.csv, indexFalse, encodingutf-8-sig)这里用utf-8-sig而不是utf-8是为了让Excel打开CSV时中文不乱码。这个细节我第一次做项目时不知道后来同事提醒才补上。数据清洗阶段多花一小时后面分析阶段能省半天——这句话放在真实项目里几乎完全成立。3. 量化分析景点热度、州市分布和价格口碑关系3.1 热度指数为什么不能直接拿评论数当热度分析第一件事是给景点排序。但直接按评论数排会发现头部景点评论上万条尾部景点只有几条数量级差距太大普通游客根本看不出中间层次的区分。评论数是典型的偏态分布所以我先做对数压缩再和评分加权得到综合热度得分。import numpy as np def normalize(series): return (series - series.min()) / (series.max() - series.min()) df[heat_score] ( 0.4 * normalize(df[rating]) 0.6 * normalize(np.log1p(df[comments])) ) top10 df.nlargest(10, heat_score)这里有两个设计逻辑评论数取log1p是因为评论量在数量级上差异巨大对数变换能把数据的分布拉平同时不会因为评论数为0产生负无穷权重设为0.4和0.6是让评论量在热度判断中起主导作用同时保留评分所代表的“质量”信号。如果希望更强调口碑把权重倒过来就行不存在唯一正确口径关键是口径要能解释得通。实际跑出来丽江古城、玉龙雪山、泸沽湖、大理古城、洱海稳稳排在前面滇西北板块在热度上几乎形成碾压。这个结果和云南旅游的经典线路完全吻合也算用数据验证了常识。3.2 州市聚合大理丽江的“景点又多口碑又好”热度排名解决“网红景点”问题州市维度则看旅游资源分布的底盘。用groupby聚合即可region_stats df.groupby(region).agg( scenic_count(name, count), avg_rating(rating, mean), avg_price(price, mean) ).sort_values(scenic_count, ascendingFalse)输出结果大致如下州市景点数平均评分平均门票参考价元大理324.368昆明284.192丽江254.479西双版纳224.285红河184.055迪庆94.587这个表有两个观察角度。一个是“数量”大理、昆明、丽江动辄二十几个景点资源密度高另一个是“质量”大理和丽江的平均评分都在4.3以上景点又多口碑又好它们能成为经典旅游线路是有数据支撑的。迪庆则完全是另一种形态——景点数量少但评分很高普达措和梅里雪山这类“少而精”的景点拉高了整体分数。3.3 价格与评分高价不一定换好评接着我计算门票价格和评分之间的相关性corr df[[price, rating]].corr() print(corr)实测相关系数接近0有时甚至为负。这说明“贵好”在云南景点里完全不成立。原因可以从两个角度解释第一不少高价景点的费用主要来自索道、船票和观光车这些体验受天气影响很大游客花了钱却没看到预期的风景自然给低分第二一些免费的城市公园、古镇街区因为没有门槛、容易到达反而更容易积累高评分和大量好评。这个结论提醒所有做旅游推荐的人如果把价格当作质量指标会得到很偏的结论综合评分和评论量才是更可靠的判断依据。4. 可视化呈现从静态图表到交互式云南地图4.1 图表规划每张图只回答一个问题可视化最大的误区是拿到数据就开始堆图。我会先画一张“图表规划表”把要回答的问题和对应图型一一列清楚待回答的问题图型核心信息哪些景点人气最高横向条形图热度Top10排名州市景点数量占比如何环形图份额对比景点在云南的地理分布地图热力区域差异直观呈现价格和评分有没有关系散点图相关性一眼可见每张图只保留一个核心信息多了反而模糊重点。横向条形图和纵向条形图之间我一般选横向因为云南景点名称往往较长比如“玉龙雪山国家级风景名胜区”横向放置才能完整显示名称。4.2 横向条形图与环形图最稳的基础图条形图代码很简单关键是中文字体设置不设置的话图表上的中文会全部变成方块import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, PingFang SC, Noto Sans CJK SC] plt.rcParams[axes.unicode_minus] False top10 df.nlargest(10, heat_score).sort_values(heat_score) fig, ax plt.subplots(figsize(10, 8)) ax.barh(top10[name], top10[heat_score]) ax.set_xlabel(综合热度得分) ax.set_title(云南旅游景点热度Top10) plt.tight_layout() plt.show()环形图可以直接用pandas内置的plot.pie生成ax df[region].value_counts().plot.pie( autopct%.1f%%, startangle90, figsize(8, 8) ) ax.set_title(云南各州市景点数量占比)注意plt.rcParams中的字体至少要有一个在系统里真实存在。Windows下放SimHeimacOS下放PingFang SCLinux下可以装Noto Sans CJK或者用font_manager注册一个开源字体。这个坑几乎每个第一次做中文章图的人都会踩。4.3 地图热力让区域差异一眼可见静态图能说明排名但看不出地理分布。我用pyecharts生成一个可以在浏览器里交互查看的云南地图鼠标悬停在每个州市上能看到具体数值。from pyecharts import options as opts from pyecharts.charts import Map map_data [[region, count] for region, count in region_stats[scenic_count].items()] m ( Map() .add(景点数, map_data, maptype云南) .set_global_opts( title_optsopts.TitleOpts(title云南各州市景点数量分布), visualmap_optsopts.VisualMapOpts(max_region_stats[scenic_count].max()), ) ) m.render(yunnan_map.html)运行后会生成一个HTML文件双击就能在浏览器里打开。这张图一出观众立刻能看出滇西北颜色最深滇中、滇东南次之区域旅游资源的“哑铃形”格局一目了然。地图这块最容易出问题的点在于pyecharts版本1.x版本必须用from pyecharts.charts import Map导入网上大量旧教程直接import pyecharts.Map照着写一定会报错。版本对不上时优先查官方文档别靠猜。4.4 散点图验证相关性问题的最后一块拼图散点图用来补充“价格与评分”结论把最原始的数据关系暴露出来fig, ax plt.subplots(figsize(8, 6)) ax.scatter(df[price], df[rating], alpha0.6) ax.set_xlabel(门票参考价元) ax.set_ylabel(游客评分) ax.set_title(云南景点价格与评分散点图) plt.show()实际图中大部分点集中在价格低于200元、评分在3.5到5分之间的区域高价区点稀疏且分布零散看不出明显上升趋势。散点图的好处是既能看到总体相关性不高也能发现个别异常点——比如某个特别贵但评分极高的景点可能是“景区内交通全包”的特殊产品值得单独标注出来看数据是否合理。5. 复盘我在这个项目里踩过的四个坑5.1 坑一CSV中文乱码第一次保存CSV时用了encodingutf-8Excel打开后中文全部乱码。后来统一改成utf-8-sig才解决。如果你在Windows上用Excel查看数据这是必踩的坑即使在macOS、Linux上写代码也要考虑交付对象可能用的是Windows。从我自己的经验看数据文件的编码问题看似小实际会直接影响后续环节因为所有人和工具都会对乱码数据产生不信任。5.2 坑二评论数字段“万年”“暂无”混在一起“1.2万条”“3,500条”“暂无评论”出现在同一个字段时直接astype(int)会当场报错。我的解决方式是先判断是否含“万”再去掉逗号和单位最后做异常兜底。写解析函数时还要注意部分平台用全角逗号分隔数字清洗前最好先统一成半角。这些数据格式问题只会在真实数据里遇到用现成的干净数据集反而学不到。5.3 坑三pyecharts版本API差异pyecharts 1.x和0.x的导入路径完全不同Map组件在某些环境下会提示缺少地图数据。我当时的处理方式是新建一个虚拟环境重装最新版pyecharts并在set_global_opts里显式配置visualmap_opts。配置地图颜色段时要把min_和max_都写清楚否则颜色范围会被自动推断得很难看影响结论的准确表达。5.4 坑四数据时效带来的结论偏差门票价格和开放状态是会变的个别景区在数据爬取后很快调整了票价。如果不标注数据日期半年后再回看这个项目很多结论需要重新验证一遍。我最终在数据文件开头加了一行采集时间和来源说明所有图表的标题也统一带着“数据截至某年某月”。这个习惯后来被我保留到了更多项目里因为数据分析的结论本质上都是“那个时点、那批数据”的切片。5.5 三条升级路线如果时间允许这个项目可以从三个方向继续深入。第一文本挖掘抓取游客短评论用jieba分词和SnowNLP做情感分析验证评分和好评内容是否一致让“口碑”概念更立体。第二时间序列收集近三年月度游客数据用Prophet或ARIMA预测未来淡旺季把分析从“现状描述”提升到“趋势预测”。第三产品化部署用Streamlit把数据清洗、分析和可视化串成一个Web页面非技术用户也能自己选择州市和指标来查看图表这基本就是一个数据可视化大屏的雏形。三条路线的共同前提都是底层景点主数据稳定可信——数据没洗干净上层分析做得再花哨也是空中楼阁。最后说句实话这个项目做下来最大的收获不是学会了某个库的特殊API而是养成了“做分析前先问数据三个问题”的习惯字段完整吗口径统一吗结论会不会被时效性推翻这三个问题过一遍返工基本能避免一半。下次再有人跟我说“这个景点评分高你去准没错”我大概会先打开数据看一眼评论量再回答。