☰
Python旅游数据分析实战:从数据清洗到可视化全流程
2026/10/22 3:36:01 网站建设 项目流程

1. 拿到一份旅游数据,先想清楚要回答什么问题

每年旅游旺季前后,总有人拿着一份景点数据来找我,问能不能帮忙看看"哪个地方值得去""哪类景点人最多""门票和热度到底有没有关系"。这次手上这份数据,是某年河北省旅游景点的汇总表,字段不算复杂,大概包含景点名称、所在城市、景区等级、门票价格、游客评分、评论数量、景点类型这几类信息。数据量不算大,几百条记录,但麻雀虽小五脏俱全,正好适合拿来完整走一遍数据处理与可视化分析的全流程。

我打算用Python把这份数据从"一堆看不懂的表格"变成"能直接下结论的图表和结论"。具体来说,要回答几个问题:河北省哪些城市的旅游资源最集中?5A、4A这些等级和门票价格之间是什么关系?游客评分高的景点有什么共同特征?不同类型的景点(自然风光、历史人文、主题乐园等)在热度和口碑上表现如何?这些问题看起来简单,但真要从数据里挖出靠谱答案,中间要踩的坑一点不少。

这篇文章适合两类人看:一类是刚学完pandas和matplotlib、想找个真实数据集练手的朋友;另一类是手头正好有类似旅游数据、需要快速出分析结论的从业者。我会把每一步的操作意图、参数选择理由、踩过的坑都讲清楚,代码可以直接复制去跑,换成你自己的数据也能用。整个流程走下来,你会发现数据处理最难的不是写代码,而是想清楚"这个字段到底代表什么""这个异常值该不该删""这张图到底想说明什么"。

2. 数据清洗:脏数据不处理,后面全是白干

2.1 先摸清数据的底细

拿到数据第一件事绝对不是急着画图,而是先看看它长什么样。我习惯用三个动作快速摸底:看形状、看类型、看缺失。

import pandas as pd import numpy as np df = pd.read_csv('hebei_tourism.csv', encoding='utf-8') print(df.shape) # 看有多少行多少列 print(df.dtypes) # 看每个字段的数据类型 print(df.isnull().sum()) # 看每列缺失值数量 print(df.head(10)) # 看前10行长什么样

这一步的意图很明确:shape告诉你数据规模,dtypes告诉你哪些字段类型不对(比如价格被读成了字符串),isnull()告诉你哪些列有空洞,head()让你对数据有个直观感受。实测下来,旅游数据最常见的三个问题就是:价格字段带"元"字导致无法计算、评分字段有空值、城市名称有"石家庄市"和"石家庄"两种写法。

提示:read_csv一定要显式指定encoding,中文数据用utf-8或gbk,读出来乱码就换另一个,这是新手最容易卡住的地方。

2.2 处理缺失值和异常值的判断逻辑

缺失值不是无脑删。我的原则是:关键字段缺失且无法推断的,删;非关键字段缺失的,能填就填,不能填就保留但分析时排除。

# 评分缺失的,用同城市同等级景点的中位数填充 df['评分'] = df.groupby(['城市', '景区等级'])['评分'].transform( lambda x: x.fillna(x.median()) ) # 价格缺失的,如果景点是免费的(有"免费"标记),填0 df['门票价格'] = df['门票价格'].fillna(0) # 评论数缺失的,说明可能没有线上数据,填0而不是删 df['评论数'] = df['评论数'].fillna(0)

为什么用中位数而不是均值?因为旅游评分这种数据容易被极端值拉偏,中位数更稳健。为什么按"城市+等级"分组填充?因为同城市同等级的景点,评分分布通常比较接近,用这个粒度填充比全局填充更合理。

异常值方面,我遇到过门票价格填成"9999"的,明显是占位符或者录入错误。处理方式是先看分布,再用业务常识判断:

print(df['门票价格'].describe()) # 如果最大值远超合理范围(比如超过500),单独看看这些记录 outliers = df[df['门票价格'] > 500] print(outliers[['景点名称', '门票价格', '景区等级']])

实测发现,超过500的基本是套票或者包含特殊项目的价格,不是错误数据,所以保留但在分析时单独标注。这就是为什么不能机械地用3σ法则删异常值——业务含义比统计规则更重要。

2.3 字段标准化:让数据能算、能比、能分组

