过去大半年,我一直在折腾一个和景区数据有关的项目。甲方最初的需求其实很朴素:把江西省内主要景区的客流、票务、天气、OTA评论这些数据定期抓下来,做成一套可视化大屏,让管理者一眼看清“今天哪里人多、哪里票卖得好”。可真当数据铺开之后,才发现“能看”两个字背后牵扯的东西远比预想的多。这个项目最终被拆成了三个并列标题:基于Python的景区数据获取与分布式分析系统、基于Hadoop生态的景区数据治理与多屏联动可视化平台、基于机器学习的景区热度预测与聚类可视化系统。
三个标题看起来是三个独立项目,实际上合在一起才是完整的一条景区数据流水线:采集、治理、分析、预测、展示。这篇文章我就把这半年踩过的坑、想明白的逻辑、用着顺手的方法完整写出来,给正在做数据采集、数仓建设、机器学习落地,或者文旅行业数字化场景的朋友一个可复用的参考样本。
1. 三个标题的合体逻辑
1.1 先拆需求:三个标题对应平台的三个阶段
立项时甲方分别提了三个标题,一开始团队都以为要开三个项目,等需求文档梳理完才反应过来,这是同一个平台的三个切面。
- 基于Python的景区数据获取与分布式分析系统:这是第一期,核心验证数据能不能稳定拿到、能不能分布式跑得动。对应的是数据最底层的采集与分析能力。
- 基于Hadoop生态的景区数据治理与多屏联动可视化平台:这是第二期,把跑通的Demo变成可维护的平台。核心词是“治理”和“联动”,对应的是数据仓库与产品化展示。
- 基于机器学习的景区热度预测与聚类可视化系统:这是第三期,在平台稳定运行之后做算法增值。核心词是“预测”和“聚类”,对应的是数据消费和决策支持。
把这三个标题连起来读,就是一个数据平台的完整成长路径:先解决数据有没有,再解决数据稳不稳,最后解决数据能不能产生决策价值。这个认知直接决定了后来的技术选型和工期排布,如果从一开始就想着“直接上一套大而全的体系”,大概率会把精力浪费在根本用不上的组件上。
1.2 整体架构与数据流向
整个平台我只用了四层:
- 数据源层:景区票务接口、天气接口、OTA平台点评、地图热力数据、节假日日历、景区基础信息表。
- 采集层:Python + Scrapy/requests + Airflow定时调度,把异构数据统一拉进原始区。
- 存储与计算层:HDFS做分布式存储,Hive做离线数仓,SparkSQL做清洗加工和特征提取,Yarn管资源。
- 应用层:机器学习模型产出的预测结果与聚类分类,加上一套基于Vue + ECharts的多屏可视化前端。
数据流是:多源数据 → Python采集程序 → 原始HDFS → ODS层 → DWD清洗层 → DWS日汇总层 → ADS应用层 → 统一API → 大屏/Web端/移动端。
这套链路最舒服的地方在于每个环节都有清晰边界。采集程序只负责拿到数据,数仓任务只管加工,机器学习任务只读ADS层数据,可视化前端永远不直接碰ODS明细。边界清晰之后,出问题时的排查范围会小很多。
1.3 技术选型:为什么是Hadoop生态而不是一套单机库
技术评审时有人提到直接用MySQL或者ClickHouse单库扛数据,我没有选,原因很简单:这三个标题对应的数据量不是一个数量级。景区基础信息只有几千条,但是按小时采集的客流明细、每一天的OTA评论增量、多年的历史天气记录,一年累计下来几亿条明细是常态。单机数据库能撑住,但磁盘成本和查询压力都不划算。
Hadoop生态的优势在于三点:
- 扩展性强:HDFS存不下就加节点,不需要做分库分表这类人工拆分的活儿。
- 生态完整:Hive让分析师能用SQL做统计,SparkSQL能直接跑清洗任务,Spark MLlib和外部算法库衔接得也顺。
- 存储成本低:Parquet列式存储加Snappy压缩之后,原始日志的存量能压缩掉70%以上。
选型结果就是:离线链路用HDFS + Hive + SparkSQL,调度用Airflow,API查询的加速层加了一个Redis做缓存,没有盲目堆Flink、Kafka这些实时组件。实时链路是二期规划,如果一开始就上,工期和运维复杂度都会失控。
2. 数据获取层:Python采集的工程化细节
2.1 数据源盘点与字段设计
采集之前最重要的事情不是写爬虫,而是把数据源和数据字段盘清楚。直接去抓页面是不可控的,优先找公开API和接口。本项目最终落地的核心数据集有四类:
- 景区日客流:景区ID、日期、检票人数、售票金额、游客来源地分布。
- 天气数据:景区所在区县、日期、最高温、最低温、天气现象、风力等级。
- OTA评论:景区ID、评论日期、评分、评论内容摘要、评论数量。
- 景区基础信息:景区编码、名称、等级(4A/5A)、所在设区市、区县、经纬度、门票价格区间。
字段设计上有一个我反复强调的原则:统一的景区ID编码。不同数据源里同一个景区可能叫“某山风景区”也可能叫“某山景区”,如果不提前建一张景区编码映射表,后面做数仓关联时清洗成本会非常高。这块我也是踩过坑的,后来才补了一张景区维度表,把每个来源的名称、别名、正式编码都维护进去。
2.2 采集框架与调度策略
技术实现层面,我用的组合是Scrapy做页面型抓取,requests/aiohttp做接口型轮询,Airflow做定时调度。核心伪代码如下:
def fetch_daily_visit(date): api_url = "https://xxx/api/visit" params = {"date": date, "page": 1} all_rows = [] while True: resp = requests.get(api_url, params=params, timeout=10) data = resp.json() all_rows.extend(data.get("rows", [])) if data.get("has_next"): params["page"] += 1 else: break write_parquet(all_rows, f"hdfs:///data/ods/ods_scenic_visit/dt={date}")这套写法有几个细节要注意。分页循环一定要设置最大重试次数,防止接口卡死导致任务挂起;写HDFS之前要先把DataFrame的字段类型固定下来,避免某个字段从int变成null时写入报错;Airflow的DAG里给每个采集任务设置超时和失败重试,采集接口偶发超时太正常了,不能一失败就告警刷屏。
限速策略也必须有。对公开接口设置每秒一两次的请求频率,随机加一点抖动,别把对方服务打挂,这也是基本的职业素养。页面型数据用Scrapy自带的下载延迟,接口型用asyncio.Semaphore控制并发数,实测下来比较稳。
2.3 采集过程中的三大坑
第一类是字段变动。第三方接口经常悄无声息地加字段或者改字段类型,排查难度很大。解决办法是写一个Schema校验器,每次采集完成后自动对比字段集,发现缺失或类型变化就报警。
第二类是数据重复。接口重试机制会导致同一批数据被写入两次。解决办法是在写HDFS时做幂等写入,按分区先删除再写入,或者对明细表加唯一键去重。
第三类是编码乱码。OTA评论里有大量繁体字和特殊符号,统一用UTF-8编码,并在入库时做一次非法字符过滤,避免后续SparkSQL解析报错。
2.4 采集数据如何规范入湖
所有源数据落原始区时,我会同时保留两个副本:一个是最原始的JSON/CSV格式,用于出问题回溯;另一个是统一转换后的Parquet格式,用于后续计算。Parquet列式存储配合Snappy压缩,HDFS空间占用比原始JSON少了将近70%,查询性能也好很多。
分区规范是第一位的,HDFS目录必须按主题+日期设计,后续的所有加工任务才能稳定复用。
3. Hadoop生态下的数据治理与分布式加工
3.1 HDFS目录设计与命名规范
目录规范这件事看着琐碎,其实是整个平台能不能长期跑下去的地基。我用的标准目录结构如下:
/data/ods/ods_scenic_visit/dt=2026-01-01 /data/ods/ods_scenic_weather/dt=2026-01-01 /data/dwd/dwd_scenic_visit_clean/dt=2026-01-01 /data/dws/dws_scenic_daily/dt=2026-01-01 /data/ads/ads_scenic_rank/dt=2026-01-01命名统一用小写字母+下划线,层级用ODS/DWD/DWS/ADS标识,清晰表达数据所处阶段。表名里要注明主题域,比如scenic_visit、scenic_weather、scenic_review,这样不管谁接手项目,看一眼路径就知道这层是什么数据。
分区字段用的dt表示日期,统一成yyyy-MM-dd格式,避免有的地方写20260101、有的地方写2026-01-01,导致关联查询时join不上。这个细节在多人协作时很容易出问题,最好在一开始就约定死。
3.2 数仓分层:从ODS到ADS分别做什么
ODS层不做事后诸葛亮,原样接入。哪怕是格式有点乱的数据,也先原样存一份,保留最全的现场信息。
DWD层做清洗和维度补充。核心操作是字段解析、乱码过滤、去重、补景区编码。去重SQL通常长这样:
INSERT OVERWRITE TABLE dwd_scenic_visit_clean PARTITION(dt='2026-01-01') SELECT scenic_id, scenic_name, visit_date, checkin_count, ticket_revenue FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY scenic_id, visit_date ORDER BY etl_time DESC) AS rn FROM ods_scenic_visit WHERE dt = '2026-01-01' ) t WHERE t.rn = 1;这是典型的保留最新一条的去重写法,按景区ID+日期分组,取ETL时间最新的记录。用它把采集层可能的重复数据清洗掉。
DWS层做日粒度聚合。把客流、天气、评论、票务按景区+日期汇总成一张宽表,冗余存储一些高频指标,避免上层每次查询都做大范围JOIN。DWS宽表是整个数仓查询性能最大的功臣。
ADS层是应用层,给机器学习任务和可视化直接使用。数据量很小,就是每个景区每天一条热度得分、一个预测客流、一个聚类标签。这层的数据要让算法和前端同学能直接读,不承担复杂加工逻辑。
3.3 Hive和SparkSQL怎么分工
有人会纠结同一份数仓任务用Hive还是SparkSQL写,我的经验是分摊而不是二选一。
重ETL任务、数据量巨大的全量加工用Hive,因为它对内存资源要求低,跑得慢一点没关系,稳定性好。SparkSQL用在两个地方:一是对DWD层做特征提取和临时分析,因为机器学习特征工程的迭代频次高,Spark的分布式内存计算跑得快;二是给前端API用的结果表计算,出数据的时效性要求高。
如果某个任务在SparkSQL里频繁OOM,可以先检查是不是分区粒度过粗、或者发生了数据倾斜。遇到临时小数据集查询,我也会直接让SparkSQL跑,省去Hive任务的启动等待。
3.4 数据质量监控:不盯就崩
数据治理里面最容易被低估的是监控环节。没有监控的数仓,就像没有仪表盘的汽车,你永远不知道数据是从哪天开始错的。
我给每条关键加工链路都挂了三类检查:
- 完整性检查:每天跑完后统计记录数,如果比上周同一天低30%,立刻告警。
- 一致性检查:DWS表中的景区ID是否都能关联到景区维度表,出现孤儿ID要排查上游。
- 时效性检查:Airflow里给每个DAG设置SLA,到点没跑完就报警。
告警方式不用搞复杂,Shell脚本加邮件通知、或者企业微信Webhook都行。重点不是告警工具本身,而是把规则定下来,并且确保每天有人看一眼质量报表。数据质量是平台的口碑,烂数据上线一次,业务方就会永久失去信任。
4. 机器学习落地:热度预测与聚类可视化
4.1 热度指标:先定义清楚“热度”是什么
做算法之前必须先定义业务口径。和甲方对齐之后,我们把“热度”定义为一个综合得分:
heat_score = 0.5 * 客流量归一化得分 + 0.2 * 票务收入归一化得分 + 0.2 * OTA评论新增得分 + 0.1 * 搜索热度得分权重为什么这么定?因为客流是热度最直接的体现,权重最高;票务收入和客流有相关性但受票价影响,所以给中等权重;OTA评论反映近期口碑活跃度,搜索热度反映潜在关注度,属于锦上添花。指标都是先做min-max归一化,再加权求和,不然量纲不同的话,票务收入的数值会比评论数大几个数量级,求和结果完全失真。
这个口径一定要和业务方一起确认再定稿,不然后续模型训练、前端展示全部白做。
4.2 热度预测:LightGBM的工程化用法
预测目标是最低未来1-7天的景区热度得分和客流量。任务本质是回归问题,模型用LightGBM,在数据量和训练速度上平衡得很不错。
特征工程分成四类:
- 日历特征:星期几、是否周末、是否法定节假日、距节假日的天数。
- 天气特征:最高温、最低温、降水等级、风力等级。
- 历史窗口特征:前一天客流、上周同期客流、去年同期客流、近7天均值、近30天峰值。
- 景区属性特征:景区等级、景区类别、所在区域、门票价格区间。
这段核心实现的关键点是时间序列的划分方式,不能像普通分类任务那样随机K折切分。我采用的是时间滚动验证:用前20个月做训练,后3个月做验证,每个月往前滚动一次。代码简化如下:
import lightgbm as lgb from sklearn.metrics import mean_absolute_percentage_error model = lgb.LGBMRegressor( n_estimators=800, learning_rate=0.05, num_leaves=31, subsample=0.8, colsample_bytree=0.8, random_state=42 ) model.fit(X_train, y_train, eval_set=[(X_valid, y_valid)]) y_pred = model.predict(X_valid) mape = mean_absolute_percentage_error(y_valid, y_pred) print(f"MAPE: {mape:.3%}")实测下来的效果是:平日场景MAPE在8%-12%之间,节假日会明显升高到20%以上。节假日客流本身就受临时事件影响大,而且假期样本只有十几天,模型可学习的规律有限。后续改进方向是给节假日样本单独加大权重,或者直接用规则倍率修正。
4.3 KMeans聚类:景区分级与可视化分色
热度预测解决的是“未来怎么样”的问题,聚类解决的是“现在哪些景区是同一类”的问题。我用KMeans对景区做分级,聚类特征选为:日均客流量、节假日客流峰值倍率、客流增长率、平均点评分。
聚类前必须先标准化,否则日均客流几千的数值会直接淹没点评分这种小数。我用的StandardScaler做z-score标准化。K值的确定用肘部法则,看簇内误差平方和的下降曲线,本项目选K=4,分成四类:核心热门景区、稳步增长景区、季节波动景区、冷门潜力景区。
聚类结果直接映射成大屏上的颜色:核心热门用深红,稳步增长用橙色,季节波动用黄色,冷门潜力用蓝色。这样不用解释算法逻辑,业务方一眼就能看懂。
4.4 预测与聚类结果的落库
训练完成后,预测和聚类结果不是只存在于模型文件里,而是要落成ADS表,交给前端查询。落库SQL并不复杂,但是字段规范要注意:
ads_scenic_heat_forecast: scenic_id, forecast_date, heat_score, forecast_visitors, model_version, etl_time ads_scenic_segmentation: scenic_id, segment_label, segment_name, model_version, update_timemodel_version这个字段特别有用。算法迭代很频繁,有时候同一张表里会混入不同版本模型的预测结果,没有版本号,前端展示就不知道该信任谁。加上model_version之后,不光能回溯历史,还能做AB对比。所有结果表都带dt分区和etl_time时间戳,这个习惯帮我排查了无数次数据问题。
5. 多屏联动可视化平台的落地细节
5.1 技术栈与多屏适配思路
可视化前端我选了Vue + ECharts,而不是直接用现成的商业可视化大屏产品。原因很实在:商业产品虽然开箱即用,但多屏联动的定制逻辑、多层地图下钻、以及和自有数据的深度绑定,自己做更可控。
所谓多屏,本项目里实际是三类屏幕:指挥中心大屏(3840x1080拼接屏)、PC管理端(1920x1080)、移动端(手机竖屏)。适配方案不是写三套代码,而是基于同一套Vue组件,用响应式栅格加rem布局做缩放适配。
大屏和PC端用的是同一套布局逻辑,核心指标卡片区、地图区、趋势图区在尺寸变化时按比例缩放;移动端单独渲染为卡片流,一个景区一张卡片,展示热度等级、预测客流、评论趋势。
5.2 多屏之间的联动机制
联动是这个标题里最容易被低估的难点。真正的联动不只是“手机也能打开看”,而是操作一块屏,其他屏同步响应。
实现方案是URL参数加WebSocket。URL参数承载筛选条件,比如region=xxx&forecast_date=2026-05-01,每个屏幕初始化时解析URL,加载对应数据;当用户在任意屏幕切换筛选条件时,通过WebSocket广播新的筛选状态,其他屏幕收到消息后自动刷新。
实现时有几个容易踩的坑:
- 事件广播要做防抖。用户拖动地图时会连续触发几十次筛选变化,不防抖的话后端API会被打爆。
- WebSocket断线要自动重连,并做心跳检测,不然大屏挂一晚上之后所有联动全部失效。
- 各屏之间要约定统一的时间基准,最好用同一份后端时间,避免前端本地时钟不一致导致数据错位。
5.3 地图下钻与聚类可视化展现
地图这块是用户感知最强的部分。我用的是省→市→区县→景区的逐级下钻,点击上级地图区域后,加载下级区域的数据。ECharts的registerMap注册GeoJSON地图数据,分级下钻时提前把每一级的地图资源打包到CDN,避免每次点击都加载地图文件。
热度预测结果用ECharts的视觉映射组件,在地图上按热度得分着色,深色代表预测热度高、浅色代表热度低。聚类结果则用不同气泡颜色区分景区分级,气泡大小对应客流规模,实现“一图看全所有景区”的效果。
实际项目里最多的反馈是“颜色看不懂”,后来我把图例做得更明确,并点击气泡时弹出景区详情侧边栏,展示预测客流曲线和同类景区对比,业务方满意度提升了一大截。
5.4 可视化性能优化
大屏端有个特殊场景:它需要7x24小时挂着,不能频繁整页刷新,也不能内存泄漏。我做了三个优化:
- 后端API结果统一走Redis缓存,缓存key按「接口名+筛选条件+日期」拼接,命中率能在90%以上,后端压力大减。
- 前端图表只在筛选条件变化时申请更新数据,不做定时全量轮询,避免长时间运行后内存暴涨。
- 地图下钻切换时,用Canvas渲染散点层,降低大量气泡下的卡顿感,同时对可视区域外的散点做剔除。
可视化这个模块看起来最“简单”,其实上线后调整次数最多。我的经验是:先把数据口径和交互逻辑和业务方对齐,再去调样式,顺序反了会反复返工。
6. 常见问题与故障排查实录
6.1 HDFS小文件过多
现象是NameNode内存占用飙升,Hive任务变慢,Spark任务启动时间变得很长。原因很典型:Spark写HDFS表时,有几万个分区就会生成几万个小文件,每个只有几十KB。
解决方法是控制写文件数量。在写结果表之前用coalesce或repartition调整分区数,让每个输出文件至少在128MB左右。对已有的存量小文件,写一个夜间定时任务做合并重写,只保留最新分区。
6.2 SparkSQL任务数据倾斜
现象是任务卡在99%或某个Reducer长时间不动。原因是某些热门景区的数据量远大于其他景区,按景区ID分组聚合时,热门景区所在的处理节点负载高到爆。
解法有两种:大表加盐做两阶段聚合,把小表用广播变量广播到各节点,省去Shuffle。加盐的思路是给景区ID后面拼一个随机的0到N数,先按加盐后的键做局部聚合,再去掉盐值做全局聚合。实测下来任务时长从40分钟降到10分钟以内。
6.3 热度预测的漂移问题
节假日预测不准是最典型的漂移问题。去年五一和今年五一的游客行为差异比较大,模型学的规律可能失效。处理方法是三管齐下:节假日样本在训练时翻倍采样;模型上线后每天计算预测值和真实值的残差,残差连续异常时告警;定期用近三个月数据做增量重训练。
新景区没有历史数据是另一个问题,冷启动方案是用同区域、同等级、同类型的景区均值做初始预测,等积累两周数据后再切到个性化模型。
6.4 多屏数据不一致
业务方最不能容忍的问题是大屏显示今天客流5万,手机端却是4.5万,两边数字对不上。这个问题的根源通常是数据请求时间差和缓存策略不一致。解法是统一API层,大屏、PC、移动端都只访问同一个接口;缓存统一用同一个Redis key和相同的过期时间;前端展示时附带数据更新时间戳,让用户明确知道数据快照时间。
还有一个细节:大屏和移动端要约定同一时区,统一用北京时间格式化日期,跨设备时不出现日期偏移。
7. 我的实际体会与可扩展方向
回到开头的三个标题,我最大的体会是:这三个标题从来不是三个独立项目,而是同一个数据平台在三个阶段的切片。数据采集是生产原料,数据治理是打磨原料,机器学习和可视化则是数据价值的一体两翼:机器学习负责回答“接下来会怎样”,可视化负责回答“这一切意味着什么”。
多屏联动上线之后,甲方看得最多的反而是热度预测模块。因为历史客流只是描述过去,而热度预测能告诉他们下一步人员怎么调度、物资怎么准备、营销资源怎么投放。这让整个平台从“展示型”变成了“决策型”,价值感完全不同。
如果就这个项目继续往下扩展,我会优先做两件事:第一,把实时客流链路补齐,用消息队列加流式计算做小时级甚至分钟级热度更新;第二,把数据血缘和自动数据字典建起来,让每一个指标都能回溯到原始采集任务,不靠人肉记忆。
最后再分享一个小习惯:所有结果表都加上dt分区和etl_time字段,并且统一用model_version标记算法版本。这个习惯看着不起眼,但排查线上数据问题时真的能救命。数据工程做得越久,越会发现,真正撑起一个平台的不是某个高深的模型,而是这些干净、规范、可回溯的日常细节。