预测骑手下一步:智慧物流中的行为序列建模与特征工程实践
简介行为序列预测是时序建模与特征工程交叉的重要课题其核心在于从离散事件流中捕捉状态转移规律。在即时配送场景下骑手的操作日志天然构成高噪声行为序列而准确的下一步动作预判直接影响路径规划、订单时效评估与异常调度决策。围绕行为分类、时空特征、统计特征与序列模型融合构建稳健的预测流水线成为智慧物流降本增效的关键一环。本文基于一次实战竞赛系统梳理从数据清洗、标签构造到LightGBM与LSTM融合的完整方法论为从事调度系统、ETA预测与运力优化的工程师提供参考。 2020年初那场疫情让全城的毛细血管都停摆过但外卖骑手反而是少数还在路上跑的人。当时饿了么搞了一场骑士行为预估比赛第一轮的核心赛题就一句话根据骑手的历史行为和当前状态预测他下一步会干什么。听起来像是一个纯粹的算法题但做过即时配送的人都知道这背后牵扯的其实是整个智慧物流调度系统最底层、最头疼的一环——运力行为的不确定性。我当时正好参与了这场比赛整个过程中踩了不少坑也积累了很多第一手经验。这篇文章不打算复述赛题本身而是想从一个参赛者和从业者的角度把我对这道题的理解、数据怎么拆、特征怎么做、模型怎么迭代、以及最容易被忽略的那些细节完整地梳理一遍。不管你是打算参加类似算法竞赛还是正在做物流调度、路径规划、ETA预测这类业务这篇文章应该都能给你一些参考。1. 赛题拆解为什么“预测骑手下一步”是智慧物流的硬骨头1.1 疫情场景下的即时配送到底特殊在哪先说理解题目这件事。很多人一看到“预测骑手下一步行动”第一反应是这不就是个行为分类问题吗给个多分类模型就可以了。但如果真按这个思路做下去后面大概率会翻车。因为疫情这个背景不是白给的它把整个即时配送的场景逻辑彻底改变了一遍。正常时期骑手的配送行为相对规律接单、到店、取餐、送餐然后循环。但在疫情管控期间很多小区的出入口被限制骑手可能到了小区门口却进不去只能在门口等用户下来取或者放到指定货架。这一等可能就是十分钟、二十分钟甚至会因为用户电话打不通直接报异常。换句话说骑手的“行为序列”里会出现大量正常时期很少见的片段长时间停留、反复联系用户、取消订单、上报异常等等。这就意味着如果我们只盯着“下一步是取餐还是送餐”这种粗粒度动作很多真正影响配送效率的环节会被漏掉。尤其当预测的时间窗口拉长到未来30分钟、60分钟时骑手是否会在某个节点停滞直接决定了这个订单能不能按时送达。所以第一轮比赛把“预测下一步行动”作为题目本质上是在考核参赛者对配送环节的颗粒度理解而不只是模型精度。1.2 “下一步行动”到底指什么从数据层面来看骑手的行为通常会被记录成一条一条的操作日志比如“到达门店”“完成取餐”“点击已送达”“上报异常”“取消订单”等等。而“下一步行动”这个目标就是说要预测骑手接下来最可能产生哪一类操作。但这个目标定义起来有个细节需要注意不同系统对行为的记录粒度不一样。有些平台会把“取餐成功”和“取餐失败”分成两条记录有些则只有一条“取餐”记录然后通过后续订单状态来反推是否成功。我参赛时拿到的数据里行为标签是通过订单状态流转推导出来的也就是说我们需要先做一遍数据清洗把原始的日志流转换成一条条可标注的行为序列然后再定义预测目标。我当时的做法是把行为粗分为五个大类接单、到店、取餐、送达、其他包括上报异常、取消、转单等。其中“其他”在正常时期占比很低但在疫情期间明显上升所以这个类别的建模价值其实很高。如果只看前三类行为模型很难区分一个骑手是正常取餐还是在门口等待反复联系用户而这类状态恰恰是调度系统最关心的。1.3 赛题背后的业务价值预测是调度系统的前置条件如果说比赛只是比一个准确率那理解赛题背后的业务动机就显得没那么重要。但实际情况是这一题的最终目标并不是让模型告诉你“骑手下一步在干什么”而是要让调度系统能够提前判断“这单接下来会不会出问题”。在即时配送的调度体系里最理想的状态是系统给骑手派单后能够准确预判骑手接下来的动作序列比如先取哪几单、走哪条路线、大概什么时间能送到。这样系统才能算出一个可靠的时间窗并在骑手遇到异常时提前介入调度而不是等用户催单了才发现已经超时。所以这个赛题虽然叫“预测骑手下一步行动”但我更愿意把它理解成“构建一个骑手行为的时序预测模型为上层调度提供决策依据”。有了这个视角你会发现特征工程的侧重点会不一样不只是关注单个动作的准确性还要关注动作之间的转移关系、持续时长、以及异常中断的可能性。2. 数据剖析拿到赛题数据之后的第一个下午2.1 数据都长什么样从原始表到可用表我记得比赛提供的数据主要包含三类信息订单数据、骑手操作日志、以及骑手和商家的位置/轨迹数据。第一眼看上去订单表里每个订单有订单号、骑手ID、商家ID、用户ID、创建时间、订单状态、取消时间等字段操作日志表里则有骑手ID、操作类型、操作时间、经纬度商家和用户也有对应的坐标信息。但这些表并不能直接用来训练模型最关键的问题是操作日志是事件流不是样本。每一行只是“某个骑手在某个时间点做了一件什么事”但模型需要的是“在当前时刻结合历史预测下一步操作”的样本结构。所以我做的第一件事是把操作日志按骑手和时间排序然后对每个操作事件生成一条样本。每个样本的特征包括当前操作类型、距离上一次操作的时间间隔、当前订单的已耗时、当前骑手过去30分钟内的操作序列统计、当前骑手所处位置到商家/用户的距离以及当前时刻的时间特征小时、星期、是否节假日等。而标签则是这条操作之后的下一个操作类型。这个过程说白了就是要把“事件流”转换成“样本表”是整场比赛里最花时间但也是最有价值的一步。2.2 标签构造把“下一步”变成一个监督学习目标标签构造看似简单其实有很多细节。比如如果一个骑手在晚上10点之后没有下一步操作了那这条样本的标签是什么我当时采用了两种方案一种是把操作日志切到骑手当天最后一次操作也就是说最后一条不产生训练样本另一种是用一个专门类别“结束当天”来标记。在一开始我用的是第一种方案简单直接但后来发现这会让模型在夜间时段出现明显偏差因为夜间停止接单是一个很重要的行为模式。所以后来我改成第二种方案多设置一个“结束”类别。这个类别的出现频率其实不低尤其是晚上九点之后很多骑手会直接下线如果模型不知道有这个可能性它就会在晚间时段强行预测成“送达”或“返程”这显然不对。另外还有一个重要问题就是标签预测的时序窗口。题目要求“预测下一步”意味着我们只关心下一次操作是什么而不关心之后几步。但在实际训练里如果把每一步操作之间的时间间隔作为特征那特征里其实已经包含了大量跟标签相关的信息容易造成数据泄漏。所以在构造特征时我特意避开了“当前操作与下一步操作之间的时间间隔”这一类信息只用“当前操作与上一步操作之间已经过去的时间”这样模型不会直接偷看到答案。2.3 时空特征位置信息怎么用最有效骑手行为和普通用户行为最大的区别在于它高度依赖空间位置。一个骑手到了商家门口下一步大概率是“取餐”到了小区门口下一步大概率是“送达”。所以位置特征非常关键。但位置特征不能直接扔经纬度给模型那样模型学不到什么反而容易过拟合到特定坐标点上。我的做法是把位置特征转化成“业务语义距离”当前骑手位置到订单商家的距离、到目的地的距离、当前最近的商家集聚区距离、以及当前所在区域在最近30分钟内的订单密度。这里有一个很实用的技巧用分钟级历史轨迹去计算“骑手在这个位置的停留时长”。如果一个骑手在某个坐标点附近停留了超过5分钟那这个位置很可能是商家、小区入口或者写字楼如果停留超过15分钟那大概率是在等餐或者联系用户。这些停留特征对预测“下一步是取餐还是报异常”非常有帮助。我当时简单统计过加了停留时长特征之后分类模型在“取餐”和“其他”之间的混淆率下降了大约8%。这说明骑手的行为不是随机的而是被空间场景和业务阶段强烈约束的。2.4 疫情数据特有的干扰噪声再提醒一下所有比赛数据处理环节里最脏的部分往往不是缺值而是噪声。疫情数据里有一个很大的噪声来源由于封控政策不断变化很多商家的营业状态会在一天之内多次切换导致订单被频繁取消而骑手这边也经常会出现到了门口才发现商家没开门的状况。这种噪声直接体现为同一时段的订单取消率、异常上报率会突然飙升。如果在特征构造时不加任何处理模型很容易把这些异常当成正常模式导致预测结果突变。我当时做了一层平滑处理对订单取消率、平均配送时长这些统计特征使用过去3天同时间段的中位数而不是当天的原始值。这样既能保留时段性特征又避免被单日突发事件干扰。3. 特征工程与模型选型从GBDT到序列模型的挣扎3.1 先上LightGBM最快建立baseline的路径当时团队成员对深度学习模型有不同偏好但最后我们还是决定先用LightGBM打底。原因很简单特征以表格型为主有大量类别特征和数值特征而且样本量在百万级LightGBM在这类问题上的性价比最高。第一天我们就用基础特征跑了个多分类模型测试集准确率大概在0.63左右。这个数字其实不高因为我们只用了最简单的操作类型、时间、距离、频率等特征。这个baseline的意义在于可以快速验证数据管道是否打通标签构造是否合理以及后续加特征能不能带来稳定提升。我强烈建议任何参加类似比赛的团队不要一上来就搞大模型先用一个简单的树模型把流水线走通然后再逐步优化。3.2 特征工程的几个重心在baseline基础上我分了几个方向同时做特征工程。第一个方向是序列统计特征。骑手过去10步操作里各个操作类型出现的次数、最近一次同类操作的间隔、操作类型的转移次数等。这类特征能够捕捉“有些骑手习惯先取多单再送有些则是一单一送”的偏好差异。第二个方向是时间节奏特征。比如当前订单已经等了多久、当前操作和上一操作之间的间隔、过去1小时内骑手完成订单的个数。这些特征在区分“正常配送节奏”和“异常停滞”时非常有效。第三个方向是上下文特征。包括当前时间在当天中的位置、是工作日还是周末、是否处于饭点高峰、所属商圈的历史下单量等。这类特征弥补了单个骑手行为数据的冷启动问题尤其是在预测新骑手时特别有用。加完这些特征后LightGBM在测试集上的准确率提升到了0.71左右。这个过程让我确认了一个判断这类比赛最终比的就是特征工程。模型本身只要不是太落后差别不会太大。3.3 尝试序列模型LightGBM解决不了的时间依赖准确率到0.71之后提升开始变慢。我们发现LightGBM在处理长序列依赖上明显力不从心尤其是骑手在“接单—到店—取餐—送餐”之间发生长时间停留时GBDT很难把这种跨步骤的关联关系建模出来。所以我们尝试了序列模型。最开始用LSTM把每个骑手过去20步操作编码成embedding序列再加上时间间隔和位置信息最后接一个softmax输出多分类预测。训练时用到了大约200万条样本跑在单卡V100上一个epoch大约要12分钟。LSTM的效果确实有提升测试集准确率到了0.74。但跟训练成本比这个提升幅度其实不划算。而且LSTM对特征工程的依赖并没有减少——它只是额外捕捉了一些连续操作之间的时序模式但并不能替代那些精心设计的业务特征。后来我们又试了Transformer结构但效果没有比LSTM强多少训练却慢得多。所以在那个赛题上最终的方案其实是LightGBM和LSTM的结果做加权融合而不是单纯依赖某一个模型。3.4 模型融合的细节加权融合和Stacking模型融合在这个比赛里是必要的。我们用的是最直接的软投票加权融合给LightGBM和LSTM分配了0.6和0.4的权重。这个权重不是拍脑袋定的而是通过网格搜索在验证集上试出来的。之所以LightGBM占的比重更大是因为它在特征依赖上更稳健对冷启动场景的适应性也更好。我们也试过Stacking用LightGBM和LSTM的输出作为二级模型的特征再加一些原始特征进去。但从结果看Stacking的提升只有0.005左右而工程复杂度却增加了很多尤其是需要处理5折交叉验证的时间泄漏问题相当麻烦。如果只是为了比赛名次可以把这块做得更精致一些但如果是为了真正落地到配送系统里我会更倾向于用简单可靠的加权融合。4. 实操总结完整的训练流程和代码要点4.1 数据预处理与训练集构建我在这里贴一段核心的训练数据构建代码方便大家参考整体逻辑。所有代码都基于pandas和numpy模型部分用的是LightGBM的sklearn接口。import pandas as pd import numpy as np # 读取原始操作日志 logs pd.read_csv(rider_action_log.csv, parse_dates[action_time]) logs logs.sort_values([rider_id, action_time]).reset_index(dropTrue) # 生成样本每个操作事件作为一条样本 # 特征当前操作类型、时间特征、最近10步操作统计、当前订单状态、位置特征 features [] labels [] for rider_id, group in logs.groupby(rider_id): group group.reset_index(dropTrue) for i in range(len(group) - 1): cur group.iloc[i] nxt group.iloc[i 1] # 基础特征 feat { rider_id: rider_id, action_time_weekday: cur[action_time].weekday(), action_time_hour: cur[action_time].hour, action_type: cur[action_type], order_status: cur[order_status], gap_to_last_action: cur[gap_to_last_action], order_wait_time: cur[order_wait_time], distance_to_merchant: cur[distance_to_merchant], distance_to_user: cur[distance_to_user], } # 最近10步操作统计 hist_start max(0, i - 10) hist group.iloc[hist_start:i] feat[hist_action_type_count] hist[action_type].value_counts().to_dict() feat[hist_action_mean_gap] hist[gap_to_last_action].mean() feat[hist_action_std_gap] hist[gap_to_last_action].std() features.append(feat) labels.append(nxt[action_type]) df_train pd.DataFrame(features) df_train[label] labels这段代码的思想非常简单对每个骑手遍历他的操作序列把第i步作为当前状态第i1步作为预测目标。然后把当前状态的信息转成特征向量。如果你的数据量非常大可以不用groupby循环的方式改用向量化的shift操作来生成样本但那样代码的可读性会差很多而且容易在边界条件上出错。比赛阶段我更推荐循环方式先把逻辑跑通。4.2 类别不平衡处理在构造完训练集后我遇到的第一个问题就是类别不平衡。正常时期“取餐”和“送达”这两个类别的样本量占了绝大多数而“上报异常”“取消订单”这些类别的样本量非常少。但在疫情期间这些稀有类别的业务价值恰恰很高因为调度系统最关心的就是在这些异常发生之前有所预警。处理类别不平衡我用了两个手段第一个是训练时给LightGBM传入类别权重稀有类别的权重是常见类别的5倍左右第二个是在模型预测后对稀有类别的概率做一个小幅提升大概乘一个1.2的系数然后再做argmax。这两个手段结合能让“上报异常”和“取消订单”的召回率从不到0.2提升到0.4左右而且对整体准确率的影响只有0.01左右。4.3 线上评测与本地验证的差异处理很多参赛者会遇到一个很头疼的问题本地验证集上的分数很高但提交到线上后分数明显下降。这种差异通常有两个原因一是数据分布不一致线上的数据可能来自未来时间段和训练集的时间分布不同二是特征构造里存在时间泄漏。为了尽量贴近线上评测一定要采用时间序列验证训练集用前80%的时间段验证集用后20%的时间段中间留出几天的gap作为缓冲。这样才能模拟“用历史预测未来”的真实场景。我踩过这个坑起初我用的是随机拆分结果本地AUC高了0.05但提交线上后立刻被打回原形。5. 常见问题与排查技巧实录5.1 冷启动骑手怎么预测比赛期间另一个比较头疼的问题是有些骑手是新人历史操作很少。如果是LightGBM一旦某个骑手ID属于“未见过的ID”模型就只能靠全局统计特征来预测效果会大打折扣。解决思路有两个层面一是多构造一些基于全局统计的特征比如“该骑手所在区域的平均配送时长”“该骑手常接的商家类型”这样即使没有个体历史数据也能给出相对合理的预测二是在训练时对骑手的历史步数做截断比如随机只保留最近20步让模型适应“信息不完整”的情况。5.2 类别混淆最严重的地方我统计过验证集上的混淆矩阵发现两个最容易混淆的地方一是“到店”和“取餐”之间二是“送达”和“取消订单”之间。第一种混淆是合理的因为到店之后往往就会取餐动作界限本来就模糊第二种混淆则说明模型抓取“取消意愿”的线索还是不够。针对第一种混淆我在特征里加了“当前订单剩余里程数”因为如果骑手距离商家还很远那“到店”的概率就低“取餐”的概率更低。针对第二种混淆我加了一个“骑手最近一次联系用户的时间距现在多久”的特征因为报异常或者取消前骑手通常都会先联系用户这个特征能很好地捕捉到取消前的状态变化。5.3 疫情特殊时段怎么处理最后再说一句关于疫情数据的切身体会。疫情的影响不是均匀分布的它更像是一波一波的脉冲信号某个区突然封控、某条街突然关闭、某个商圈的订单量突然减半这些事件是模型无法从历史数据里预测到的。所以无论模型做得多好在实际业务中都需要一个实时规则层或人工干预机制来兜住模型的盲区。在比赛里我们能做的只是在特征工程阶段尽量保留那些和“异常波动”有关的信号比如近15分钟内订单取消率、商家营业状态突变次数等让模型至少能对“风向变化”有所感知。至于这种感知够不够用说实话永远不够用但这已经是数据驱动方法在当前阶段能触碰到的天花板了。6. 最后想分享的一点个人心得如果你从头到尾看到这里应该能感觉到这场比赛的难点不在某个单一模型有多强而在于把业务问题翻译成数据问题的过程。每一次特征构造、每一个标签设定、每一步验证策略背后其实都对应着对即时配送业务的理解。那些跑得高的方案通常不是用了多么复杂的网络结构而是在细节上做得更扎实多算了一个停留时长、多平滑了一天波动、多留了一步时间gap。如果后续有人想再深入这个方向我的建议是不要只停留在分类准确率上可以试着把预测结果接到一个简单的仿真调度环境里看看预测误差对骑手路径规划和订单时效的影响。那才是“预测骑手行为”真正发挥价值的地方。毕竟在智慧物流这个领域模型的终极目标不是打败竞赛榜单而是让系统在真实世界里做决定时少一点盲目多一点从容。本文还有配套的精品资源点击获取