原始数据里,景区等级可能是"5A""AAAAA""国家5A级"三种写法,城市名可能带"市"也可能不带。不统一,后面groupby出来的结果就是散的。

# 统一景区等级 df['景区等级'] = df['景区等级'].str.extract(r'(\d+A)') # 统一城市名,去掉"市"字 df['城市'] = df['城市'].str.replace('市', '') # 价格字段转数值 df['门票价格'] = df['门票价格'].astype(str).str.extract(r'(\d+\.?\d*)').astype(float)

str.extract配合正则表达式是处理这类问题的利器。(\d+A)的意思是"提取一个或多个数字后面跟一个大写A",这样"国家5A级"就变成了"5A"。价格字段先用astype(str)确保是字符串,再用正则提取数字部分,最后转float。这套组合拳打下来,数据就规整了。

注意:str.extract提取不到匹配内容时会返回NaN,所以提取完一定要检查isnull().sum(),确认没有意外丢失数据。

3. 多维度分析:从城市、等级、价格三个切口挖信息

3.1 城市维度:旅游资源分布的马太效应

河北的旅游资源分布很不均衡,这点从数据里一眼就能看出来。我按城市统计景点数量和平均评分:

city_stats = df.groupby('城市').agg( 景点数量=('景点名称', 'count'), 平均评分=('评分', 'mean'), 平均票价=('门票价格', 'mean'), 总评论数=('评论数', 'sum') ).sort_values('景点数量', ascending=False) print(city_stats.round(2))

跑出来的结果很有意思:石家庄、保定、承德这几个城市的景点数量明显领先,而有些城市只有个位数。这背后是历史积淀和地理条件的双重作用——承德有避暑山庄这类顶级资源,保定有红色旅游和自然风光的双重优势,石家庄作为省会则有集散中心的便利。

但景点多不等于质量高。我把"景点数量"和"平均评分"放在一张散点图里看,发现有些城市景点不多但评分很高,属于"精品路线";有些城市景点扎堆但评分平平,可能是同质化竞争。这个发现对旅游规划很有参考价值——如果你时间有限,优先选"少而精"的城市。

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] # 中文显示 plt.rcParams['axes.unicode_minus'] = False fig, ax = plt.subplots(figsize=(10, 6)) ax.scatter(city_stats['景点数量'], city_stats['平均评分'], s=city_stats['总评论数']/1000, alpha=0.6) for city in city_stats.index: ax.annotate(city, (city_stats.loc[city, '景点数量'], city_stats.loc[city, '平均评分'])) ax.set_xlabel('景点数量') ax.set_ylabel('平均评分') ax.set_title('各城市旅游资源数量与质量分布') plt.tight_layout() plt.show()

plt.rcParams那两行是中文显示的必备配置,不设置的话标题和标签全是方框。s参数用总评论数控制气泡大小,一眼就能看出哪些城市既有量又有热度。

3.2 等级维度:5A和4A的差距到底在哪

景区等级是官方评定的,理论上等级越高资源越好。但数据告诉我们,等级和游客口碑之间并不是简单的正相关。

level_stats = df.groupby('景区等级').agg( 数量=('景点名称', 'count'), 平均票价=('门票价格', 'mean'), 平均评分=('评分', 'mean'), 平均评论数=('评论数', 'mean') ).round(2) print(level_stats)

从结果看,5A景区的平均票价确实最高,但平均评分未必比4A高多少,有些4A景区的评分甚至反超5A。原因不难理解:5A评定标准包含大量硬件和服务指标,但游客评分更看重个人体验,一些4A景区在体验感上做得更细致。另外,5A景区往往人满为患,排队时间长反而拉低了评分。

这个发现的实际意义是:如果你只看等级选景点,可能会错过一些口碑极佳的4A甚至3A景区。反过来,一些5A景区如果评分偏低,可能是被过度商业化拖累了。

3.3 价格维度:门票价格和热度到底是什么关系

"门票越贵是不是人越多"这个问题,数据给出的答案是否定的。我算了一下门票价格和评论数的相关系数:

corr = df['门票价格'].corr(df['评论数']) print(f'门票价格与评论数的相关系数: {corr:.3f}')

实测下来这个系数通常在0.1到0.3之间,属于弱相关。也就是说,价格高低对热度的解释力很有限。真正决定热度的,更多是景点本身的独特性和知名度。

但价格分布本身很值得看。我把门票价格分成几个区间,统计每个区间的景点数量和平均评分:

