旅游景点数据分析实战:从脏数据清洗到可视化洞察全链路

发布时间:2026/11/2 2:16:03
旅游景点数据分析实战:从脏数据清洗到可视化洞察全链路

简介:这份资源面向希望入门数据分析实战的初学者与旅游行业从业者,以去哪儿网国庆景点数据为样本,完整演示从数据清洗到可视化呈现的分析流程,帮助读者掌握用Python处理真实业务数据的基本方法。压缩包共7个文件,约79KB,包含5个html可视化页面、1个xlsx原始数据表和1个py分析脚本,分别对应图表成果展示、结构化数据存储与数据预处理及绘图逻辑,结构紧凑、便于对照学习。目前已有1385人学习下载。内容覆盖热力图、柱状图、饼状图等多种图表实现,涉及地理信息展示、门票销量对比、景区星级占比等具体场景,并串联缺失值与异常值处理、时间序列趋势观察、业务洞察提炼等环节,读者可据此理解数据驱动决策的完整链路,适合作为课程作业或小型分析项目的参考范例。

1. 旅游景点数据分析实战:从一堆脏评论到能答辩的洞察报告

做旅游景点数据分析,最反直觉的一点是:真正花时间的不是建模,而是把爬下来的评论、评分、门票、客流这些脏数据洗成能算的格式。我见过太多同学一上来就调库跑聚类,结果字段类型全是字符串、缺失值占三成,模型跑出来自己都不敢信。这套方案要解决的就是这个问题——把「景点数据采集 → 清洗 → 多维分析 → 可视化 → 结论输出」串成一条能复现的链路,适合正在做数据分析毕设、课程设计,或者要给业务方交一份景点运营分析报告的从业者。下面按需求、设计、实现、避坑、验收的顺序拆开讲,每一步都给到能直接改参数复用的程度。

2. 需求与技术选型:先想清楚分析什么,再决定用什么

2.1 三类核心分析需求怎么定

景点数据分析听起来宽泛,落到交付物上通常收敛成三类问题,先把这三类定死,后面的字段设计和指标才有依据。

第一类是景点热度与口碑分析:哪些景点讨论量大、评分高、差评集中在哪些维度。这类需求对应的是评论表 + 评分字段,核心指标是评论量、平均分、评分分布、差评关键词。

第二类是客流与票价的关联分析:票价区间和客流、评分之间有没有关系,节假日和工作日的客流差异。这类需求需要门票价格、日期、客流量字段,核心是分组对比和相关性。

第三类是游客画像与偏好分析:评论里反映出的游客关注点(交通、排队、卫生、性价比),以及不同评分段游客的用词差异。这类需求靠文本处理和词频统计,不需要复杂模型。

我一般会建议把这三类做成三个分析模块,而不是混在一个大脚本里。原因是毕设答辩时评委最爱问「你这个结论是怎么来的」,模块化之后每个结论都能追溯到具体的表和具体的计算步骤,翻车概率低很多。

2.2 技术栈选择与理由

这套方案的技术栈不需要多花哨,够用、能讲清楚就行。下面是我常用的组合及选择理由:

环节工具选择理由
数据采集requests + BeautifulSoup轻量,字段可控,适合结构化页面
数据存储SQLite / MySQL毕设用 SQLite 零配置,数据量大再上 MySQL
数据清洗pandas缺失值、类型转换、去重一把梭
文本分析jieba + 词频统计中文分词成熟,不需要训练模型
可视化matplotlib / pyecharts静态图交报告,交互图答辩演示
报告输出Jupyter Notebook代码和结论同页,答辩直接翻

选 SQLite 而不是直接读 CSV 的原因很实际:清洗过程要反复查、反复改,用 SQL 做聚合比在 pandas 里写一堆 groupby 清晰得多,而且答辩时能展示「数据是存在库里的」,显得工程完整度高。文本分析不上 BERT 这类模型,是因为景点评论的诉求是「看出游客在抱怨什么」,词频加人工归类足够,上大模型反而增加解释成本,评委一问「你这个模型为什么这么判」就答不上来。

2.3 数据字段设计

字段设计是后面所有分析的地基,我一般会先画一张表结构再动手写代码。核心三张表:

