资讯动态

基于大数据与推荐系统的旅游行程规划系统设计与实现

发布时间:2026/10/17 3:50:09 来源:尧图企业网站定制
1. 从一个痛点说起旅游行程规划为什么需要大数据做这个基于大数据驱动的旅游行程规划系统最初是被一次自由行折腾出来的。那次我在旅游平台上翻了三天攻略收藏了二十多篇游记最后排出来的行程还是把两个相距两小时车程的景点塞进了同一天。回来后我就想能不能用数据把这件事做得更靠谱把游客行为、POI热度、天气、交通这些信息汇集起来自动生成一份能直接用的行程再用可视化把整条路线和时间安排摊在用户面前。于是就有了这个叫“某行程工具”的模拟项目X。这套系统本身解决的并不是“找个景点推荐给你”这种单点问题而是一连串的矛盾用户既要个性化又要省时间既要覆盖热门地标又要照顾更小众的选择既要一天走完合理路线又不能被现实中的通勤时间打脸。它适合三类人参考一是自由行用户想理解行程规划工具背后的逻辑二是做旅游产品、OTA相关功能的产品经理想知道推荐和规划怎么结合三是用 Python 做数据项目、推荐系统、可视化练手的开发者。这篇内容不会只给你看结果而是把从数据到推荐的完整链路讲清楚包括我踩过的坑。1.1 传统行程规划的三个老大难先列一下当时让我觉得“必须得用数据做”的三个核心痛点它们也是后来系统设计里的主要约束。第一信息过载且互相冲突。打开任何旅游社区同一个城市能搜出几十篇攻略每篇推荐的路线都不同。评论区和榜单也经常打架某景点评分很高但评论里全是“排队两小时参观十分钟”。纯靠人肉比较效率太低而且很容易被单篇攻略的写作倾向带偏。这时候需要的是把多源数据汇总成可比较的定量指标而不是继续刷评论。第二约束条件太多人工规划很容易遗漏。一个完整的自由行路线涉及景点开放时间、停留时长、两两之间的交通耗时、用餐时间、体力上限甚至天气影响。人工规划默认只关注“哪些景点有名”很容易忽略“今天该不该去户外”或者“两个景点之间实际开车要多久”。一旦路线排出来用户真正执行时就会到处卡壳。第三通用榜单不解决个性化问题。热门榜单给的是“对大多数人比较稳”的结果但用户需求的差异非常大带老人小孩的和年轻人徒步需求的节奏完全不同喜欢拍照的人希望多留观景时间美食向用户可能希望中午路线经过老城区小吃街。通用榜单没有用户画像自然也没法解释“为什么推荐这个”。1.2 大数据和可视化切入的解题思路我把这套系统定义为“数据驱动”核心不是堆数据量而是把多维度数据变成可计算的信号。系统用了三类数据静态数据景点坐标、开放时间、类型、门票参考价、动态数据节假日热度、天气、实时路况、用户行为数据点击、收藏、下单、搜索。三类数据汇合后经过清洗和特征工程进入推荐与规划模块。可视化在这里不是花架子而是行程规划系统里真正的一环。用户在页面上看到的不应只是一串“第几天去哪个景点”的列表而是一张能体现时间、空间、拥挤度、推荐理由的图地图上的路线连线、热力图层表示人流、时间轴上的景点块表示停留时长、侧边卡片解释推荐原因。用户发现问题可以直接修改系统会重新优化。这个交互闭环是传统行程规划工具最缺的部分。从实现上看这也解释了为什么选择 Python从数据清洗到推荐模型再到后端接口和图表数据聚合Python 生态几乎全覆盖。所以后面整个项目的技术栈也都是围绕 Python 展开的。2. 技术选型与整体架构Python 生态怎么搭2.1 一个可以复用的分层架构如果你准备从零开始做类似系统先不要急着写代码我把最终的项目结构列出来这样后面每个模块都有落位。数据采集层爬取公开旅游评论、POI 信息对接天气和地图路线接口把数据落到原始表。存储层MySQL 存结构化数据包括景点、评论、用户行为Redis 存热榜、缓存和实时计算中间结果。计算层Pandas/Dask 做特征处理implicit 做协同过滤推荐自研规则和约束算法做行程编排。服务层FastAPI 提供查询、推荐、行程规划、可视化数据接口。可视化层前端页面用 Vue3 ECharts 渲染地图热力、路线、时间轴内部调试时也会用 Streamlit 快速搭建看板。这个分层不是拍脑袋定的。比如一开始我没有用 Hadoop 那套重型体系因为项目的数据量虽然叫“大数据”但通常也就是几十 GB 级别单机 Dask 并行处理完全够。真正决定用 Dask 是因为 Pandas 在数据量增长后内存容易爆而 Dask 惰性计算能把超过内存的数据按分区处理让清洗和聚合流程不用改写太多。如果以后数据真的到了 TB 级这套计算层也可以平滑替换成 Spark。2.2 数据处理从零构造一个“可被推荐”的景点池原始数据拿回来之后第一件事不是训练模型而是做归一化。我见过很多项目上来就调参结果数据里同一个景点因为别名不同被拆成两条记录推荐结果当然稀碎。景点名称、地址、坐标、类型、开放时间这些都是基础字段必须保证唯一和干净。景点坐标的处理我用了 GeoHash 编码。比如某个景点的精确定位是 30.123456, 120.654321GeoHash 前 6 位可以把坐标粗粒度到几百米范围方便做区域聚合。这样在做“某片区热度榜”时不需要每次都去跑空间索引直接按 GeoHash 前缀分组就行。清洗后的景点池字段大致如下poi_id、poi_name、city_id、geo6、poi_type景区/博物馆/美食/购物/自然风光、score、comment_cnt、click_cnt、avg_stay_minutes、open_time、close_time、price_level。这里 avg_stay_minutes 我用了分位数而不是平均值因为平均值容易被极端值拉高比如有人在一个博物馆逛了 6 小时但绝大多数人只待 1.5 小时。2.3 推荐引擎多路召回 排序为什么不用上来就上深度学习推荐模块我没直接上深度模型。核心原因是用户行为数据量不足以训练复杂网络而且用户需要一个“为什么推荐这里”的解释直接用深度模型很难给。最终实现的是多路召回再加规则排序。第一路是热榜召回。基于近 7 天点击量、评论量、评分加权得到一个综合热度分保证冷启动用户看到的不是垃圾内容。第二路是规则召回根据用户主动选择的城市、主题、同行人类型来做标签过滤。第三路是协同过滤召回用隐式反馈矩阵做物品的相似推荐解决“看过故宫的人也可能喜欢南锣鼓巷”这类问题。排序阶段没有单独上排序模型而是按一套启发式分数融合个性化得分占 0.5热度占 0.3距离合理性占 0.2。为什么这么设因为在行程场景里用户对“顺路”的容忍度很低一个高评分但和你起点相距一个半小时的景点排在前面没有意义。这套权重的值不需要一开始很精确等收集到用户对推荐结果的反馈后再迭代就行。3. 核心算法落地行程是怎么被算出来的3.1 用户画像把“我想轻松一点”变成可计算的特征很多行程规划工具把用户输入做成了选择题但选择题收集到的信息和真实需求之间还有一层翻译。比如用户说“我不想太赶”系统不能直接理解这句话需要把它映射为参数每天景点数量上限、相邻景点最大通勤时间、每天自由活动时间下限。我项目里引入了三个节奏档位轻松、平衡、紧凑。轻松档要求每天景点不超过 3 个每段车程不超过 40 分钟午休时间至少 1 小时紧凑档允许每天 5 个景点车程上限放宽到 60 分钟午休缩短到 30 分钟。用户第一次使用时如果行为数据不足就靠这个档位做最基础的过滤。如果用户已经有浏览、收藏行为画像特征还包含偏好类型分布自然风光、人文历史、亲子、美食、购物各自权重加总为 1。3.2 把“一日行程”变成一个带约束的路径问题行程规划核心是“给定一堆可能想去的景点怎么在同一天内排出一条合理路线”。这个问题的本质是一个带时间窗的路径优化问题每个景点有开放时间需要停留一段时间景点之间移动需要通勤时间而且一天的总时间有限。我一开始直接用全排列枚举发现景点超过 8 个以后计算量就完全不可控了。后来改成贪心 2-opt 优化先用贪心规则生成一条初始路线规则是每一步选择“综合得分除以时间成本”最大的景点然后做 2-opt 局部搜索交换线路中的两对相邻关系如果总耗时减少了就保留交换。由于单日候选景点通常控制在 10 个以内这个算法在毫秒级就能跑完而且效果比纯贪心稳定得多。通勤时间的计算是另一个容易翻车的地方。最理想的方式是直接调地图服务商的路径规划接口拿真实驾车或公共交通耗时但接口有配额且按次数收费。我的方案是分层计算步行 15 分钟以内的距离用直线距离乘 1.3 系数估算超过这个范围调用地图批量路线接口并且把结果缓存到 Redis。这样既保证准确度又控制成本。3.3 可视化看板设计让用户看得懂也改得动可视化看板我做了三个核心模块。第一个是城市热度地图用 GeoHash 聚到的区域数据画在地图上变成热力图层颜色越暖代表该片区近期热度越高。第二个是路线总览地图上按时间顺序把景点用连线串起来点击每个节点可以看到景点名称、停留时间、推荐理由和天气提醒。第三个是每日行程时间轴左边是景点横向是时间一段一段的色块表示几点到几点在这做什么拖动色块可以调整顺序和时长。ECharts 实现上面几种图表都不难geo 坐标系下用 heatmap 做热力用 lines 系列做路线连线用 custom 系列做时间轴。真正花时间的不是画图而是接口返回的数据结构。前期我把每个景点的原始坐标和几百个点全量扔给前端结果地图渲染卡顿接口响应也慢。后来改成后端预聚合只输出前端需要的缩放层级数据并且加上缓存问题才解决。4. 实操过程与关键代码片段4.1 数据清洗与地理聚合这部分我直接放代码片段比文字描述更清楚。假设原始数据已经落到数据库我的做法是统一读成 Parquet 或 DataFrame再做清洗和聚合。import pandas as pd import geohash2 as gh df pd.read_parquet(poi_daily.parquet) # 清洗去掉坐标异常、开放时间为空的数据 df df[(df[lat].between(3, 54)) (df[lng].between(73, 136))] df df.dropna(subset[open_time, close_time]) # 地理聚合用 geohash 前6位把同一个片区内的 POI 合并 df[geo6] df.apply( lambda r: gh.encode(r[lat], r[lng], precision6), axis1 ) hot_raw df.groupby([geo6, poi_type]).agg( click_cnt(click_cnt, sum), comment_cnt(comment_cnt, sum), score(score, mean), avg_stay(avg_stay_minutes, median), ).reset_index() # 综合热度分点击、评论、评分的加权权重按业务需要调 hot_raw[hot_rank] ( hot_raw[click_cnt] * 0.6 hot_raw[comment_cnt] * 0.25 hot_raw[score] * 0.15 )这段代码里有几个细节值得注意。坐标过滤的范围只是为了避免脏数据不要把它当成严格的地域校验。GeoHash 的 precision 参数我建议先可视化几个样本看看效果6 位大约是一个街区级别如果发现一个片区跨了太多景点就升到 7 位让粒度更细。4.2 基于协同过滤的景点推荐用户对景点的真实态度我主要用行为事件来代理浏览算 1 分收藏算 3 分下单算 5 分。把这些事件聚合成 user × item 的隐式反馈矩阵后用矩阵分解做召回。下面是我用的一个简洁版本from scipy.sparse import csr_matrix from implicit.als import AlternatingLeastSquares import numpy as np # data 包含 user_id, poi_id, weight 三列 user_item csr_matrix( (data[weight], (data[user_id], data[poi_id])), shape(num_users, num_pois), ) # implicit 不同版本对矩阵行列的要求不完全一致 # 我这里统一转成 item × user避免训练时报错。 item_user user_item.T.tocsr() model AlternatingLeastSquares( factors64, regularization0.01, iterations15 ) model.fit(item_user)训练完之后我没有直接调 recommend 接口而是用模型产出的 item_factors 做相似推荐。这样可以给“收藏过某个景点的用户”一个可解释的理由def recommend_like(user_history, target_user, topn10): liked_items np.array(user_history[target_user]) if len(liked_items) 0: return [] # 用户向量近似为他收藏过的景点向量的平均 user_vec np.mean(model.item_factors[liked_items], axis0) scores model.item_factors user_vec ids np.argsort(scores)[-topn:][::-1] return ids这个方法的优点在于即使只收藏过一个景点也能立刻给出“喜欢这个景点的人通常也会喜欢下面这些”的推荐理由。缺点是后面逐步有了用户行为反馈后直接用模型返回的个性化得分会更准。4.3 行程路线生成与可视化接口路线生成的流程是用户传入城市和游玩天数先从候选集中选出各天可能去的景点再逐日求解路径。下面是一个单日规划的简化版本def plan_one_day(poi_list, start_pos, end_pos, start_time, max_time600): feasible [p for p in poi_list if p.open_time start_time p.avg_stay p.close_time] route, current, now [], start_pos, start_time while feasible: best None for p in feasible: travel get_travel_time(current, p.position) arrive now travel leave max(arrive, p.open_time) p.avg_stay if leave p.close_time or leave - start_time max_time: continue ratio p.score / (travel p.avg_stay) if best is None or ratio best[0]: best (ratio, p, leave) if best is None: break route.append(best[1]) current best[1].position now best[2] feasible.remove(best[1]) return route这段代码的贪心逻辑很直白每一步都选“单位时间收益最高”的景点。实际项目中我会再加一个 2-opt 环节把同一天内两两景点之间的顺序做局部交换减少总通勤时间。前端可视化接口我用 FastAPI 暴露返回结构包含每日的景点列表、坐标、时间预估、交通方式以及用于画热力图的城市区域热度app.get(/api/plan) def get_plan(city: str, days: int, pace: str balanced): candidates recommend_candidates(city, top_k50) plan build_multi_day_plan(candidates, days, pace) return { plan: plan, vis_data: build_vis_data(plan), }build_vis_data 会把每个景点的坐标、停留时间、当前天气、拥挤指数一起塞进返回体前端拿到直接渲染地图和时间轴。实践下来这个接口的响应时间主要卡在实时天气和路况查询上所以我给每个 city_id 和日期做了缓存过期时间设成 30 分钟能大幅减少对第三方接口的调用。5. 踩坑记录与排查技巧5.1 冷启动没有行为数据时怎么办冷启动问题在推荐系统里是老生常谈但在旅游场景下特别明显。新用户没有任何点击、收藏数据协同过滤根本算不出个性化结果。如果直接把协同过滤结果展示给他效果基本等同于随机推荐用户大概率会流失。我的处理方式是“规则 热榜 动态问卷”三层兜底。新用户进入系统后先让他选择旅行主题亲子、美食、文化、自然风光、购物以及每天能接受的移动时长。系统根据这些标签结合热榜热度分生成一个初始候选池。用户在候选池里的每一次点击都会被记入行为日志积累到一定数量后协同过滤才开始介入。另外对于那些刚上线、还没有任何行为数据的新景点我用描述文本算相似度比如“老城文化街区”和“历史博物馆”在语义上有一定相似性会更容易出现在相关推荐里。5.2 地理聚类“看起来近”不等于真的近这是我最开始忽视的一个坑。直接用经纬度欧氏距离判断两个景点是否“顺路”在大多数城市都会出错两个景点直线距离只有 500 米中间可能隔着一条河或一座山实际开车要绕 20 分钟反过来两个位于不同行政区的景点可能因为快速路直连通勤时间反而很短。后来我对所有候选景点对都先做一次“期望通勤时间”的计算而不是用坐标距离。短距离用步行系数估算中长距离用地图服务商的路线接口获取真实耗时并把结果缓存下来。这样在贪心路径优化时通勤时间这个核心参数才可信。要注意地图服务商接口的配额是按次数的如果一次缓存太久周末节假日路况变化大也会导致推荐路线和实际不符。我最后用的是分时缓存工作日缓存 12 小时节假日缓存 2 小时。5.3 可视化性能同样的数据换个方式展示差很多可视化性能问题主要出现在两个地方地图点数过多以及时间轴的 DOM 元素过多。地图热力层一开始直接把所有景点的经纬度返回前端ECharts 绘制时明显掉帧。改成后端按 GeoHash 聚合之后前端拿到的是区域级别数据比如“某片区热度值为 87”而不是几百个散点。视觉效果没有变差反而因为聚合更干净。时间轴的问题则是把每个景点和每个时间段都做成独立组件导致 DOM 数量爆炸。我用的是 ECharts custom 系列让它在 canvas 上绘制时间轴色块而不是生成几百个 HTML 节点渲染速度提升明显。5.4 常见问题速查表下面这组问题来自我在项目里实际遇到的情况整理成速查表方便你排查时快速定位。现象可能原因解决方法推荐结果和用户兴趣不搭行为权重设置不合理重新调节浏览、收藏、下单的权重必要时做线上 AB 对比行程总在同一个片区打转候选集中相似度太高加入城市分片区多样性限制每天最多从同一片区选两个景点地图路线重叠严重连续多天推荐了相似路线在跨天规划中加入“昨天已去片区”的惩罚项接口响应突然变慢第三方天气/路况接口超过配额增加本地缓存减少单用户请求频率夜间景点排进了白天行程没有做开放时间过滤在候选集阶段就按计划游玩时间过滤 open_time 与 close_time6. 复盘与经验哪些设计值得复用哪些下次一定改6.1 可解释性比精度更重要在这个项目里我体会最深的一点是旅游行程推荐不是一个“拼准确率”的问题而是一个“说服用户”的问题。用户看到一个陌生的景点第一反应是“你凭什么推荐这里”。如果推荐卡片上直接写“喜欢故宫的人也会喜欢这里”用户会觉得有说服力。但如果只给一个分数哪怕分数很高也很难让人信任。后续我在规划结果里加入了“顺路理由”和“时间理由”。比如“两个景点之间车程只有 15 分钟适合安排在上午”或者“这家老字号距离你要住的酒店步行 5 分钟适合作为晚餐选择”。这种可解释性设计虽然不直接提升算法指标但极大地提升了用户对结果的接受度。6.2 数据成本要在设计阶段就想清楚另一个教训是不要低估第三方数据成本。地图路线接口、天气接口、实时路况接口虽然单个次数看着不贵但一旦用户量上来接口费用会非常可观。我是在系统联调时才发现一次完整的行程规划可能触发近十次第三方请求。后来加了两层优化一层是距离矩阵缓存另一层是后端预聚合这才把成本压到可控范围。如果你的系统是上线给外部用户用建议在设计阶段就估算好接口调用量和预算而不是等到线上监控报警再处理。6.3 留给下一次迭代的三个改动这次项目还有一些明显没做好的地方。第一用户对行程的反馈没有得到充分利用。当前系统只能通过点击和收藏隐式判断喜好但用户删除某个景点可能是因为时间冲突也可能就是单纯不喜欢这个信号没有被区分。下一次我会在可视化时间轴上增加“不喜欢”按钮把显式负反馈纳入模型。第二多日行程之间的衔接还是偏弱。目前只是逐日约束求解没有充分考虑“前一天酒店位置”和“第二天出发地点”的关系。第三天气对行程的影响目前是硬过滤比如雨天屏蔽户外景点但现实中用户可能愿意冒小雨去拍照这需要一个更细粒度的偏好参数。这次项目里我最大的体会是旅游行程规划系统真正难的不是某一个算法而是把数据、推荐、路径约束和可视化全部串起来让用户感到“你懂我想要什么”。如果只做推荐不做可用的行程编排用户依然不知道怎么安排时间如果只做可视化没有推荐逻辑那又回到手工排行程的老路。作为开发者把这条链路理清楚比堆砌再多的模型和框架都重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