bins = [0, 0.01, 50, 100, 200, 1000] labels = ['免费', '50元以下', '50-100元', '100-200元', '200元以上'] df['价格区间'] = pd.cut(df['门票价格'], bins=bins, labels=labels) price_stats = df.groupby('价格区间', observed=True).agg( 景点数量=('景点名称', 'count'), 平均评分=('评分', 'mean') ).round(2) print(price_stats)

结果通常显示:免费景点的数量占比不低,而且平均评分往往不差;50-100元区间是竞争最激烈的,景点最多;200元以上的高价景点数量少,评分两极分化。这个分布对游客的启示很直接——免费和低价景点里藏着不少高性价比的选择。

提示:pd.cut的bins里我用了0.01而不是0作为免费和低价的分界,因为有些免费景点价格字段是0,有些是0.01的占位,这样分能避免边界问题。observed=True在新版pandas里必须加,否则会报未来警告。

4. 可视化呈现:让图表自己说话

4.1 城市景点数量对比:横向条形图最直观

城市名称是中文,用横向条形图比纵向柱状图更合适,因为标签不会挤在一起。

fig, ax = plt.subplots(figsize=(10, 8)) city_stats['景点数量'].sort_values().plot(kind='barh', ax=ax, color='steelblue') ax.set_xlabel('景点数量') ax.set_title('河北省各城市景点数量对比') for i, v in enumerate(city_stats['景点数量'].sort_values()): ax.text(v + 0.5, i, str(v), va='center') plt.tight_layout() plt.show()

barh是横向条形图,sort_values()让数据从少到多排列,视觉上更舒服。ax.text在每根柱子末尾标上具体数值,读者不用去猜长度。这个图一出来,哪些城市是旅游大户一目了然。

4.2 等级与价格的关系:箱线图揭示分布差异

箱线图特别适合展示"不同类别下数值的分布情况",比只看平均值信息量大得多。

fig, ax = plt.subplots(figsize=(10, 6)) df.boxplot(column='门票价格', by='景区等级', ax=ax, grid=False) ax.set_xlabel('景区等级') ax.set_ylabel('门票价格(元)') ax.set_title('不同等级景区的门票价格分布') plt.suptitle('') # 去掉pandas自动加的标题 plt.tight_layout() plt.show()

箱线图能看出中位数、四分位距和异常值。实测中经常发现:5A景区的中位数价格明显高于4A,但4A的价格分布更集中,5A的分布更分散——说明5A里既有免费开放的,也有高价收费的,差异很大。plt.suptitle('')是为了去掉pandas自动添加的冗余标题,让图更干净。

4.3 评分分布:直方图加核密度曲线

评分是连续变量,直方图能看分布形态,叠加核密度曲线能更平滑地看出趋势。

fig, ax = plt.subplots(figsize=(10, 6)) df['评分'].plot(kind='hist', bins=20, density=True, alpha=0.6, ax=ax, color='coral', edgecolor='white') df['评分'].plot(kind='kde', ax=ax, color='darkred', linewidth=2) ax.set_xlabel('评分') ax.set_ylabel('密度') ax.set_title('景点评分分布') plt.tight_layout() plt.show()

density=True让直方图变成密度图,这样才能和KDE曲线在同一尺度上对比。bins=20是经验值,太少看不出细节,太多显得毛躁。通常旅游评分会呈现左偏分布——大部分景点评分集中在4分以上,少数低分景点拖出左边的尾巴。如果发现双峰分布,那说明数据里可能混了两类差异很大的景点,值得进一步拆分分析。

4.4 城市评分热力图:一眼看出哪里口碑好

如果想同时对比多个城市的多个指标,热力图是很好的选择。

import seaborn as sns pivot = df.pivot_table(values='评分', index='城市', columns='景区等级', aggfunc='mean') fig, ax = plt.subplots(figsize=(10, 8)) sns.heatmap(pivot, annot=True, fmt='.2f', cmap='YlOrRd', ax=ax) ax.set_title('各城市不同等级景区平均评分热力图') plt.tight_layout() plt.show()

pivot_table把数据透视成"城市×等级"的矩阵,aggfunc='mean'算平均评分。annot=True在格子里显示数值,fmt='.2f'保留两位小数,cmap='YlOrRd'是黄到红的渐变色,颜色越深评分越高。这张图能快速定位"哪个城市的哪类景区表现最好",对做旅游推荐特别有用。