-- 景点基础表:一个景点一行 CREATE TABLE spot ( spot_id INTEGER PRIMARY KEY, -- 景点唯一编号 spot_name TEXT NOT NULL, -- 景点名称 city TEXT, -- 所在城市 ticket_price REAL, -- 门票价格,单位元 level TEXT -- 景区等级,如 5A/4A ); -- 评论表:一条评论一行,是分析的主力表 CREATE TABLE review ( review_id INTEGER PRIMARY KEY, spot_id INTEGER, -- 关联景点 score REAL, -- 评分 1-5 content TEXT, -- 评论正文 review_date TEXT, -- 评论日期,统一存 YYYY-MM-DD FOREIGN KEY (spot_id) REFERENCES spot(spot_id) ); -- 客流表:一个景点一天一行 CREATE TABLE visitor_flow ( spot_id INTEGER, flow_date TEXT, -- 日期 visitor_cnt INTEGER, -- 当日客流量 is_holiday INTEGER -- 是否节假日 1/0 );

字段设计有两个容易忽略的点:一是review_date统一存成YYYY-MM-DD字符串,不要存成各种格式混着来,否则后面按月份聚合时全是坑;二是is_holiday单独建字段而不是从日期推算,因为节假日调休规则复杂,手工标注一次比写推算逻辑省事。这两点看着小,但直接决定后面分析能不能顺利跑通。

3. 数据清洗与预处理:把脏数据变成能算的表

3.1 缺失值与异常值处理

真实爬下来的数据,缺失和异常是常态。我一般按「先看分布,再定策略」的顺序处理,而不是无脑 dropna。

import pandas as pd import numpy as np # 读取原始评论数据 df = pd.read_csv("raw_review.csv") # 第一步:看缺失情况,决定每列怎么处理 print(df.isnull().sum()) # 评分缺失:无法用于评分分析,直接删除 df = df.dropna(subset=["score"]) # 评论正文缺失:保留记录但标记,评分分析仍可用 df["content"] = df["content"].fillna("") # 评分异常:超出 1-5 范围的视为脏数据,剔除 df = df[(df["score"] >= 1) & (df["score"] <= 5)] # 日期格式统一:转成标准日期,无法解析的置空后剔除 df["review_date"] = pd.to_datetime(df["review_date"], errors="coerce") df = df.dropna(subset=["review_date"]) df["review_date"] = df["review_date"].dt.strftime("%Y-%m-%d") # 去重:同一用户同一景点同一天重复评论只留一条 df = df.drop_duplicates(subset=["spot_id", "content", "review_date"])

这段代码的关键决策在缺失值策略上:评分缺失必须删,因为评分是核心指标;评论正文缺失不能删,因为这条记录的评分还有用,只是文本分析时跳过。很多同学一刀切dropna(),结果样本量掉一半,分析结论的置信度直接崩。异常值处理同理,评分超出 1-5 的先怀疑是爬虫把「评分人数」错抓成了「评分」,剔除前最好抽样看几条原始记录确认原因,别把正常数据误杀。

3.2 文本清洗与分词

评论正文是半结构化文本,直接分词会混进大量符号和停用词,需要先清洗再分词。

import re import jieba # 加载停用词表,去掉「的、了、很」这类无信息词 with open("stopwords.txt", encoding="utf-8") as f: stopwords = set(line.strip() for line in f) def clean_and_cut(text): # 去掉标点、数字、英文字符,只保留中文 text = re.sub(r"[^\u4e00-\u9fa5]", "", text) # 精确模式分词 words = jieba.lcut(text) # 过滤停用词和单字 return [w for w in words if w not in stopwords and len(w) > 1] df["words"] = df["content"].apply(clean_and_cut)

清洗逻辑里len(w) > 1这个过滤条件值得说一下:单字词在中文里信息量极低(「好」「差」单独出现说明不了什么),保留下来只会污染词频统计。停用词表建议自己按景点场景补几条,比如「景点」「景区」「感觉」这类词在评论里高频但无区分度,加进去之后词频结果会干净很多。分词后不要急着统计,先抽十条看看切分效果,jieba 对景点名、地名的切分经常出错,必要时用jieba.add_word()把专有名词加进词典。

3.3 数据入库与一致性校验

清洗完的数据写回数据库,写之前做一次一致性校验,避免脏数据回流。

