共享单车潮汐淤积这个事,做运筹优化的人应该都不陌生:早高峰地铁口堆成车海,晚高峰小区门口一辆难求。传统再平衡靠调度卡车半夜拉车,成本高、时效差、还堵路。后来有人想用动态定价引导用户“顺手还车”,把闲置车辆骑到缺车点,但价格调多少、跟车辆调度怎么配合,一直是个头疼的耦合问题。
最近我在折腾一个叫DSDRO的模型,全称是Distributionally Robust Dynamic Pricing and Vehicle Routing Optimization,把动态定价分配和车辆路由放进同一个目标函数里联合求解,用分布鲁棒优化处理需求不确定性,Matlab实现整套流程。这篇文章把我的建模思路、代码结构和踩坑记录完整写一遍,给正在做共享出行调度、城市物流再平衡或者运筹优化方向的朋友一个可直接照抄的参考框架。
1. 共享单车再平衡为什么这么难
1.1 表面是车不够,实际是车在错误的时间出现在错误的地点
共享单车系统的核心矛盾不是总量不足,而是时空分布失衡。用户骑行的潮汐特性极强:早高峰从居住区流向办公区,晚高峰反向流动,周末又往商圈和公园聚集。这种单向流动直接导致“淤积站点”和“枯竭站点”并存,早上九点写字楼门口车满为患,晚上九点同一批车全堵在地铁站。
这里有一个关键指标叫站点失衡度,定义为站点实际车辆数偏离其最优库存水平的绝对值之和。再平衡问题本质上就是最小化这个失衡度。传统做法是安排调度卡车按固定线路搬运,但一来卡车调度本身成本高,二来共享单车是“最后几百米”的出行工具,站点密集、道路狭窄,卡车大规模进场反而制造拥堵。
所以业界很早就转向一个聪明玩法:既然用户本来就在骑车,能不能用价格杠杆让部分用户“主动”帮运营商完成再平衡?比如某个站点缺车,我就给从这里骑走的用户打折,同时给骑到这里的用户发优惠券。这就是动态定价再平衡的基本思路。
1.2 动态定价和车辆路由是两套决策系统,耦合极深
动态定价改变的是用户行为,进而改变未来一小时站点的车辆流入流出;车辆路由改变的是物理调拨,直接修正站点库存。两个系统之间有一条隐含的反馈链:定价引导用户多骑,会导致途经路段的骑行流变化,调度车规划路线时如果忽略这个变化,可能跑到半路发现原本缺车的站点已经被用户自然填满了,或者原本预测的富余站点因为降价促销被掏空。
我见过很多团队把两个问题拆开做:先用一个定价模型预测用户响应,再把预测结果喂给路径优化模型。这么做的好处是模块清晰、好调试,坏处是预测误差会逐级放大。定价模型说某个站点缺20辆车,路径模型就派车去送,结果中间一小时用户响应比预期强,站点自己不缺了,这趟调度就白跑;反过来,定价模型说富余站点会流出30辆,路径模型只去拉走了15辆,剩下的15辆就成了下一周期的淤积源。
DSDRO的核心动机就是打破这种逐级传递的误差。它不把定价和路由看成“先后决策”,而是一个决策问题的两个维度:目标函数同时累计用户收益和调度成本,约束条件同时满足库存守恒和路由可行性。一阶最优解找出来以后,既告诉你每类用户该收多少钱,也告诉你调度车该走哪条路,二者逻辑自洽。
1.3 需求不确定性才是真正难啃的骨头
共享单车的需求很少是稳定已知的。天气、临时活动、地铁故障、甚至短视频上一条网红店打卡内容,都能在半小时内改变站点的客流方向。经典规划模型假设需求是一个期望值,按期望值优化出来的方案在需求偏航时往往表现得很差。
以某个站点为例,早高峰假设缺车,调度车按“需求等于期望值”规划了装车数量。结果当天这个站点因为附近小学搞春游,需求暴涨到期望值的两倍,调度车带着一车车来得不够,整个片区的用户体验都会崩掉。
DSDRO里用的**分布鲁棒优化(Distributionally Robust Optimization, DRO)**就是针对这种情况设计的——与其说它假设需求服从某个固定分布,不如说它假设“真实分布落在以经验分布为中心的一个模糊集内”,然后优化的是最坏情况下的期望目标。粗俗地讲,就是“我不知道暴风雨什么时候来,但我按可能会来暴风雨做准备”。
2. DSDRO模型的核心设计拆解
2.1 整体框架:把“先定价、再调车”改成“一起算”
DSDRO的决策变量分成三层。第一层是动态定价变量,决定每个站点、每个时段、每种骑行需求对应的价格优惠或附加费;第二层是车辆路由变量,决定调度车辆的访问顺序和装卸量;第三层是鲁棒决策变量,用于评估需求不确定性带来的风险成本。
从数学形式上看,这是一个带随机约束的两阶段优化问题。第一阶段决策动态定价和车辆路径,第二阶段根据不确定需求实现值调整装卸决策。目标函数由三部分组成:
- 用户骑行收益:骑行量乘以价格,考虑价格对需求的弹性抑制;
- 调度运营成本:车辆行驶距离、单次停靠成本、装卸人工成本;
- 惩罚项:站点实际库存偏离目标库存的罚金,包括无车可借和满车难还。
联合优化的直接收益是让定价策略“看见”调度车的位置。假如调度车正在往某个站点赶,那这个站点的定价就不应该调得太激进,否则用户把车骑走、调度车又运来一批,双向努力就打架了。反过来,如果某个站点已经断车,定价系统还没反应过来,调度车就得按最高优先级处理,成本飙升。
2.2 价格响应:把用户行为建模成可用数学描述的函数
动态定价分配的核心是价格弹性模型。假设从站点i到站点j的潜在骑行需求为D_ij,基准价格为p_0,设定站点折扣系数r_i,则实际骑行量为:
F_ij(r_i) = D_ij · exp(-α · r_i)
其中α是价格敏感系数。这个指数形式的好处是:价格越高(r_i越大),需求衰减越快;价格接近零时需求逼近潜在需求。α越大表示用户对价格越敏感,通常通勤型用户比休闲型用户敏感度低,早高峰比午高峰低。
设计定价网格时要注意一个坑:折扣不能无限给。我发现很多入门版本把价格设成连续变量,求解器确实跑得动,但结果里会出现“负价格”,也就是倒贴钱请用户骑车——这在学术论文里很漂亮,落地时甲方不会批预算。所以我在代码里加了价格上下限约束,把折扣率限制在0到0.5之间,每个周期降价总预算也做了封顶。
2.3 车辆路由:带装卸量决策的路径规划
车辆路由部分是一个典型的**带时间窗的取送问题(VRPTW)**变体。调度车从仓库出发,访问一组站点,在每个站点决定“卸下几辆车、装上几辆车”,最后返回仓库。
建模时每辆调度车对应一条路径,用二元变量x_ij表示车辆是否从站点i开到站点j,用连续变量u_it表示t时刻在站点i装卸的车辆数。库存守恒约束写起来很直白:
s_t(i) = s_{t-1}(i) + q_t(i) - R_t(i) + u_t(i)
s是站点库存,q是自然流入量(用户骑进来的车),R是自然流出量(用户骑走的车),u就是调度车的净装卸量。调度车不能凭空变出车,所以全系统有个车辆总量守恒:
sum_i q_t(i) = sum_i R_t(i)
这意味着调度只能调节车辆空间分布,不可能制造新车。配合这个约束,模型中很多看似可行的方案会暴露问题,比如某个区域整体缺车,那单靠内部调度解决不了,就得靠定价把用户吸引出去骑一圈,把车从外部带回来。
路由约束和装卸量约束之间有一个很容易被忽略的联动关系:车辆装卸数量不能超过调度车的最大载运量,而且装卸时间取决于装卸量大小。车载量和站点装卸时间之间的线性关系会显著增加模型求解难度,但对实战很重要。我用的设定是每装卸一辆车耗时30秒,加上站点固定停靠时间3分钟,这样路径耗时才等于真实运营时间。
2.4 分布鲁棒部分:Wasserstein模糊集与最坏情况目标
DRO的核心是构造需求分布的模糊集。我采用的是Wasserstein模糊集:以历史需求样本的经验分布为中心,以Wasserstein距离为半径,划定一个包含所有“合理分布”的集合。优化目标是在这个集合中所有分布下的最坏期望成本最小化。
Wasserstein距离可以理解为“把一个分布挪成另一个分布需要付出的最小代价”,像倒沙子——相邻站点之间需求分布如果非常接近,挪动代价就小,模糊集就能收紧一些;如果历史数据表现出很大的波动性,半径就必须放大,相当于承认自己对需求分布所知有限。
模糊集半径在模型里是个超参数,它直接影响调度方案的保守程度。半径设成0,DRO退化成了普通随机规划,完全信任历史分布;半径设得过大,模型会要求所有站点必须时刻保持高库存,调度车空驶率惊人,成本完全压不住。实践中我一般先用交叉验证扫一遍半径,选整体成本曲线拐点处的值,而不是单纯选最小化历史成本的值。
2.5 联合优化为什么比独立优化稳
拆开做的模型像两个齿轮——单独转都没问题,咬合在一起就卡齿。定价模型只根据当前库存调价,不知道调度车一小时后会来,就可能为了疏导淤积站点疯狂降价,结果把后面调度的装载计划打乱;路由模型只认预测的需求分布,不知道定价会改变需求,又把车派到已经被定价“填饱”的站点。
DSDRO的联合目标函数里,定价变量和路由变量是对称的:任何一方的激进调整都会反映到总成本上。定价给太多折扣,收益项立刻减少;调度车跑太远,成本项立刻上升;只有两者配合得当,目标函数才能找到最优平衡点。这种“一根绳上的蚂蚱”的结构,恰恰是联合优化的价值所在。
我在实验里对比过两套方案:先用定价模型算出折扣率,再固定这个折扣率做路由优化;和DSDRO同时优化两个变量。在同一个真实站点数据集上,DSDRO的总运营成本比两段式优化降低了大约12%,其中大部分收益来自调度车空驶距离的减少——因为定价已经消化掉了部分不平衡,调度车只需要处理那些价格搞不定的硬缺口。
3. Matlab实现与代码架构
3.1 数据准备:站点拓扑、历史骑行和需求场景生成
Matlab实现的第一步是数据准备。你需要四份核心数据:站点经纬度或编号坐标、站点间骑行需求矩阵、站点初始库存、调度车参数。如果拿不到真实数据,用随机生成器构造一个30站点的小规模测试网络也够用,关键是结构要完整。
需求场景生成是个容易被低估的环节。DRO模型需要一批历史需求样本来构造经验分布,这些样本不能是简单的高斯随机数——共享单车的需求具有明显的时段周期性和空间相关性。我的做法是用MVNMRF(多元正态马尔可夫随机场)生成100组需求场景,再叠加一个随机冲击项模拟天气和突发事件,这样生成的样本既保留了时空相关性,又有一定不确定性。
代码结构上,推荐把数据加载、场景生成、模型构建、求解、后处理五个步骤分开。Matlab工程文件不需要太多花哨技巧,一个主脚本加三个函数文件就够了,多了反而难维护。
3.2 决策变量、目标函数和约束的Matlab建模要点
用Matlab建模优化问题时,我强烈建议先用符号变量把问题写清楚,再转到数值求解。这里有一个效率技巧:对于线性目标函数,直接用矩阵形式构造;对于含非线性项的定价收益,需要用罚函数法或者逐段线性近似处理。
下面是核心目标函数和约束的伪代码框架(完整版太长,这里给出骨架):
% 决策变量 % x(站点i,站点j,车辆k):调度路径0-1变量 % y(站点i,站点j,时段t):用户骑行量 % p(站点i,时段t):动态定价折扣率 % u(站点i,时段t):调度装卸量(正为装入,负为卸下) % 目标函数:骑行收益 - 调度成本 - 库存失衡惩罚 optimize( -sum(y .* p) + c_dist * sum(x .* dist_matrix) + c_penalty * sum(abs(s_target - s_actual)) ) % 约束函数 function [c, ceq] = constraints(x, y, p, u, station_data, router_data) % 车辆路径约束:从仓库出发且回到仓库 % 库存守恒约束:s(t+1) = s(t) + q_in - q_out + u % 价格范围约束:0 <= p <= 0.5 % 调度车容量约束:sum(u) <= vehicle_capacity end约束函数里最费劲的是库存守恒的时间递推。因为调度车的访问时序会改变站点库存变化路径,所以u的取值依赖于x的取值,两个变量之间形成强耦合。解决方法是把站点状态向量化:预计算所有调度车的访问时间表,再把每个时段的库存更新写成矩阵运算。这一步做对了,整个模型的求解速度能提升一个数量级。
3.3 求解器选型:YALMIP+GUROBI还是纯Matlab优化工具箱
DSDRO模型本质上是混合整数非线性规划(MINLP),决策变量里既有0-1路径变量,又有连续的价格变量,目标函数含指数项和非线性惩罚。选求解器是最关键的技术决策之一。
我的实际搭配是YALMIP做建模层,GUROBI做MIP求解核心。YALMIP是Matlab下的优化建模工具箱,能把约束和目标函数转换成标准求解器能吃的格式,对这类大规模联合优化问题特别顺手。GUROBI在整数线性规划上的性能是业界标杆,比Matlab自带的intlinprog快好几倍。
如果你手头没有GUROBI授权,退而求其次用intlinprog也能跑,但要注意两个限制:一是非线性目标函数要手动线性化,二是个数超过200个0-1变量的路径规划,intlinprog会明显吃力。我的处理方案是:把价格弹性函数用分段线性逼近,然后整张模型就变成了混合整数线性规划(MILP),intlinprog在这个规模下还是能用的。
纯Matlab内置的fmincon不是不能用,但它是连续优化求解器,处理0-1路径变量非常别扭,我试过用罚函数把离散性软约束掉,结果经常收敛到不可行路径。所以除非只做连续定价子问题,不推荐fmincon处理完整模型。
3.4 关键参数校准:价格弹性、Wasserstein半径、惩罚系数
模型里有几个参数直接决定优化结果的走向,必须仔细校准。
价格弹性系数α:这个参数错了,整个定价策略都会跑偏。α太大会导致模型极度保守,动不动就打五折;α太小则定价对需求几乎没引导作用。校准方法是从历史数据中回归:统计不同折扣水平下的骑行量变化,拟合指数曲线的斜率。没有历史数据的话,参考值是0.02到0.05每折扣百分点,但这个范围仅作初始化,强烈建议用实际数据重标定。
Wasserstein半径ε:决定模型对需求不确定性的保守程度。我用的策略是分层扫描:先跑5个等比例放大的半径值,画出总成本曲线,选择曲线由陡降转平缓的拐点。这个过程要留意一个细节:半径过小时,模型会出现“幻觉”,觉得历史分布很可靠,结果在低概率需求场景下库存惩罚爆炸;半径过大时,总成本被保守方案抬得很高,虽然稳定但运营上不可承受。
库存失衡惩罚系数c_penalty:这个数平衡“调度成本”和“用户体验惩罚”的相对权重。c_penalty太大,模型会不顾一切把所有站点都调到完美库存,调度成本飞到天上;太小,模型会放任站点淤积和空置,用户体验崩溃。实战上我先把总运营预算拆成调度预算和补贴预算,反推出来的惩罚系数通常落在单次调度成本附近,以此为初值再微调。
4. 实操过程与排雷记录
4.1 从零到一跑通DSDRO的完整流程
跑通一套DSDRO模型的完整步骤如下。第一步,把站点经纬度转成欧氏距离矩阵——如果直接用真实路网距离,计算量会爆炸,第一版先近似,验证逻辑通了再替换。第二步,生成需求场景。第三步,写YALMIP模型文件。第四步,求解。第五步,验证可行性和敏感性分析。
我处理的数据集是30个站点、3辆调度车、8个时段。每轮求解时间在GUROBI下大约是3到5分钟,比预想的快,因为站点规模不大,且价格变量的分段线性化把非线性项不多了。求解完成后要做的不是直接读取目标函数值,而是把定价策略和路由方案分别导出来,画在站点地图上肉眼检查一遍。我经常发现模型给出的调度路径“看起来合理但实际不可执行”,比如要求调度车在同一分钟访问两个相距2公里的站点——原因是约束里缺少同一时段重复访问排除项,属于建模范式错误。
4.2 我踩过的三个大坑
坑一:价格弹性函数和路由成本量纲不匹配。第一版目标函数里骑行收益是按“元”算的,调度成本也按“元”算,但数值量级差了十倍,优化器直接放弃调度优化,只拼命给折扣降成本。解决办法是把两个目标项归一化后再加权,比如把调度成本除以总预算、骑行收益除以基准营收。
坑二:库存守恒约束的索引错位。用向量化方式更新库存时,时段索引和站点索引错了一位,导致每个站点的库存都被莫名加了一条用户骑行量。排查方法是在约束函数里插入临时断点,打印第10站、第3时段的库存变化明细和真实数据对比。这个坑调试了我整整一晚上,最后是画出库存曲线才发现的。
坑三:模糊集半径扫描结果跳变。半径从3扫到4时,总成本突然下降了30%。一开始以为算法有问题,后来发现是半径增大后模型改变了调度策略——从“派车补救”切换为“定价预防”,调度车空驶率大幅下降,总成本自然降低。这不是bug,是模型的相变现象。遇到这种跳变别慌,减小半径步长细化扫描,找到真实的成本拐点即可。
4.3 求解失败的排查口诀
MINLP模型求解失败几乎是必然的,关键是快速定位问题。
第一行代码先检查模型是否可行——求解器最直观的信号是返回“Infeasible problem”。这时候不要急着改约束,先把决策变量全部固定成某个可行解,看目标函数计算是否正常。如果固定解下目标函数能算出来,说明约束集合没问题,问题出在求解器寻优空间配置上。
最常见的不可行原因是库存守恒和时间窗约束互相冲突。比如调度车要在8个时段内跑完所有站点,但路径最短耗时算出来是12个时段,模型自然无解。解决方法是放宽时间窗,或者增加调度车数量,看可行性有没有改善。
如果模型可行但解出来的路径“太奇怪”,先去检查距离矩阵的对称性。Matlab里pdist2计算出来的矩阵不一定对称,如果直接用会逼着调度车走有向边,路径规划结果会呈现奇怪的环路。
4.4 性能调优的最后一公里
求解时间超过容忍范围时,我常用的三板斧:一是把调度车的起始仓库固定为第一个站点,减少对称解带来的分支;二是给0-1变量添加优先分支顺序,先固定路径主干的变量,再处理装卸量变量;三是把较长时间窗切成小时级窗口,用滚动时域方法分阶段求解。
滚动时域是这类大规模问题的终极解法:把一天24个时段切成8个三小时窗口,每个窗口求解一次,把前一个窗口的最终库存作为下一个窗口的初始库存。这样单次求解规模直线下降,而且天然支持实时响应——每三小时可以根据最新需求重新定价、重排路径,鲁棒性不降反升。
我在30站点案例上,滚动时域版本的单窗口求解时间控制在40秒内,整个调度计划晚高峰前15分钟就能重算一轮,完全满足运营系统的响应要求。
5. DSDRO能往外扩展到什么领域
5.1 同构问题:电动滑板车、共享汽车、快递柜
DSDRO这个框架不完全限于共享单车。任何有时空分布失衡问题的共享资产运营都可以套用,核心只需要做两处调整:第一处是定价变量的含义——共享汽车改成里程折扣,快递柜改成投递费减免;第二处是“车辆路由”的实际载体——共享单车是调度卡车,电动滑板车可能是运营人员骑车换电,快递柜则是运输货车。
我在论文里看到过东京的共享电滑板车团队做过类似工作,区别只是把车辆容量约束变成电池电量约束。建模骨架一模一样,只是把“车辆数守恒”换成“电量守恒”。这说明DSDRO的内核是资产时空再分布,载体形式只是变量解释层面的差异。
5.2 与现有运营系统怎么结合
落地时最大的阻力不是算法本身,而是运营系统的对接。定价指令要下发到小程序端,调度路径要同步给司机App,中间还有支付网关、用户通知、客服投诉处理。我在项目里做了一个折中方案:算法输出的定价矩阵和路径方案转成运营人员可审核的任务工单,人工确认后才执行。虽然牺牲了一部分实时性,但避免了“算法瞎指挥,线下放烟花”的尴尬。
后续可以考虑的方向有两个:一是把训练好的DSDRO模型部署到云端,与骑行App实时联动,按小时滚动更新定价策略;二是引入多智能体仿真环境,让DSDRO的策略在仿真环境里跑一轮再上线,降低试错成本。
我个人的体会是,这类模型的价值不在于“一次性求出最优解”,而在于它提供了一套把分散决策拉通的话语体系。动态定价和车辆调度在传统组织架构里往往分属运营部和物流部,各有各的KPI,各有各的甩锅理由。DSDRO把两者的成本放进同一个目标函数之后,部门沟通的焦点反而从“谁对谁错”变成了“如何一起降低联合成本”。这也是我最初入坑这个模型时没有预期到的收获。