注意:如果某个"城市×等级"组合没有数据,热力图上会显示空白。这时候要么在pivot_table里加fill_value=0,要么在分析时说明该组合无样本,不要误读为评分为0。

5. 实操中踩过的坑和排查技巧

5.1 中文乱码:三个地方都要设置

中文乱码是Python可视化的头号杀手,而且它会在三个地方分别出现:控制台输出、图表标题、文件保存。控制台乱码改encoding,图表乱码改rcParams,保存乱码在savefig里指定bbox_inches和字体。

plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False

sans-serif列表里放多个字体是保险做法,不同系统装的字体不一样,列表会按顺序找第一个可用的。axes.unicode_minus = False是解决负号显示成方框的问题,虽然旅游数据一般没有负数,但养成习惯没坏处。

5.2 groupby之后索引变复杂:reset_index救场

groupby之后得到的对象索引是多级的,直接画图或导出会出问题。我的习惯是每次聚合完立刻reset_index():

city_stats = df.groupby('城市').agg(...).reset_index()

这样城市就变回普通列,后续操作不用再跟多级索引较劲。如果忘了这一步,city_stats['城市']会报KeyError,新手经常卡在这。

5.3 评论数差异巨大:考虑对数变换

评论数这种数据通常跨度极大,头部景点几十万条,尾部景点几十条。直接画图的话,小值全挤在底部看不见。这时候用对数变换:

df['log_评论数'] = np.log1p(df['评论数'])

log1p是log(x+1),专门处理有0的情况。变换后再画图,分布会舒展很多。但要注意,对数变换后的数值失去了原始量纲,解读时要说明"这是对数尺度"。

5.4 常见问题速查表

问题现象可能原因解决方法
图表中文显示方框未设置中文字体设置rcParams['font.sans-serif']
groupby后取列报错多级索引未重置聚合后加.reset_index()
价格字段无法计算含非数字字符正则提取数字后转float
城市分组结果分散名称不统一统一去掉"市"字等后缀
热力图出现空白格该组合无数据加fill_value或单独说明
散点图小值看不见数值跨度过大对数变换或调整坐标轴范围

5.5 几个容易被忽略的细节

第一,pd.cut分箱时,如果数据里有负值或超出范围的值,会被标记为NaN。分箱前先用describe()确认数据范围,避免漏掉记录。

第二,seaborn的heatmap默认会显示所有格子,如果数据量大,标签会重叠。这时候要么减少行列数,要么调小annot的字体。

第三,保存图片时用plt.savefig('fig.png', dpi=300, bbox_inches='tight'),dpi=300保证清晰度,bbox_inches='tight'去掉多余白边。直接截图的话分辨率不够,放到报告里会糊。

第四,分析结论一定要结合业务常识。比如发现某个5A景区评分只有3分,先别急着下"名不副实"的结论,去看看评论数是不是很少——样本量小的时候,个别差评就能把平均分拉很低。

6. 从数据到结论:这份分析能用来做什么

走完整个流程,手上就有了一套完整的分析结果:城市旅游资源分布图、等级与价格关系图、评分分布图、城市口碑热力图。这些图表组合起来,能支撑好几类实际决策。

对游客来说,可以快速定位"哪些城市景点多且口碑好""哪个价格区间性价比最高""哪些4A景区评分反超5A值得一去"。对旅游从业者来说,能看出"哪些城市旅游资源稀缺存在开发空间""哪类景点同质化严重需要差异化""定价和口碑的平衡点在哪里"。对做数据分析的朋友来说,这套流程可以原样套用到其他省份甚至其他行业的数据上,换掉字段名和业务解释就行。

我个人在实际操作中的体会是,旅游数据分析最忌讳"为了画图而画图"。每张图背后都要有一个明确的问题,图只是回答问题的工具。有时候一张简单的横向条形图,比五张花哨的雷达图更有说服力。另外,数据清洗花的时间往往比分析本身还多,但这一步省不得——脏数据跑出来的结论,越漂亮越危险。

最后分享一个小技巧:分析做完后,把关键结论用一两句话写在每张图下面,形成"图+结论"的卡片式报告。这样别人不用懂代码也能直接看懂你的分析,传播效率会高很多。这个习惯我从做第一份数据报告保持到现在,屡试不爽。

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

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

立即咨询