资讯详情

资讯详情

基于高德地图的车辆通行时间预测:Python特征工程与LightGBM实战

1. 从一个被忽略的细节说起为什么通行时间预测值得单独做很多人做路径规划时习惯性地把两个路口之间的距离除以一个平均车速然后拍脑袋给出一个到达时间。我早期做物流调度系统时也这么干过结果被现实狠狠教育了一顿——早高峰从城东到城西同样的路线周一早上八点出发要 52 分钟周日早上八点出发只要 23 分钟。差了整整一倍多。用户看到预估时间不准第一反应不是路况复杂而是这系统不靠谱。这就是车辆通行时间预测这件事的核心价值它要解决的不是能不能到而是到底要多久到。在网约车派单、外卖配送时效承诺、物流干线调度、企业班车排班这些场景里通行时间预测的误差每降低 1 分钟背后可能就是真金白银的成本差异和用户体验差异。这篇内容我打算围绕基于高德地图车辆通行时间预测的 Python 实践来展开。需要先说明一点这里的高德地图指的是调用其开放平台提供的路径规划与路况能力作为数据来源我们做的是在拿到这些基础数据之后如何用 Python 构建一套可用的通行时间预测流程。整篇内容适合有一定 Python 基础、做过接口调用、想把这套东西真正落地到业务里的开发者也适合刚接触时空数据、想搞清楚预测时间到底怎么算的入门者。我会从数据获取、特征构造、模型选择、误差分析、工程化落地几个层面把我在实际项目里踩过的坑和总结的方法完整讲一遍。不堆砌公式重点讲清楚每一步为什么这么做以及不这么做会怎样。2. 数据从哪来路径规划接口的三种取数姿势与取舍2.1 距离、时长、路况三个字段的真实含义调用路径规划类接口时返回结果里通常有几个关键字段总距离、预估时长、以及分段的路况信息。很多人拿到duration字段就直接用了但这里有个容易忽略的点——接口返回的时长本身就是一个预估值它是平台基于当前和历史路况给出的并不是实测值。这意味着什么如果你直接拿接口返回的时长当作真值去训练模型那你训练出来的模型只是在拟合平台的算法而不是在拟合真实世界的通行规律。这个坑我在第一个版本里就踩了模型在验证集上 R² 高达 0.96上线后实际误差却很大因为验证集和训练集都来自同一个二手真值。正确的做法是接口数据作为特征输入真实通行时间作为标签。真实时间怎么来如果你有车队可以用车载 GPS 的轨迹数据如果是众包场景可以用司机端上报的实际到达时间。没有真实标签时退而求其次的做法是用接口返回的分段路况时长做加权组合但要清楚这只是权宜之计。2.2 分段路况数据的解析逻辑路径规划返回的往往是整条路线的聚合结果但真正有价值的是分段信息。一段路可能包含畅通、缓行、拥堵三种状态每种状态对应的实际速度差异巨大。我在解析时习惯把路线拆成若干路况段每段记录距离、路况等级、该段预估时长。这里有个实操细节不同接口对路况等级的划分粒度不一样有的是三档畅通/缓行/拥堵有的是四档甚至更多。解析时一定要先做一次等级归一化把不同来源的数据映射到统一的档位上否则后续特征会混乱。我一般用下面这个映射表原始等级归一化档位典型速度区间城市道路畅通/顺畅140-60 km/h缓行/较慢220-40 km/h拥堵/缓慢310-20 km/h严重拥堵40-10 km/h速度区间只是参考实际要以你所在城市的历史数据为准。一线城市和三四线城市的畅通标准完全不同这点后面讲特征工程时还会展开。2.3 批量取数时的频率控制与缓存策略做预测模型需要大量样本一条一条调接口肯定不现实。批量取数时有两个必须处理的问题调用频率限制和重复请求浪费。频率控制方面我的做法是用一个令牌桶做限流把 QPS 控制在配额的安全线以下比如配额是 10 QPS我实际控制在 6-7。为什么要留余量因为网络抖动和重试会吃掉一部分配额卡着上限跑很容易触发限流反而拖慢整体进度。缓存策略更关键。同一条路线在短时间内反复查询结果几乎一样完全没必要重复请求。我用的是路线指纹 时间窗口的缓存键把起点终点坐标做网格化处理比如精确到小数点后四位加上一个 5 分钟的时间桶作为缓存 key。这样既保证了时效性又能把重复请求挡掉一大半。实测下来缓存命中率能到 40% 左右取数时间直接砍掉近一半。import hashlib import time def make_cache_key(origin, dest, strategy0): # 坐标网格化降低精度避免缓存碎片化 o f{round(origin[0], 4)},{round(origin[1], 4)} d f{round(dest[0], 4)},{round(dest[1], 4)} # 5分钟时间桶 bucket int(time.time() // 300) raw f{o}|{d}|{strategy}|{bucket} return hashlib.md5(raw.encode()).hexdigest()注意坐标网格化会带来精度损失如果你的场景对起终点精度要求很高比如门到门配送网格粒度要调细或者干脆不做网格化改用其他去重手段。3. 特征工程才是胜负手把路况翻译成模型能懂的语言3.1 时间特征别只用小时周期编码更靠谱最直觉的特征就是出发时间的小时数但直接用 0-23 的整数有个致命问题23 点和 0 点在数值上差了 23但在现实里只差 1 小时。模型会误以为这两个时间点差异巨大。解决办法是周期编码把时间映射到单位圆上import numpy as np def encode_cyclical(value, period): sin_val np.sin(2 * np.pi * value / period) cos_val np.cos(2 * np.pi * value / period) return sin_val, cos_val # 小时编码周期24 hour_sin, hour_cos encode_cyclical(hour, 24) # 星期编码周期7 dow_sin, dow_cos encode_cyclical(day_of_week, 7)这样 23 点和 0 点在编码空间里就非常接近了。我实测过加上周期编码后模型在早晚高峰时段的预测误差能降低 8%-12%效果相当明显。除了小时和星期还要区分工作日/周末/节假日。这三类的通行规律差异极大。节假日的处理尤其麻烦因为每年的具体日期都在变我的做法是维护一张节假日表同时加一个节前/节中/节后的标记因为节前一天的下班高峰往往比平时更堵。3.2 路况特征拥堵比例比平均速度更有信息量前面提到把路线拆成路况段那么聚合特征怎么构造很多人第一反应是算平均速度但平均速度会掩盖分布信息。举个例子一条路一半畅通一半严重拥堵另一条路全程缓行两者的平均速度可能差不多但实际通行体验和预测难度完全不同。我更倾向于用拥堵里程占比这类分布特征畅通段里程占比缓行段里程占比拥堵段里程占比最长连续拥堵段长度路况切换次数反映路况的碎片化程度其中最长连续拥堵段长度是我发现的一个高价值特征。一段 2 公里的连续拥堵和分散在 10 公里里的 5 段 400 米拥堵虽然总拥堵里程一样但前者的通行时间往往更长因为连续拥堵意味着你很难找到机会提速。3.3 道路属性特征红绿灯数量是被低估的变量道路本身的属性对通行时间影响巨大但很多人在做特征时只用了距离。我总结下来以下几类属性值得纳入特征类别具体特征为什么重要道路等级高速/主干道/次干道/支路占比不同等级的设计速度差异大交叉口红绿灯数量、路口数量每个红绿灯平均消耗 30-90 秒转向左转次数、掉头次数左转和掉头等待时间远高于直行道路形态弯道数量、坡度影响实际行驶速度红绿灯数量这个特征特别值得说。我做过一个对比实验在特征里加入红绿灯数量后短途路线5 公里以内的预测误差降低了约 15%。原因很简单短途路线上红绿灯的等待时间占比很高忽略它误差自然大。红绿灯数据从哪来路径规划接口一般会返回途经路口信息如果没有可以用路网数据自己算或者用历史轨迹反推在某个位置速度频繁降到 0 附近大概率是红绿灯。3.4 历史统计特征目标编码的正确打开方式对于起点区域终点区域这类类别特征直接做独热编码维度太高用目标编码Target Encoding更合适。但目标编码有个大坑数据泄漏。如果你用全量数据算均值再编码训练集里就混入了标签信息模型会过拟合。正确做法是用 K 折交叉的方式计算编码值from sklearn.model_selection import KFold def target_encode(df, col, target, n_splits5, smoothing10): df[f{col}_encoded] 0.0 kf KFold(n_splitsn_splits, shuffleTrue, random_state42) global_mean df[target].mean() for train_idx, val_idx in kf.split(df): train_fold df.iloc[train_idx] stats train_fold.groupby(col)[target].agg([mean, count]) # 平滑处理避免小样本类别编码值极端 stats[smooth] (stats[mean] * stats[count] global_mean * smoothing) / (stats[count] smoothing) df.loc[df.index[val_idx], f{col}_encoded] df.iloc[val_idx][col].map(stats[smooth]).fillna(global_mean) return df平滑处理这一步很关键。某个区域如果只有 2 条样本均值可能非常极端加平滑后会被拉向全局均值更稳健。4. 模型选型从线性回归到梯度提升我为什么最终选了这套组合4.1 先跑通基线线性回归不是用来上线的做任何预测任务我都会先跑一个最简单的基线模型通常就是线性回归。它的作用不是上线而是给你一个误差的下限参考。如果复杂模型的误差比线性回归还高那说明特征工程或者数据处理出了问题而不是模型不够强。线性回归在这个任务上的表现如何我实测下来用距离、平均速度、小时数这几个基础特征MAE平均绝对误差大概在 6-8 分钟。这个数字看着还行但它的误差分布很不均匀——平峰时段误差 2-3 分钟高峰时段能到 15 分钟以上。原因很简单通行时间和特征之间是非线性关系线性模型拟合不了。4.2 梯度提升树当前性价比最高的选择在结构化特征数值、类别混合的回归任务上梯度提升树类模型XGBoost、LightGBM、CatBoost几乎是默认答案。我最终选的是LightGBM理由有三第一训练速度快。同样的数据量LightGBM 比 XGBoost 快 2-3 倍做特征迭代时这个优势太重要了你一天能试的模型数量直接翻倍。第二对类别特征友好。LightGBM 原生支持类别特征不需要做独热编码省事且效果好。第三内存占用低。直方图算法让它在百万级样本上也能跑得动普通开发机就能训练。核心参数我一般这么设import lightgbm as lgb params { objective: regression_l1, # MAE损失对异常值更鲁棒 metric: mae, learning_rate: 0.05, num_leaves: 63, max_depth: -1, min_data_in_leaf: 50, # 防止过拟合 feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, lambda_l2: 1.0, verbose: -1 }这里重点说两个参数。objective用regression_l1而不是默认的regressionL2 损失是因为通行时间数据里异常值不少比如偶发的交通事故导致某条样本时间特别长L1 损失对异常值更鲁棒。min_data_in_leaf设 50 是为了防止模型记住噪声这个值要根据你的数据量调整数据越多可以设得越大。4.3 为什么没有上深度学习经常有人问通行时间预测能不能用 LSTM、Transformer 这类时序模型。我的看法是可以但要看你的数据形态。如果你的数据是单车的连续轨迹序列那时序模型确实有优势因为它能捕捉速度的连续变化。但如果你的数据是**起点-终点-时间这样的离散样本**那梯度提升树在性价比上完胜。深度学习需要大量数据才能发挥优势而很多业务场景下你能拿到的标注样本可能就几万条这个量级上树模型更稳。我做过对比实验在 5 万条样本上调参后的 LightGBM 和 LSTM 的 MAE 差距在 0.3 分钟以内但 LSTM 的训练时间是 LightGBM 的 20 倍以上而且调参难度大得多。所以除非你有明确的序列建模需求否则没必要上深度学习。4.4 模型融合简单平均就够了如果想再压一点误差可以试试模型融合。我的做法是训练三个模型LightGBM、XGBoost、CatBoost然后对预测结果做加权平均。权重不用太讲究简单的等权平均往往就能带来 2%-3% 的提升。但要注意融合的前提是模型之间要有差异性。如果三个模型的特征和参数都差不多融合收益几乎为零。我一般会让它们用不同的特征子集或不同的损失函数保证多样性。5. 误差分析预测不准的时候到底该查什么5.1 先看误差分布别只看平均值模型上线前我一定会做一次完整的误差分析。第一步不是看 MAE 这个总指标而是看误差的分布。把预测误差画成直方图你会发现很多信息。我遇到过一个典型情况整体 MAE 是 4.2 分钟看着不错但直方图显示有一小撮样本误差超过 20 分钟。把这批样本单独拎出来看发现全是跨江/跨桥路线。原因也清楚——桥梁是交通瓶颈一旦拥堵通行时间会非线性暴涨而模型没见过足够多的极端样本学不会这种突变。这个发现直接指导了后续优化针对跨江路线单独加特征比如桥梁的实时拥堵指数并对这类样本做加权训练。5.2 分时段、分距离看误差整体指标会掩盖很多问题必须做分组分析。我习惯按两个维度切分组维度分组方式常见问题时段早高峰/平峰/晚高峰/夜间高峰时段误差普遍偏大距离短途(5km)/中途(5-20km)/长途(20km)短途受红绿灯影响大长途受路况变化影响大分完组你会发现误差往往集中在某几个格子。比如短途 早高峰这个组合误差可能是整体均值的两倍。针对性地对这个格子补充特征或单独建模比盲目调参有效得多。5.3 特征重要性别迷信但要会看LightGBM 能输出特征重要性这个一定要看但不能全信。重要性高说明这个特征对模型有用但重要性低不代表这个特征没用——可能是它和另一个特征高度相关被抢了重要性。我一般会结合两种重要性来看分裂次数重要性特征被用来分裂的次数和增益重要性特征带来的损失下降。如果某个特征分裂次数多但增益低说明它被频繁使用但贡献有限反之则说明它虽然用得少但每次都很关键。有一次我发现距离这个特征的重要性异常低排查后发现是因为我同时放了距离和预估时长而预估时长本身就包含了距离信息两者高度相关。去掉预估时长后距离的重要性立刻上来了。这就是特征相关性的典型影响。6. 工程化落地从 Notebook 到线上服务要跨过的坎6.1 训练和推理的特征必须一致这是最容易出事的地方。Notebook 里做特征工程时你可能用了一些全局统计量比如全量数据的均值但线上推理时你只能拿到当前这一条请求的数据算不出全局统计量。解决办法是把统计量固化下来。训练时把需要的均值、分位数等统计量存成文件线上加载后直接用。所有特征的计算逻辑要封装成统一的函数训练和推理调用同一个函数避免两份代码算出两种特征的悲剧。import joblib # 训练时保存统计量 stats { global_mean_duration: df[duration].mean(), region_mean: df.groupby(region)[duration].mean().to_dict(), } joblib.dump(stats, feature_stats.pkl) # 推理时加载 stats joblib.load(feature_stats.pkl)6.2 模型版本管理与灰度发布模型不是训练一次就完事的路况在变、城市在变、用户行为在变模型需要持续迭代。我建议至少做到两点一是模型版本管理。每次训练的模型、对应的特征版本、训练数据的时间范围都要记录清楚。出问题时能快速定位是哪个版本引入的。二是灰度发布。新模型不要一次性全量替换先放 10% 的流量跑一两天对比新旧模型的线上指标。线上指标和离线指标往往有差距灰度能帮你提前发现问题。6.3 监控预测偏差要能自动报警线上服务跑起来后必须监控预测偏差。做法是当实际通行时间回传后计算预测值和实际值的偏差按小时聚合。如果某个小时的偏差突然超过阈值就触发报警。我踩过一个坑有段时间模型预测普遍偏短排查后发现是城市新开通了一条地铁导致部分路段车流量下降通行时间缩短了但模型还停留在旧的路况认知上。这种变化靠人工发现很慢有了偏差监控就能第一时间感知。7. 几个让我印象深刻的实操教训7.1 别用未来数据训练过去做时间序列相关的任务数据切分一定要按时间切不能随机切。我早期图省事用了随机切分结果模型偷看了未来的数据离线指标虚高上线后原形毕露。正确做法是按时间划分训练集和验证集比如用前 80% 的时间段做训练后 20% 做验证。如果要做交叉验证也要用时间序列交叉验证TimeSeriesSplit保证验证集永远在训练集之后。7.2 极端天气要单独处理雨雪天气的通行时间和正常天气完全不是一个量级。如果训练数据里极端天气样本很少模型遇到这种天气就会严重低估。我的处理方式是把天气作为一个特征加进去同时对极端天气样本做过采样让模型见过足够多的坏天气。天气数据可以从公开的气象接口获取按小时对齐到你的样本上。这个特征加上后雨雪天的预测误差降低了 20% 以上。7.3 短途预测的精度天花板做了这么多优化我得承认一个现实短途预测3 公里以内的精度是有天花板的。因为短途路线上一个红绿灯的等待时间波动就能占到总时间的 20%-30%而这种波动本质上是随机的你无法预测到路口时刚好是红灯还是绿灯。对于短途场景与其死磕精度不如给一个时间区间而不是一个点估计。比如预测15-22 分钟用户对区间的接受度反而更高。这也是很多成熟产品采用区间预估的原因。7.4 特征不是越多越好我一度陷入特征越多越好的误区往模型里塞了上百个特征结果训练变慢、过拟合加重效果反而下降。后来做了一轮特征筛选砍掉一半特征模型效果基本持平训练时间减半。筛选方法我用的是基于重要性的递归消除先训练一版去掉重要性最低的 10% 特征再训练如此循环直到效果开始下降为止。这个过程虽然繁琐但能帮你真正理解哪些特征在起作用。8. 写在最后的一点个人体会这套通行时间预测的流程我从最初拍脑袋估时间到后来搭起完整的特征工程和模型体系前后迭代了十几个版本。最大的感受是模型本身从来不是难点难的是对业务场景的理解和对数据的敬畏。同样一个 LightGBM你放进去的特征能不能反映真实的通行规律决定了它是 4 分钟误差还是 10 分钟误差。红绿灯数量、拥堵分布、天气、节假日这些看似琐碎的细节才是把预测做准的关键。工具和算法都是现成的真正拉开差距的是你愿不愿意蹲下来把每一条路、每一个时段的数据看透。如果你正准备做类似的项目我的建议是先用最简单的特征跑通全流程拿到一个基线然后再一个特征一个特征地加每加一个都做一次误差分析。这个过程慢但每一步都扎实最后得到的模型你才敢真正放到线上用。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →