Python旅游网站数据分析与可视化:从数据清洗到交互大屏的完整实战

发布时间:2026/10/10 21:50:24
Python旅游网站数据分析与可视化:从数据清洗到交互大屏的完整实战
简介这是一份面向高校学生与Python数据分析初学者的旅游网站数据分析及可视化期末大作业完整源码包评审分达95分以上经过严格调试可直接运行适合作为课程设计参考、大作业提交模板或数据分析练手项目。压缩包共74个文件约13.62MB以36个html可视化页面、10个js脚本、8个css样式、9张jpg图表为主另含3个py脚本、3个ipynb笔记本、1个csv数据集与1份docx说明手册覆盖数据清洗、统计分析到图表呈现的完整链路。内容围绕去哪儿2020年五一旅游数据展开包含热门城市景点评分、全国旅游地图、词云图、漏斗图、水球图、星级数量对比及门票销售情况等多维度可视化成果并配有Flask应用与模板页面便于二次开发与展示。目前已有616人学习下载读者可借此快速理解旅游数据的分析思路、图表选型与项目目录组织方式省去从零搭建的时间成本。1. 从一份期末大作业说起旅游网站数据到底能挖出什么很多人第一次接触 Python 数据分析都是从一份「旅游网站数据分析及可视化」的期末大作业开始的。标题看着简单真动手才发现坑不少数据从哪来、字段怎么清洗、可视化用什么库、图表怎么讲出一个能拿高分的故事每一步都能卡住人。我带过几届学生的课程设计也帮朋友改过类似的商业数据分析项目最常见的翻车现场不是代码写不出来而是数据脏得没法用、图表堆了一屏却没人看得懂结论。这篇笔记就围绕这个方向把一套能跑通、能复现、能拿得出手的旅游网站数据分析与可视化方案拆开讲。适合两类人一是正在做课程设计、需要一份 95 分以上作业思路的同学二是想用 Python 做一份真实业务数据分析练手的从业者。核心链路是「数据获取 → 清洗 → 指标计算 → 可视化 → 结论输出」工具栈以 pandas、matplotlib、pyecharts 为主不依赖大数据集群一台普通笔记本就能跑完。2. 数据从哪来旅游网站数据的三种获取路径与字段设计2.1 三种常见数据来源的取舍做旅游网站数据分析第一步永远是数据。我一般会把来源分成三类按获取难度和真实度排序来源类型典型获取方式优点缺点公开数据集Kaggle、天池、和鲸社区下载字段规整、拿来即用场景偏通用缺少「旅游网站」特色网页结构化抓取requests BeautifulSoup / lxml字段贴合业务需要处理反爬、分页、动态渲染平台开放接口部分旅游平台提供的 API数据干净、稳定有调用配额字段受限课程设计里最稳的做法是「公开数据集打底 少量网页抓取补充」。纯抓取容易在答辩时被问「数据合规吗」纯公开数据集又容易被说「没体现工作量」。我通常建议学生用一份公开的旅游评论或景点数据集做主表再抓一小批景点详情页做维度补充。2.2 字段设计一张旅游数据宽表该有哪些列不管数据从哪来最后都要落到一张分析宽表上。旅游网站场景下我一般会保留这几类字段维度字段景点名称、城市、省份、景点等级5A/4A、标签自然风光/人文历史/主题乐园度量字段门票价格、评分、评论数、收藏数、浏览量时间字段评论时间、抓取时间文本字段评论内容、景点简介字段设计的原则是「能算指标、能分组、能画图」。比如「评分」用来算均值排名「评论数」用来衡量热度「门票价格」用来做价格分布「城市」用来做地域对比。如果原始数据里没有这些列就要在清洗阶段派生出来。2.3 用 pandas 读入并做第一轮体检拿到数据后别急着画图先做体检。下面这段代码是我每次开新项目都会跑的模板import pandas as pd import numpy as np # 读入数据注意编码中文数据常见 utf-8 或 gbk df pd.read_csv(tourism_spots.csv, encodingutf-8) # 第一轮体检形状、类型、缺失、重复 print(数据形状:, df.shape) print(\n字段类型:\n, df.dtypes) print(\n缺失值统计:\n, df.isnull().sum()) print(\n重复行数:, df.duplicated().sum()) # 数值字段的分布概览 print(\n数值字段描述:\n, df.describe()) # 关键分类字段的取值分布 for col in [city, level, tag]: if col in df.columns: print(f\n{col} 取值分布:\n, df[col].value_counts().head(10))这段代码的逻辑是先看数据规模再看字段类型是否合理比如价格被读成字符串就要警惕然后统计缺失和重复最后看数值分布和分类分布。参数上encoding要根据实际文件调整读进来乱码就换gbk或utf-8-sig。describe()能快速发现异常值比如门票价格出现负数或 99999 这种明显是占位符的值。提示如果dtypes里价格、评分显示为 object说明列里混了「¥」「分」这类符号清洗时要先去掉非数字字符再转 float。3. 清洗与指标计算把脏数据变成能画图的表3.1 缺失值、异常值、重复值的处理策略数据清洗没有标准答案但有几条我踩过坑之后固定下来的习惯缺失值数值字段用中位数填充比均值抗异常值分类字段用「未知」填充文本字段缺失直接丢弃该行异常值用 IQR 或 3σ 识别价格类字段我一般设一个业务上限比如门票超过 2000 元的直接标记为异常重复值按「景点名称 城市」去重保留评论数最多的那条# 价格字段清洗去掉货币符号转数值 df[price] df[price].astype(str).str.replace(r[¥元], , regexTrue) df[price] pd.to_numeric(df[price], errorscoerce) # 用中位数填充价格缺失 df[price] df[price].fillna(df[price].median()) # IQR 识别价格异常值 Q1 df[price].quantile(0.25) Q3 df[price].quantile(0.75) IQR Q3 - Q1 lower, upper Q1 - 1.5 * IQR, Q3 1.5 * IQR df df[(df[price] lower) (df[price] upper)] # 按景点名称城市去重保留评论数最多的 df df.sort_values(comment_count, ascendingFalse) df df.drop_duplicates(subset[name, city], keepfirst) print(清洗后形状:, df.shape)errorscoerce的作用是遇到无法转换的值直接变 NaN避免整列报错。IQR 的 1.5 倍是统计学常用系数业务上如果发现砍掉太多数据可以放宽到 3 倍。去重时先排序再保留第一条这个顺序不能反否则保留的不是评论数最多的那条。3.2 派生指标热度、性价比、口碑综合分原始字段往往不够用需要派生一些能讲故事的指标。我常用的三个热度指数 评论数 × 0.6 收藏数 × 0.4权重可按业务调整性价比 评分 / 门票价格价格做归一化后计算口碑综合分 评分 × 0.7 log(评论数) × 0.3# 热度指数先归一化再加权 df[comment_norm] df[comment_count] / df[comment_count].max() df[favorite_norm] df[favorite_count] / df[favorite_count].max() df[heat] df[comment_norm] * 0.6 df[favorite_norm] * 0.4 # 性价比价格归一化后取倒数 df[price_norm] df[price] / df[price].max() df[value_score] df[rating] / (df[price_norm] 0.01) # 口碑综合分 df[reputation] df[rating] * 0.7 np.log1p(df[comment_count]) * 0.3 # 按城市聚合算平均热度和平均评分 city_stats df.groupby(city).agg( spot_count(name, count), avg_rating(rating, mean), avg_price(price, mean), total_heat(heat, sum) ).reset_index().sort_values(total_heat, ascendingFalse) print(city_stats.head(10))归一化的目的是让不同量纲的字段能加权。0.01是防止除零。log1p是对评论数做对数平滑避免头部景点因为评论数过大把综合分拉爆。聚合时用agg一次算出多个指标比循环高效得多。3.3 分组聚合城市、等级、标签三个维度的对比旅游数据分析最有价值的对比维度是城市、景点等级和标签。城市看地域差异等级看资源质量标签看游客偏好。# 按景点等级分组 level_stats df.groupby(level).agg( count(name, count), avg_price(price, mean), avg_rating(rating, mean) ).reset_index() # 按标签分组注意标签可能是多值字段需要先拆分 df[tag] df[tag].fillna(未知) tag_stats df.groupby(tag).agg( count(name, count), avg_heat(heat, mean) ).reset_index().sort_values(avg_heat, ascendingFalse) print(level_stats) print(tag_stats.head(10))如果标签字段是「自然风光;人文历史」这种多值格式要先str.split(;)再explode否则分组会把组合当成一个类别结果没法看。这是我在做电商快递账单数据分析时也遇到过的同类问题多值字段不拆开聚合全是错的。4. 可视化落地从静态图到可交互大屏4.1 matplotlib 静态图先把基础图表画对matplotlib 是基本功课程设计里至少要有几张拿得出手的静态图。我一般会画四张城市热度条形图、价格分布直方图、评分与价格散点图、等级占比饼图。import matplotlib.pyplot as plt import matplotlib # 中文字体设置Windows 用 SimHeiMac 用 Arial Unicode MS matplotlib.rcParams[font.sans-serif] [SimHei] matplotlib.rcParams[axes.unicode_minus] False fig, axes plt.subplots(2, 2, figsize(14, 10)) # 城市热度 Top10 条形图 top_city city_stats.head(10) axes[0, 0].barh(top_city[city], top_city[total_heat], color#4C72B0) axes[0, 0].set_title(城市旅游热度 Top10) axes[0, 0].invert_yaxis() # 价格分布直方图 axes[0, 1].hist(df[price], bins30, color#DD8452, edgecolorwhite) axes[0, 1].set_title(门票价格分布) axes[0, 1].set_xlabel(价格(元)) # 评分与价格散点图 axes[1, 0].scatter(df[price], df[rating], alpha0.5, c#55A868) axes[1, 0].set_title(价格与评分关系) axes[1, 0].set_xlabel(价格(元)) axes[1, 0].set_ylabel(评分) # 等级占比饼图 level_counts df[level].value_counts() axes[1, 1].pie(level_counts, labelslevel_counts.index, autopct%1.1f%%) axes[1, 1].set_title(景点等级占比) plt.tight_layout() plt.savefig(tourism_overview.png, dpi150) plt.show()中文字体不设置会显示方块这是新手最常见的翻车点。axes.unicode_minus False是解决负号显示问题。tight_layout()自动调整子图间距dpi150保证导出清晰度。四张图覆盖了分布、对比、相关、构成四类基本分析答辩时够用。4.2 pyecharts 交互图让图表能点、能悬停静态图适合报告交互图适合演示。pyecharts 是百度开源 ECharts 的 Python 封装做可视化大屏很顺手。from pyecharts.charts import Bar, Pie, Map, Page from pyecharts import options as opts # 城市热度柱状图 bar ( Bar() .add_xaxis(city_stats.head(10)[city].tolist()) .add_yaxis(热度指数, city_stats.head(10)[total_heat].round(2).tolist()) .set_global_opts( title_optsopts.TitleOpts(title城市旅游热度 Top10), xaxis_optsopts.AxisOpts(name城市, axislabel_optsopts.LabelOpts(rotate30)), yaxis_optsopts.AxisOpts(name热度指数), toolbox_optsopts.ToolboxOpts(is_showTrue), ) ) # 省份分布地图 province_data df.groupby(province)[heat].sum().round(2).reset_index() province_data.columns [province, heat] map_chart ( Map() .add(热度, province_data.values.tolist(), china) .set_global_opts( title_optsopts.TitleOpts(title全国旅游热度分布), visualmap_optsopts.VisualMapOpts(max_province_data[heat].max()), ) ) # 组合成一个页面 page Page(layoutPage.SimplePageLayout) page.add(bar, map_chart) page.render(tourism_dashboard.html)ToolboxOpts开启右上角工具栏支持下载图片和切换视图。VisualMapOpts控制地图颜色深浅max_设成数据最大值让色阶分布合理。Page把多个图表拼成一个 HTML打开就是一个小型可视化大屏。省份字段要和 ECharts 地图的省份名对齐比如「广东省」要写成「广东」否则地图上不显示。4.3 可视化大屏的布局与配色原则大屏不是图表堆砌布局和配色直接决定观感。我的经验是布局顶部放核心指标卡景点总数、平均评分、平均价格中间放地图和趋势图底部放明细排行配色主色不超过三种用同一色系的深浅表示数值大小避免红绿同时大面积出现字体标题 16-20px正文 12-14px数字用等宽字体更清晰注意pyecharts 生成的 HTML 依赖 CDN 加载 ECharts 脚本离线演示要提前把脚本下载到本地并改引用路径否则断网就是白屏。5. 避坑与排查那些让作业从 95 分掉到 70 分的细节5.1 中文乱码从读文件到画图全链路现象读 CSV 报UnicodeDecodeError或者图表标题显示方块。 原因文件编码和读取编码不一致matplotlib 默认字体不含中文。 解决读文件依次尝试utf-8、utf-8-sig、gbk画图前设置font.sans-serif为SimHei或Microsoft YaHei并设axes.unicode_minus False。5.2 地图不显示数据省份名称对不上现象pyecharts 地图一片空白或者只有几个省有颜色。 原因数据里的省份名带「省」「市」「自治区」后缀和 ECharts 内置名称不匹配。 解决写一个映射函数把「广东省」→「广东」、「内蒙古自治区」→「内蒙古」、「北京市」→「北京」统一后再传入地图。5.3 分组聚合结果为空字段类型是字符串现象groupby之后mean()报错或结果全是 NaN。 原因数值列里混了非数字字符被 pandas 识别为 object 类型。 解决聚合前先pd.to_numeric(col, errorscoerce)再检查dtypes确认转换成功。这个坑在做离婚率数据分析时也经常遇到百分比字段带「%」号不处理就没法算。5.4 图表横坐标太密集标签重叠看不清现象城市名太多x 轴标签挤成一团。 原因分类数量超过 10 个时默认标签会重叠。 解决axislabel_optsopts.LabelOpts(rotate30)旋转标签或者只取 Top10 展示其余归入「其他」。matplotlib 里可以用plt.xticks(rotation45)。5.5 内存不够数据量大了就卡死现象读几十万行数据时内存飙升程序卡住。 原因pandas 默认用 int64/float64内存占用大。 解决读入时指定dtype比如价格用float32分类字段用category类型能省一半以上内存。数据量再大就分块读chunksize10000逐块处理。6. 进阶技巧让分析结论真正站得住6.1 用相关性分析验证「价格越高评分越高」是不是错觉很多人画完散点图就下结论「价格和评分正相关」但散点图只能看趋势不能给系数。用scipy.stats算一下 Pearson 相关系数和 p 值from scipy import stats # 剔除缺失后计算相关系数 valid df[[price, rating]].dropna() corr, p_value stats.pearsonr(valid[price], valid[rating]) print(f相关系数: {corr:.3f}, p值: {p_value:.4f}) # 如果 p_value 0.05 且 corr 绝对值 0.3才认为有实际相关性 if p_value 0.05 and abs(corr) 0.3: print(价格与评分存在显著相关) else: print(相关性不显著不能下结论)p 值小于 0.05 只说明「不是随机产生的」还要看相关系数绝对值。0.3 以下算弱相关画图时看着有趋势实际业务意义不大。这一步能帮你在答辩时挡住「你这个结论怎么证明」的追问。6.2 用 RFM 思路给景点做分层RFM 是商业数据分析的经典模型套到旅游场景R 是最近一次评论时间F 是评论频率M 是热度值。按三分位数把景点分成高、中、低三档组合出 8 类重点标记「高热度 高评分 低价格」的性价比之王。# 构造 RFM 三个维度 df[R] pd.to_datetime(df[last_comment_date]) df[R_days] (pd.Timestamp.now() - df[R]).dt.days df[F] df[comment_count] df[M] df[heat] # 按三分位数打分注意 R 越小越好要反向 df[R_score] pd.qcut(df[R_days], 3, labels[3, 2, 1]).astype(int) df[F_score] pd.qcut(df[F].rank(methodfirst), 3, labels[1, 2, 3]).astype(int) df[M_score] pd.qcut(df[M], 3, labels[1, 2, 3]).astype(int) # 组合分层 df[RFM_level] df[R_score].astype(str) df[F_score].astype(str) df[M_score].astype(str) # 找出高价值景点 high_value df[df[RFM_level] 333][[name, city, rating, price, heat]] print(high_value.sort_values(heat, ascendingFalse).head(10))qcut按分位数切分保证每档数量接近。rank(methodfirst)是防止评论数有大量重复值导致切分报错。R 维度要反向打分因为「最近评论时间越近」代表越活跃天数越小越好。这套分层比单纯按评分排序更有说服力答辩时能体现方法论深度。6.3 导出分析报告把图表和结论串成一份文档最后一步是把分析结果导出。我一般用两种方式一是df.to_excel()导出清洗后的数据和统计表二是用jinja2模板把图表和结论渲染成 HTML 报告。# 导出多 sheet Excel with pd.ExcelWriter(tourism_analysis.xlsx) as writer: df.to_excel(writer, sheet_name清洗后数据, indexFalse) city_stats.to_excel(writer, sheet_name城市统计, indexFalse) level_stats.to_excel(writer, sheet_name等级统计, indexFalse) tag_stats.to_excel(writer, sheet_name标签统计, indexFalse) print(报告已导出)ExcelWriter支持一次写多个 sheet比分别导出再合并省事。sheet 命名要能看出内容别用 Sheet1、Sheet2。如果数据量超过 Excel 行数上限约 104 万行就改用 CSV 或数据库。做这类项目我最大的习惯是每清洗一步就存一个中间文件命名带日期和步骤号。这样后面发现指标算错了不用从头跑直接回退到上一步。血泪经验是千万别在一个脚本里从读数据一路写到出图中间任何一步出错都得重来。分步骤、留中间产物、每步验证这套流程帮我省了无数后悔药。希望帮到你。本文还有配套的精品资源点击获取