旅游景点评论数据挖掘实战:从5万条评论中定位游客抱怨与改进点

📅 发布时间:2026/10/10 20:49:40
旅游景点评论数据挖掘实战:从5万条评论中定位游客抱怨与改进点
简介这份资源面向数据分析初学者与旅游行业从业者围绕去哪儿网国庆期间景点数据展开实战演练帮助读者掌握从数据清洗到业务洞察的完整分析流程。压缩包共7个文件以5个html可视化页面为主辅以1个xlsx原始数据表和1个py分析脚本整体约79KB轻量易上手。内容涵盖各省份旅游景点分布热力图、景区门票销量柱状图、星级分布比例饼状图及热门景点推荐排行等模块脚本部分涉及Pandas、NumPy与Matplotlib等常用库的数据预处理与呈现。已有1379人学习下载读者可借此理解地理信息可视化、时间序列趋势分析与数据驱动决策思路适合作为课程作业、项目练手或旅游运营分析的参考案例。1. 旅游景点数据分析实战从5万条评论里挖出游客真正在抱怨什么手里攥着某平台五万条景点评论却只会算平均分和词频这是很多做旅游数据分析的人踩过的第一个坑。旅游景点数据分析实战的核心不是把数据丢进模型跑个准确率而是回答三个具体问题游客在哪个环节体验断崖、哪些抱怨是共性问题、改进哪一项能撬动评分。我做过几个景区的评论挖掘最反直觉的结论是——评分4.8的景点差评里出现最多的词不是“门票贵”而是“排队”和“找不到”。这套方法适合有Python基础、手头有评论或客流数据、想做出可落地结论的运营和数据分析师。下面按数据获取、清洗、主题挖掘、归因分析、可视化验证的顺序拆开讲每一步都给能直接抄的代码和参数。2. 数据获取与清洗把脏评论变成能分析的表格2.1 评论数据从哪来、字段怎么定旅游景点评论的常见来源有三类平台公开评论页、景区自有小程序的评价模块、第三方问卷。我一般优先用平台公开数据因为样本量大且带评分和时间戳。采集时不要只抓评论文本下面这几个字段缺一个都会让后续分析翻车字段名类型用途缺失后果comment_idstring去重主键重复评论拉高词频contentstring文本分析主体无法做主题挖掘scoreint评分归因无法区分好评差评publish_timedatetime时间趋势看不出季节性抱怨user_idstring用户去重水军刷评干扰结论spot_namestring多景点对比无法横向比较采集环节我不建议一上来就上分布式爬虫单景点先用requests加时间休眠跑通确认字段完整再扩量。常见做法是每页休眠1到2秒单次采集不超过5000条避免触发反爬导致数据断档。2.2 清洗脚本去重、去噪、分句原始评论里混着表情符号、重复刷屏、广告导流直接进模型会污染主题。下面这段清洗代码我用了很多次核心是三步去重、去非中文字符、按标点分句。import re import pandas as pd def clean_comments(df): # 1. 按comment_id去重保留第一条 df df.drop_duplicates(subset[comment_id], keepfirst) # 2. 去掉纯表情、纯符号、广告关键词 ad_pattern re.compile(r(加微信|代购|私聊|优惠券|点击链接)) df df[~df[content].str.contains(ad_pattern, naFalse)] # 3. 只保留含中文的评论长度过滤掉3字以下的 df df[df[content].str.contains(r[\u4e00-\u9fa5], naFalse)] df df[df[content].str.len() 3] # 4. 按中文标点分句展开成句级数据 def split_sentences(text): parts re.split(r[。\n], str(text)) return [p.strip() for p in parts if len(p.strip()) 4] df[sentences] df[content].apply(split_sentences) df df.explode(sentences).dropna(subset[sentences]) df df.rename(columns{sentences: sentence}) return df[[comment_id, sentence, score, publish_time, spot_name]] # 调用示例 raw pd.read_csv(comments_raw.csv) cleaned clean_comments(raw) cleaned.to_csv(comments_clean.csv, indexFalse) print(f清洗后句级数据量{len(cleaned)})逻辑说明去重放在第一步是因为重复评论会让后续词频和主题权重失真。广告过滤用正则匹配常见导流词这一步宁可多滤一点广告句混进主题模型会带出无意义的聚类。分句的目的是把一条长评论拆成多个独立语义单元比如“风景不错但厕所太脏”拆成两句后能分别归到正面和负面主题不拆的话整条评论的情感会被平均掉。参数说明len(p.strip()) 4这个阈值可以调评论短句多就降到3长评论为主就升到6。ad_pattern里的词按你实际数据补充不同平台导流话术不一样。分句标点里加上\n是因为很多用户习惯换行写多条感受。清洗完先别急着上模型用cleaned[score].value_counts()看一眼评分分布如果1分和5分占比超过70%说明极端评价多后续主题分析要分评分段做不能混在一起。3. 主题挖掘LDA和BERTopic怎么选、参数怎么调3.1 两种主题模型的选型理由旅游评论的主题挖掘常见做法是LDA和BERTopic二选一。LDA基于词袋速度快、可解释性强适合评论量在1万到10万条、主题边界清晰的场景。BERTopic基于预训练句向量能捕捉语义相似但用词不同的句子比如“排队两小时”和“等了好久才进去”能归到同一主题代价是计算资源高、调参更玄学。我的选择标准很简单如果差评里大量出现同义不同词的情况直接上BERTopic如果评论用词比较统一、主题区分明显LDA足够且更快出结果。下面两个都给出最小可跑通的代码。3.2 LDA主题挖掘sklearn实现与主题数确定from sklearn.feature_extraction.text import CountVectorizer from sklearn.decomposition import LatentDirichletAllocation import jieba # 中文分词去掉单字和常见停用词 stopwords set([的, 了, 是, 在, 我, 有, 和, 就, 不, 人, 都, 一, 一个, 上, 也, 很, 到, 说, 要, 去, 你, 会, 着, 没有, 看, 好, 自己, 这]) def tokenize(text): words jieba.lcut(text) return [w for w in words if len(w) 2 and w not in stopwords] cleaned[tokens] cleaned[sentence].apply(tokenize) cleaned cleaned[cleaned[tokens].map(len) 2] # 构建词频矩阵限制最大词数和文档频率 vectorizer CountVectorizer( tokenizerlambda x: x, preprocessorlambda x: x, token_patternNone, max_features3000, min_df5, max_df0.8 ) dtm vectorizer.fit_transform(cleaned[tokens]) # LDA训练主题数先设8后续用困惑度调 lda LatentDirichletAllocation( n_components8, max_iter20, learning_methodbatch, random_state42 ) lda.fit(dtm) # 打印每个主题的前10个词 feature_names vectorizer.get_feature_names_out() for topic_idx, topic in enumerate(lda.components_): top_words [feature_names[i] for i in topic.argsort()[:-11:-1]] print(f主题{topic_idx}: { .join(top_words)})逻辑说明分词后过滤单字是因为单字在中文里歧义太大进主题模型只会增加噪声。max_features3000控制词表大小太大跑得慢且主题发散太小会丢掉长尾关键词。min_df5表示至少出现在5个句子里才纳入滤掉偶然出现的词。max_df0.8过滤掉80%以上句子都有的词这类词通常是“景点”“门票”这种无区分度的。参数说明n_components是主题数这是LDA最关键的参数。我的做法是先设8到12跑一遍看主题词是否重叠重叠就减分不清就加。更严谨的方法是用困惑度曲线但实操中人工看主题词可解释性更快。max_iter20对多数评论数据够用如果主题还没稳定就加到50。3.3 BERTopic语义聚类与动态主题from bertopic import BERTopic from sentence_transformers import SentenceTransformer # 用中文预训练模型生成句向量 embedding_model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) topic_model BERTopic( embedding_modelembedding_model, languagechinese, min_topic_size30, # 一个主题最少30句 nr_topicsauto, # 自动合并相似主题 calculate_probabilitiesTrue ) topics, probs topic_model.fit_transform(cleaned[sentence].tolist()) cleaned[topic] topics # 查看主题概览 topic_info topic_model.get_topic_info() print(topic_info.head(20)) # 查看某个主题的关键词 print(topic_model.get_topic(1))逻辑说明BERTopic先对句子做向量化再用UMAP降维、HDBSCAN聚类最后用c-TF-IDF提取每个簇的关键词。min_topic_size30是核心参数太小会碎成很多无意义主题太大则会把不同问题合并。nr_topicsauto让模型自动合并相似簇省去手动调主题数的麻烦。参数说明paraphrase-multilingual-MiniLM-L12-v2是多语言模型中文效果够用且比大模型快。如果评论量超过10万条UMAP降维会慢可以先把min_topic_size调大减少簇数量。calculate_probabilitiesTrue会给出每句属于各主题的概率后续做归因时能按概率加权但计算时间翻倍数据量大时可以关掉。跑完两个模型后把主题词和实际评论对照看哪个模型的主题更能对应“排队”“卫生”“指示牌”“票价”这些具体问题就用哪个。我一般两个都跑LDA出快速结论BERTopic验证语义归并是否合理。4. 归因分析差评到底集中在哪个环节4.1 评分与主题的交叉表主题挖出来只是第一步真正有用的是知道哪个主题拉低了评分。下面这段代码把主题和评分做交叉算出每个主题的平均分和差评占比。import pandas as pd # 假设cleaned里已有topic列score是1到5 pivot cleaned.groupby(topic).agg( 评论数(sentence, count), 平均分(score, mean), 差评数(score, lambda x: (x 2).sum()) ).reset_index() pivot[差评占比] pivot[差评数] / pivot[评论数] pivot pivot.sort_values(平均分) print(pivot.to_string(indexFalse))逻辑说明按主题分组后算平均分和差评占比平均分低且差评占比高的主题就是优先改进项。这里用score 2定义差评如果你的数据评分分布偏斜可以改成score 3。交叉表出来后把平均分最低的三个主题对应的原始评论抽出来读20条确认模型归因是否符合直觉。参数说明评论数少于30的主题建议合并或忽略样本太少结论不稳。差评占比和平均分要一起看有的主题平均分不低但差评占比高说明体验两极分化比如“缆车”可能有人觉得省力有人觉得排队久。4.2 时间维度抱怨是不是季节性的cleaned[month] pd.to_datetime(cleaned[publish_time]).dt.month monthly cleaned[cleaned[score] 2].groupby([month, topic]).size().unstack(fill_value0) print(monthly)逻辑说明把差评按月份和主题交叉能看出哪些问题是旺季集中爆发。比如“排队”主题如果只在7、8月差评飙升说明是客流超载而非流程设计问题改进方向是限流和分时预约而不是加派人手。如果某个主题全年差评都高那就是结构性问题得从流程上改。参数说明月份提取用publish_time如果数据里没有时间字段这一步跳过但结论会少一个维度。看月度交叉表时注意区分绝对数量和占比旺季评论总量大差评绝对数高不一定代表占比高。5. 避坑与排查旅游评论分析里最容易翻车的5个地方5.1 现象主题词全是“景点”“门票”“不错”看不出问题原因停用词表没覆盖领域高频无意义词且max_df设得太高导致通用词霸占主题。解决在停用词里加入“景点”“景区”“门票”“值得”“不错”“推荐”这类词同时把max_df降到0.6强制模型关注更有区分度的词。5.2 现象LDA每次跑出来的主题都不一样原因LDA是随机初始化random_state没固定且max_iter不够导致未收敛。解决固定random_state42把max_iter加到50如果主题还在变说明数据量不够或主题数设错了先减主题数再跑。5.3 现象BERTopic跑完发现大量句子被归到主题-1原因min_topic_size设得太大HDBSCAN把不够成簇的句子全标为噪声。解决把min_topic_size从30降到15或10或者用topic_model.reduce_outliers()把噪声句重新分配到最近主题。注意降太小会产生大量碎主题需要权衡。5.4 现象差评占比算出来所有主题都差不多原因评分和主题没有真正关联可能是评分字段本身有问题比如所有评论都是5分但文本是差评。解决先做cleaned.groupby(score)[sentence].count()看评分分布如果评分集中在4到5分但文本有明显负面词说明评分不可信改用情感分析模型重新打标再和主题交叉。5.5 现象分句后句子太短主题模型跑出来全是无意义词原因分句阈值设得太低把“很好”“不错”这种短句也纳入这些句子信息量太低。解决把分句的最小长度从4提到6或者在分词后过滤掉词数少于3的句子。短句不是不能用但只适合做情感统计不适合做主题挖掘。6. 用情感得分验证主题结论一个可复用的校验技巧主题挖掘和归因分析做完后怎么确认结论不是模型幻觉我的习惯是加一步情感得分校验。具体做法是用一个轻量情感分析模型对每句打分然后看每个主题的情感均值是否和评分交叉表的结论一致。如果某个主题在评分交叉表里平均分低但情感得分却偏高说明要么评分数据有问题要么主题归类错了得回去查。from snownlp import SnowNLP def get_sentiment(text): try: return SnowNLP(text).sentiments # 返回0到1越接近1越正面 except: return None cleaned[sentiment] cleaned[sentence].apply(get_sentiment) cleaned cleaned.dropna(subset[sentiment]) sentiment_check cleaned.groupby(topic).agg( 情感均值(sentiment, mean), 评分均值(score, mean), 样本数(sentence, count) ).reset_index() sentiment_check[情感评分差] sentiment_check[情感均值] - (sentiment_check[评分均值] / 5) print(sentiment_check.sort_values(情感评分差))逻辑说明SnowNLP返回0到1的情感分把它和评分除以5后的值做差差值大的主题就是评分和文本情感不一致的地方。差值接近0说明两者一致结论可信差值绝对值大于0.2就要警惕可能是评分刷分或者主题归类有误。这个校验步骤花不了几分钟但能挡掉大部分“模型跑出来但结论不可信”的情况。参数说明SnowNLP对旅游评论这种短文本效果尚可如果数据里反讽句多它的准确率会下降这时候可以换用更大的中文情感模型但计算成本会上去。样本数少于20的主题不看这个指标样本太少情感均值波动大。最后说个我自己的习惯每次跑完主题模型我都会随机抽10条被归到“差评主题”的原始评论读一遍人工确认模型没把反讽当好评、没把广告当体验反馈。这个动作看起来笨但比任何指标都管用。旅游评论里“真是太好了排队三小时”这种反讽句情感模型和主题模型都可能翻车只有人眼能兜住。希望帮到你。本文还有配套的精品资源点击获取