import sqlite3 conn = sqlite3.connect("tourism.db") # 写入前校验:评论表里的 spot_id 必须都能在景点表找到 valid_ids = set(pd.read_sql("SELECT spot_id FROM spot", conn)["spot_id"]) df = df[df["spot_id"].isin(valid_ids)] # 写入评论表 df[["review_id", "spot_id", "score", "content", "review_date"]].to_sql( "review", conn, if_exists="append", index=False ) # 校验写入结果 cnt = pd.read_sql("SELECT COUNT(*) AS c FROM review", conn)["c"][0] print(f"评论表当前记录数:{cnt}")

外键校验这一步很多人省掉,结果后面按景点聚合时出现一堆spot_id对不上的孤儿记录,图表上凭空多出几个「未知景点」。写入后打印记录数是个低成本的好习惯,能第一时间发现「怎么比预期少了一半」这类问题。数据入库后,后续所有分析都从库里查,不再碰原始 CSV,保证分析口径统一。

4. 多维分析与可视化:三个模块出结论

4.1 景点热度与口碑分析

这个模块回答「哪些景点值得重点关注」。核心是评论量、平均分、评分分布三个指标,按景点聚合。

# 按景点聚合:评论量、平均分、差评率 sql = """ SELECT s.spot_name, COUNT(r.review_id) AS review_cnt, ROUND(AVG(r.score), 2) AS avg_score, ROUND(SUM(CASE WHEN r.score <= 2 THEN 1 ELSE 0 END) * 1.0 / COUNT(r.review_id), 3) AS bad_rate FROM review r JOIN spot s ON r.spot_id = s.spot_id GROUP BY s.spot_id ORDER BY review_cnt DESC """ hot_df = pd.read_sql(sql, conn)

差评率用score <= 2定义,这个阈值可以按业务调整,但一旦定了就要在报告里写清楚,否则评委问「为什么 2 分算差评」会答不上来。热度分析不要只看评论量绝对值,评论量高可能是景点本身大,要结合差评率一起看——评论量高且差评率也高的景点,才是运营真正要介入的对象。可视化上用横向条形图排评论量,用散点图把评论量和平均分放一起,一眼能看出「高热度低口碑」的异常点。

4.2 客流与票价关联分析

这个模块回答「票价和客流、口碑有没有关系」。做法是按票价分箱,对比各箱的客流均值和评分均值。

# 票价分箱:按 50 元一档切分 bins = [0, 50, 100, 150, 200, 10000] labels = ["0-50", "50-100", "100-150", "150-200", "200以上"] spot_df = pd.read_sql("SELECT * FROM spot", conn) spot_df["price_bin"] = pd.cut(spot_df["ticket_price"], bins=bins, labels=labels) # 合并客流数据后按票价档聚合 flow_df = pd.read_sql("SELECT spot_id, AVG(visitor_cnt) AS avg_flow FROM visitor_flow GROUP BY spot_id", conn) merged = spot_df.merge(flow_df, on="spot_id") result = merged.groupby("price_bin").agg( avg_flow=("avg_flow", "mean"), spot_cnt=("spot_id", "count") ).reset_index()

分箱的边界不是拍脑袋定的,要先看票价的分布直方图,尽量让每档里的景点数量别差太多,否则某一档只有一两个景点,均值没有代表性。相关性分析用皮尔逊系数之前,先画散点图看是不是线性关系,票价和客流很可能是倒 U 型(太便宜人挤人、太贵人少),直接算相关系数会得出「没关系」的错误结论。这个坑我在模拟项目里踩过,后来养成习惯:任何相关性结论出来之前,先看一眼散点图。

4.3 游客关注点词频分析

这个模块回答「游客到底在夸什么、骂什么」。做法是把好评和差评分开做词频,对比两边的用词差异。

from collections import Counter # 按评分分组:好评 4-5 分,差评 1-2 分 good_words = [w for words in df[df["score"] >= 4]["words"] for w in words] bad_words = [w for words in df[df["score"] <= 2]["words"] for w in words] good_top = Counter(good_words).most_common(20) bad_top = Counter(bad_words).most_common(20) print("好评高频词:", good_top) print("差评高频词:", bad_top)

词频结果不要直接堆进报告,要做人工归类。比如差评里「排队」「人多」「等」可以归到「拥挤」维度,「贵」「不值」「性价比」归到「价格」维度,「脏」「卫生」「厕所」归到「卫生」维度。归类之后每个维度给个占比,报告里就是「差评中拥挤类占 35%、价格类占 28%」这种能直接支撑运营建议的结论。归类这一步没有标准答案,但一定要在报告里附上归类规则,让结论可追溯。

