简介这份资源面向具备一定Python基础、希望入门数据分析实战的开发者与旅游行业从业者围绕去哪儿网国庆期间景点数据展开完整分析流程。包内共7个文件以5个html可视化页面、1个xlsx数据源和1个py分析脚本为主压缩包约79KB体积轻便却覆盖了从数据清洗到结果呈现的关键环节。已有1379人学习下载说明其在同类实战案例中具备一定参考价值。读者可借助脚本完成缺失值、异常值处理并生成各省份景点分布热力图、门票销售额TOP20柱状图、景区星级比例饼图及热门景点推荐排行等图表直观理解游客分布热点、销售业绩差异与星级偏好。整体流程涵盖数据收集、清洗、建模到业务洞察适合作为数据分析入门练手项目也可为旅游营销、定价与服务优化提供数据驱动决策的思路参考。1. 旅游景点数据分析实战从评论表到客流预测的完整复现路径手里拿到一份景区评论数据字段散落在 CSV 和数据库里评分、时间、客源地、门票价格混在一起想跑个客流预测却卡在清洗环节——这是很多做旅游数据分析的人真实的状态。旅游景点数据分析实战这套资源核心就是解决从原始评论、订单、客流记录到可建模特征这条链路上的工程问题。它适合两类人一类是刚接触文旅数据、想拿真实场景练手的分析师另一类是在景区或 OTA 做运营、需要自己动手出报表和预测的从业者。资源本身覆盖数据清洗、评分情感倾向、客流时间序列、客源地分布几个模块不是纯理论讲义而是能直接跑通的脚本和样例数据。下面按我实际拆包的顺序把每个环节的参数、坑和验证方法讲清楚。2. 数据清洗与字段对齐把评论表和客流表拼成一张宽表2.1 先搞清楚两张表的粒度差异旅游数据最麻烦的地方在于评论表是「一条评论一行」粒度是用户-景点-时间客流表往往是「一个景点一天一行」粒度是景点-日期。直接 merge 会炸行数。我一般先把两张表的时间字段统一成date类型评论表按天聚合出「当日评论数」「当日均分」再去和客流表做左连接。这一步不做后面所有时间序列都是错的。常见做法是用 pandas 的to_datetime加dt.normalize()把时间戳压到天再用groupby([poi_id,date]).agg()生成日粒度特征。注意评论表里可能有同一用户同一天对同一景点的多条评论聚合时要先去重否则评论数会虚高。2.2 清洗脚本与参数说明import pandas as pd import numpy as np # 读取原始评论表注意 encoding 常见为 utf-8 或 gbk comments pd.read_csv(comments.csv, encodingutf-8) flow pd.read_csv(flow.csv, encodingutf-8) # 时间字段统一评论时间可能带时分秒压到天 comments[date] pd.to_datetime(comments[comment_time]).dt.normalize() flow[date] pd.to_datetime(flow[stat_date]).dt.normalize() # 去重同一用户同一天同一景点只保留一条 comments comments.drop_duplicates(subset[user_id,poi_id,date]) # 按天聚合评论特征 daily_comment comments.groupby([poi_id,date]).agg( comment_cnt(comment_id,count), avg_score(score,mean), neg_cnt(score, lambda x: (x 2).sum()) # 2分及以下视为负面 ).reset_index() # 左连接客流表保留客流表全部日期 wide pd.merge(flow, daily_comment, on[poi_id,date], howleft) wide[comment_cnt] wide[comment_cnt].fillna(0) wide[avg_score] wide[avg_score].fillna(wide[avg_score].median())逻辑上drop_duplicates的 subset 必须包含poi_id否则跨景点的同日评论会被误删。neg_cnt的阈值 2 分是文旅场景常用经验值如果数据评分是 1-5 分制2 分及以下基本对应差评。fillna用中位数而不是 0是因为评论缺失不代表评分为 0用 0 会把均分拉低后面做相关性分析会失真。参数上encoding如果报UnicodeDecodeError先试gbk再试utf-8-sig。howleft保证客流表日期不丢因为预测目标是客流评论只是特征。如果反过来用inner会丢掉没有评论的日期导致时间序列断档。2.3 字段对齐后的检查清单拼完宽表别急着建模先跑三行检查wide.shape看行数是否等于客流表行数wide.isnull().sum()看还有哪些列有缺失wide[date].diff().dt.days.value_counts()看日期是否连续。如果出现大于 1 的间隔说明客流表本身有缺日需要补全日期索引再 reindex。这一步很多教程跳过但实际做预测时日期不连续会让滞后特征全部错位。3. 评分情感倾向与客源地分布用轻量脚本替代重型模型3.1 为什么不用 BERT 做评论情感旅游评论普遍短、口语化、带方言用预训练大模型跑情感不是不行但资源里给的样例数据量不大跑 BERT 的收益远不如把规则做细。我一般用「评分 关键词规则」双通道评分直接作为强标签关键词规则处理评分缺失或评分与文本矛盾的情况。比如「风景不错但厕所太脏」这种评分可能给 4 分但文本里有负面词规则通道会把它标成混合情感。关键词表可以按景区场景定制正面词包括「值得」「惊艳」「方便」「干净」负面词包括「排队」「宰客」「脏」「失望」。匹配时用str.contains加正则注意中文分词边界别用简单的in否则「不干净」会被「干净」误判。3.2 客源地提取与分布统计import re # 从用户信息表提取省份常见格式广东省深圳市、北京朝阳区 def extract_province(addr): if pd.isna(addr): return 未知 match re.match(r(.{2,8}?(省|市|自治区)), str(addr)) return match.group(1) if match else 其他 user_info[province] user_info[address].apply(extract_province) # 合并到评论表统计各省评论量 comments comments.merge(user_info[[user_id,province]], onuser_id, howleft) province_dist comments.groupby(province)[comment_id].count().sort_values(ascendingFalse) # 计算客源地集中度前三个省份占比 top3_ratio province_dist.head(3).sum() / province_dist.sum()正则(.{2,8}?(省|市|自治区))里的?是非贪婪匹配防止「黑龙江省哈尔滨市」被整段吞掉。.{2,8}限制长度是因为直辖市和省份名称长度差异大不限制会匹配到奇怪的后缀。top3_ratio这个指标比单纯看排名有用如果超过 0.6说明客源高度集中做营销投放时应该优先打透这几个省而不是全国撒网。3.3 分布结果的可视化验证统计完别只看数字画个横向条形图按评论量降序排。如果发现「未知」或「其他」占比超过 20%说明地址解析规则覆盖不够需要回头补正则。常见漏网格式包括「XX省XX市XX区」带空格、「XX市」前面没有省名。这时候可以加一条兜底规则如果匹配不到省就看地址里有没有出现已知城市名用城市反查省份。4. 客流时间序列建模滞后特征与节假日编码的实操细节4.1 特征工程比模型选择更重要客流预测的精度八成取决于特征而不是用 ARIMA 还是 LightGBM。我一般构造四类特征滞后特征前 1、7、14 天客流、滑动窗口7 天均值、14 天标准差、日历特征星期、月份、是否节假日、评论特征当日评论数、均分。滞后特征用shift生成注意 shift 之后第一行会是 NaN建模时要 drop 掉或者用前向填充。节假日编码别只用 0/1因为节假日的「前一天」和「后一天」客流也异常。常见做法是加一列is_holiday_eve和is_holiday_after用pd.DateOffset对节假日列表做前后偏移。如果资源里没有节假日表可以用chinese_calendar库但注意它只覆盖法定节假日景区自己的活动日需要手动补。4.2 建模脚本与参数import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit # 构造滞后特征 for lag in [1, 7, 14]: wide[fflow_lag_{lag}] wide.groupby(poi_id)[flow].shift(lag) # 滑动窗口 wide[flow_roll_7] wide.groupby(poi_id)[flow].transform( lambda x: x.rolling(7, min_periods1).mean() ) # 日历特征 wide[weekday] wide[date].dt.weekday wide[month] wide[date].dt.month wide[is_weekend] (wide[weekday] 5).astype(int) # 去掉含 NaN 的行主要是滞后特征产生的 model_data wide.dropna(subset[flow_lag_14]) features [flow_lag_1,flow_lag_7,flow_lag_14,flow_roll_7, weekday,month,is_weekend,comment_cnt,avg_score] X model_data[features] y model_data[flow] # 时间序列交叉验证不能随机 split tscv TimeSeriesSplit(n_splits5) for train_idx, val_idx in tscv.split(X): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] model lgb.LGBMRegressor(n_estimators300, learning_rate0.05, num_leaves31, min_child_samples20) model.fit(X_train, y_train) pred model.predict(X_val) # 这里算 MAE 或 MAPEgroupby(poi_id)不能省否则不同景点的客流会串行 shift滞后特征完全错乱。TimeSeriesSplit替代train_test_split是硬性要求随机切分会让未来数据泄露到训练集验证分数虚高。min_child_samples20是防止过拟合的常用值如果数据量小于 5000 行可以调到 10。4.3 预测结果怎么验证才可信别只看 MAE。客流预测要分场景看工作日误差、周末误差、节假日误差分开算。如果节假日 MAE 是工作日的三倍说明节假日特征没做好需要加「节假日类型」多分类而不是一个 0/1。另外把预测值和真实值按时间画折线看峰值有没有对上。峰值对不上MAE 再小也没用因为景区最关心的就是高峰日。5. 避坑与常见问题排查那些跑不通的报错到底卡在哪5.1 日期解析报 OutOfBoundsDatetime现象pd.to_datetime报OutOfBoundsDatetime: Out of bounds nanosecond timestamp。原因原始数据里有 1970 年之前或 2262 年之后的异常日期常见于测试数据或脏数据。解决加errorscoerce把异常日期转成 NaT再统一 drop 或填充。别直接改系统时间范围那是治标不治本。5.2 merge 后行数暴涨现象左连接后行数比左表多出几倍。原因右表评论聚合表里poi_id date不唯一可能因为聚合时漏了某个维度或者原始评论表有重复。解决merge 前先daily_comment.duplicated(subset[poi_id,date]).sum()检查如果有重复回去检查 groupby 的 key 是否完整。5.3 滞后特征全为 NaN现象shift(1)之后整列都是 NaN。原因数据没有按poi_id和date排序shift 是在原始顺序上操作的。解决shift 之前必须wide.sort_values([poi_id,date], inplaceTrue)否则 groupby shift 的结果没有意义。这个坑我踩过不止一次排序这行代码看着不起眼漏了后面全白做。5.4 模型验证分数高得离谱现象交叉验证 R² 超过 0.98。原因特征里混入了未来信息比如用了当天的客流去预测当天或者滑动窗口没有加closedleft。解决检查所有特征列凡是和预测目标同一天生成的一律删掉。滑动窗口用rolling(7).mean()默认包含当前行要改成rolling(7).mean().shift(1)。5.5 中文编码导致关键词匹配失效现象负面词规则一条都匹配不上。原因CSV 读取时编码不对中文变成乱码str.contains自然找不到。解决读文件时显式指定encodingutf-8-sig如果还不行用chardet.detect先探测编码。别用默认编码硬扛Windows 和 Linux 默认值不一样换台机器就翻车。6. 进阶技巧用分位数回归看客流区间而不是单点单点预测给运营的价值有限景区更想知道「明天客流大概率在什么范围」。把 LightGBM 的损失函数从regression改成quantile分别跑 0.1、0.5、0.9 三个分位点就能得到客流区间。0.5 分位是中位数预测0.1 和 0.9 构成 80% 置信区间。参数上alpha控制分位点objectivequantile时学习率要调小到 0.03 左右否则分位线会交叉。# 分位数回归示例 for alpha in [0.1, 0.5, 0.9]: model lgb.LGBMRegressor(objectivequantile, alphaalpha, n_estimators500, learning_rate0.03, num_leaves31) model.fit(X_train, y_train) pred model.predict(X_val) # 保存三个分位的预测结果跑完之后把三条线画在一起如果 0.1 和 0.9 的区间在节假日明显变宽说明模型捕捉到了不确定性这是好事。如果区间宽度恒定说明特征里没有区分高低峰的信息需要加「是否节假日」「天气」这类变量。天气数据资源里没给但常见做法是接一个公开天气 API按天对齐注意 API 返回的是当天天气预测时要滞后一天使用否则又是未来信息泄露。验证区间是否合理可以用「命中率」真实值落在 0.1 到 0.9 之间的比例理论上应该接近 80%。如果只有 60%说明区间偏窄把分位点改成 0.05 和 0.95 再试。这个指标比 MAE 更贴近业务运营看的是「有没有漏掉高峰」。从那以后我每次做时间序列都强制先跑一遍sort_values和shift的单元检查确认滞后特征没有 NaN 才往下走。希望帮到你。本文还有配套的精品资源点击获取