4.4 可视化图表选型

图表选型的原则是「一张图只说一件事」。评论量排名用横向条形图,评分分布用堆叠柱状图,票价与客流关系用散点图加趋势线,词频用词云或条形图。pyecharts 适合答辩演示时做交互,matplotlib 适合导出静态图进报告。不要在一张图里塞太多维度,我见过把评论量、评分、票价画在同一张双轴图里的,答辩时自己都讲不清哪根柱子对应哪个轴。

5. 避坑与常见问题排查:五个最容易翻车的点

5.1 爬虫字段错位导致评分全错

现象:评分分布图显示大量 5 分,但差评文本里全是抱怨,两者对不上。

原因:页面结构里评分和评分人数挨着,选择器写错抓成了人数,或者分页时字段顺序变了没发现。

解决:爬完先抽 20 条原始记录和页面人工比对,确认每个字段对得上;评分字段加范围校验,超出 1-5 的直接报警而不是静默入库。

5.2 日期格式混乱导致月度趋势断裂

现象:按月聚合的评论量趋势图中间断了一截,或者某个月数据异常少。

原因:原始数据里日期有2024/1/5、2024-01-05、1月5日多种格式,to_datetime解析失败被置空剔除。

解决:入库前统一格式,解析失败的记录单独存一张异常表人工处理,不要直接丢;月度聚合前先GROUP BY substr(review_date, 1, 7)验证每个月都有数据。

5.3 停用词表不匹配导致词频全是废话

现象:词频 Top20 里一半是「景点」「感觉」「真的」这类词,看不出游客关注点。

原因:用了通用停用词表,没有针对景点评论场景补充领域停用词。

解决:先跑一遍词频,把高频但无区分度的词手工加进停用词表,迭代两三轮;同时把景点名、城市名也加进去,避免地名霸榜。

5.4 相关性结论没有散点图支撑

现象:报告写「票价与客流显著负相关」,答辩时被问「你看过散点图吗」,一看是倒 U 型。

原因:直接算皮尔逊系数,没验证线性关系假设。

解决:任何相关性结论出来前先画散点图;非线性关系改用分箱对比或斯皮尔曼等级相关,并在报告里说明选择理由。

5.5 分析脚本和数据库口径不一致

现象:Notebook 里算出的评论总数和数据库里查出来的对不上。

原因:部分分析直接读了原始 CSV,部分读了数据库,两边清洗规则不同。

解决:定一条规矩——清洗后的数据只从数据库读,原始 CSV 只在清洗阶段碰;每个分析模块开头打印一次数据量,和预期对不上立刻停下来查。

6. 验收与答辩:怎么证明这套分析站得住

验收这套方案,我一般分三层查:数据层、分析层、结论层。数据层看三张表的记录数、字段完整性、外键一致性,跑一遍SELECT COUNT(*)和空值统计,数字对得上才算过。分析层看每个模块的输出能不能复现——换台机器、清空缓存重跑一遍 Notebook,结果一致说明没有隐藏的随机性或手工改数。结论层最容易被忽略:每条写进报告的结论,都要能指回具体的 SQL 或代码单元格,评委随机挑一条问「这个数哪来的」,你要能三秒内翻到出处。

答辩时的技巧,我总结成一张对照表:

评委常问回答要点
数据哪来的、多少条说清采集范围、时间跨度、清洗后有效样本量
为什么用这个词频方法说清诉求是看关注点分布,词频加人工归类足够,不上模型是为了可解释
差评率阈值为什么是 2 分说清业务定义,并说明换阈值结论是否稳健
这个结论能指导什么落到具体运营动作,比如「拥挤类差评占比高,建议限流或分时预约」
方案有什么不足主动说样本偏差、单平台数据局限,比被问出来强

最后一条是血泪经验:主动暴露局限比藏着掖着得分高。我见过一个同学被问到数据代表性时支支吾吾,其实他只要说「样本集中在节假日,工作日数据偏少,结论对节假日场景更可靠」就能过关。从那以后我每次带项目,都要求学生在报告最后一页专门写「局限与改进方向」,答辩时主动讲,反而成了加分项。这套从需求到验收的链路,你按自己的数据替换字段和参数就能跑起来,希望帮到你。

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