简介:一份面向新能源汽车与共享出行领域研究者、从业者的学术文献,聚焦单向共享电动汽车(OW-EC)站点间车辆库存不平衡问题,提出基于需求预测的车辆调度方法。资源为PDF格式,共1个文件,大小3.32MB,便于直接阅读与存档。文中采用长短期记忆神经网络对站点车辆需求进行预测,并构建以调度成本、库存成本和潜在损失收益均最小为目标的多目标优化模型,利用遗传模拟退火混合算法求取最优解,系统阐述了影响因素选取、模型构建与实证验证流程。针对区域人口特性、交通通达性、土地使用属性和时间属性四类关键因素,论文给出了细致分析,适合交通运输、汽车工程及相关专业学生与工程技术人员在课题研究、算法设计或论文写作中参考。目前已有124人学习浏览。
1. 单向共享电动汽车调度,难在车不会自己回位
单向共享电动汽车的调度,不是「哪里有空位就往哪里塞车」那么简单。用户把车从 A 开到 B,车辆分布马上就偏离了原本的供需匹配:早高峰车辆涌向办公区,晚高峰又涌向居住区,两股车流在时间上永远错峰。系统的成本不在行驶里程,而在那些「被塞满的站点」和「被掏空的站点」之间的反差。
这个标题代表了一条完整的数据链路:先用需求预测算出每个站点未来若干时段的借出量与归还量,再把预测出的盈余车辆搬到亏空站点。这里的车辆调度方法,既指调度车的路径与装卸数量,也指通过价格激励让用户「人工搬车」。下面按「先预测、后调度」的顺序,讲一套可以直接落地的方案。这套路子适合共享出行平台做算法的工程师,也适合研究智能交通系统的人用来搭原型。
2. 需求预测:把每个站点的「借出」与「归还」分别建模
2.1 预测对象是净库存变化,不是订单量
很多人一开始会把预测目标设成「站点订单量」,这是错的。调度模型真正关心的是每个站点在时段 t 的库存变化:
S(i, t+1) = S(i, t) - B(i, t) + R(i, t) + M(i, t)其中B(i, t)是借出量,R(i, t)是归还量,M(i, t)是调度搬入量。用户从站点 i 借车,车会在其他站点被归还,所以一个站点的库存受两个方向的流量控制。如果把订单量当目标,等于把「流出」和「流入」混在一起,预测出来的净库存偏差很大。
借出和归还应该拆成两个独立目标来训练。原因是它们背后对应不同的行为模式:借出量受出发地属性影响大,比如住宅区早上出门、办公区午间外出;归还量则受目的地属性影响大,比如办公区早上涌进车、商场晚间积车。把两个序列分开建模,后续调度模型才能分别调参。
还有一个工程问题需要先处理:单站点数据稀疏。一个小时只有两三个订单的站点占一半以上,直接训练时序模型会学到一堆噪声。常见做法是按 15 分钟或 30 分钟聚合,再把半小时内订单量低于某个阈值的站点合并成「区域级预测」;区域内部再用比例系数拆回站点。比例系数可以用过去 4 周该站点在该时段占区域总量的比例,不用每次重新训练。
2.2 特征怎么取:以站点为锚点,叠加时间与空间上下文
站点级特征围绕三组信息展开,我一般把特征分成「上下文」「潮汐」「邻居」三类,构建成一张宽表:
| 特征组 | 具体字段 | 说明 |
|---|---|---|
| 时间上下文 | hour、weekday、is_holiday、半小时序号 | 捕捉通勤、午休、夜生活等周期 |
| 环境上下文 | 温度、降雨量、空气质量指数 | 雨雪天对分时租赁影响非常明显 |
| 潮汐因子 | 前 7 天同时段净流量均值 | 代表该站点「早上出车多还是进车多」,比单纯的 POI 属性更贴实际 |
| POI 属性 | 站点 500 米内办公楼、住宅、地铁站、商场数量 | 处理新站点的冷启动,也可以作为潮汐因子的补充 |
| 邻居状态 | 周边 3 公里内其他站点的平均借出/归还量 | 捕获区域级事件,比如演唱会散场导致周边站点同时被取空 |
潮汐因子是我在共享单车和电动汽车项目里都验证过有效的特征。它不需要额外数据源,只从历史订单表里聚合:净流量均值 = 过去 7 天同时段归还量均值 - 借出量均值。正值代表这个时段车往这个站点聚集,负值代表车往外流。
特征要滞后对齐。预测 t 时段的借出量,等 t 时段的真实值带着点建模型会出现「特征穿越」,离线指标漂亮,线上全部失效。我的习惯是:所有统计特征只允许用 t-1 之前的数据,t 当天的实时订单可以单独做一个近实时修正模型,不要混进主模型。
2.3 模型选型与评估:为什么用分位数回归而不是普通回归
站在工程落地角度,时空图神经网络的精度提升通常不超过 5% 到 8%,但部署成本和排查难度上升一个量级。需求预测不是排行榜竞赛,调度的输出需要的是「偏差方向可控」的预测,而不是「平均误差最小」的预测。
普通回归模型优化的是均方误差或平均绝对误差,它给出的是条件期望。这在调度场景有实际问题:当模型低估了某个站点的借出量,调度系统就不会派车过去,用户体验直接变差;而当模型高估了借出量,多调度几次的成本是有限的。所以预测误差的分布不是对称的,需要分位数回归来直接控制偏差方向。
评估指标用两个,一个是常规的 RMSLE:
RMSLE = sqrt( (1/N) * sum((log(p_i + 1) - log(a_i + 1))^2) )另一个是「方向命中率」:预测净流量(归还减去借出)的符号是否和真实值一致。方向命中率低于 60% 时,调度模型输出会非常不稳定,因为库存预测的正负频繁翻转,调度指令也跟着翻。
2.4 用 LightGBM 训练分位数预测模型
下面是一个可以直接跑的 LightGBM 分位数回归示例,目标分别建模借出量和归还量:
import lightgbm as lgb import pandas as pd # 假设 train 是通过特征工程后的宽表,一张采样表 # 行含义:某站点在某个时段 train = pd.read_parquet("station_features.parquet") features = [ "station_id", "hour", "weekday", "is_holiday", "temp", "precip", "poi_office", "poi_metro", "tide_factor", "lag_borrow_7d", "lag_return_7d" ] X = train[features] y_borrow = train["borrow_cnt"] # 目标1:借出量 y_return = train["return_cnt"] # 目标2:归还量 # alpha=0.7:对借出量做高估,宁可预计多借一点 model_b = lgb.LGBMRegressor( objective="quantile", alpha=0.7, n_estimators=300, learning_rate=0.05, max_depth=5, num_leaves=31 ) # alpha=0.3:对归还量做低估,不指望站点回太多车 model_r = lgb.LGBMRegressor( objective="quantile", alpha=0.3, n_estimators=300, learning_rate=0.05, max_depth=5, num_leaves=31 ) model_b.fit(X, y_borrow) model_r.fit(X, y_return)这里两个模型的分位数参数方向是刻意的:借出量用 0.7 分位数,代表「往多了借」的方向估;归还量用 0.3 分位数,代表「往少了还」的方向估。两者叠加后,预测出来的净库存变化是偏保守的——系统会认为站点比实际更容易缺车,从而提前安排调度。这对生产环境友好:宁可多派一次车,也不要让用户到站无车可用。
num_leaves=31和max_depth=5是控制模型复杂度的参数,站点数据量不大时这两个值足够;数据量超过百万行可以适当放开。learning_rate=0.05搭配n_estimators=300,如果验证集损失还在下降,就把 n_estimators 往上加,同时调低 learning_rate。
3. 车辆调度方法:把失衡问题转成最小费用流
3.1 库存失衡的数学表达
预测做完后,每个站点在预测时域内的净变化就已知了。接下来要回答的问题是:哪些站点多车,哪些站点少车,调度车该从哪儿搬多少到哪儿。
先定义每个站点在当前策略下的「目标库存区间」。目标区间不是 [0, 容量],因为实际运营必须留安全缓冲:如果库存被调度到刚好满桩,用户在高峰时段还不上车,系统照样流失单子。我一般取容量的 20% 到 85% 作为正常区间,剩下的 15% 上下空间留给预测误差。
站点 i 在时段 t 的预期库存为:
expected_inv(i, t) = current_inv(i) - borrow_pred(i, t) + return_pred(i, t)如果expected_inv高于上限cap_max,该站点的超额车辆surplus = expected_inv - cap_max可以搬走;如果低于下限cap_min,亏空量deficit = cap_min - expected_inv需要补入。这个「供应量/需求量」结构,正好是运输问题的输入。
3.2 目标函数与约束参数
调度任务的优化目标包含三部分:搬移成本、缺车惩罚、满桩惩罚。三者必须统一进同一个目标函数,否则模型会做出「干脆不搬」的局部最优解。
min sum( move_cost(i,j) * x(i,j) ) + sum( miss_penalty(i) * miss(i) ) + sum( full_penalty(i) * full(i) )决策变量x(i, j)表示从站点 i 搬到站点 j 的车辆数,miss是搬完后仍然低于下限的亏空量,full是搬完后仍然高于上限的溢出量。约束条件包括:站点的供应量上限、需求量下限、调度车一次最大装载量、调度车到达时间窗。
缺车惩罚和满桩惩罚一定要大于搬移成本,否则模型宁可让站点空着也不派车。工程经验是:惩罚系数设为搬移成本的 10 到 50 倍。下面是缺省参数参考:
| 参数 | 默认值 | 说明 |
|---|---|---|
搬移成本move_cost | 每辆车每公里 2 元 | 包含司机人力与车辆损耗 |
缺车惩罚miss_penalty | 60 元 | 相当于失去一个订单的收益加品牌损失 |
满桩惩罚full_penalty | 40 元 | 满桩会导致还车用户流失,严重时触发调度 |
| 调度车容量 | 6 辆 | 常见金杯车改造的后备箱可装载量 |
| 目标库存下限 | 容量的 20% | 低于该值触发补车 |
| 目标库存上限 | 容量的 85% | 高于该值触发收车 |
3.3 滚动时域与批量调度
调度计划要不要覆盖未来 24 小时?不建议。预测误差会随时间快速增大,调度计划排到 6 小时以后基本就不具备执行价值。我见过比较稳定的做法是「滚动优化」:每次只生成未来 2 到 3 小时的调度指令,每 30 分钟滚动一次重算。
重算频率与执行成本要平衡。调度车每单派出的固定成本在 30 到 50 元,如果每 30 分钟都调度,一天下来可能触发上百单派遣,成本失控。实际参数是「触发式调度」:只有当某个站点的亏空或溢出量超过阈值(比如 3 辆车),才把该站点放入优化模型。低峰期 3 小时跑一次批量任务,高峰期 30 分钟跑一次,这样既能控制执行成本,又不至于让失衡累积。
在代码实现上,把预测函数和优化函数封装成两个接口即可:预测接口输入当前站点状态和特征,输出预测净流量;优化接口输入预测净流量和目标区间,输出调度指令。滚动时域只是改变这两个接口的调用频率,不需要改动模型本身。
3.4 价格激励如何进入调度模型
调度车搬移是成本最高的手段。单向共享系统里更经济的做法是动态定价:当站点 B 缺车时,把「从 A 开往 B」的订单价格下调,或者给用户发定向优惠券,引导用户自己把车开过去。这就是基于用户参与的重平衡。
在数学模型里,价格激励可以表达为「虚拟调度车」:它不需要实车搬运,而是通过调整订单流量分布来改变净库存。具体做法是在目标函数里增加一项incentive_cost * y(i, j),y表示通过价格引导产生的用户骑行量。约束条件不再是车辆容量,而是价格敏感弹性系数:超过一定量级的引导会失效,因为愿意为了 5 元优惠改变目的地的用户始终是少数。这个上限我一般按该时段订单总量的 10% 到 15% 估计,具体值需要做 AB 实验校准。
4. 跑通最小调度闭环:从预测输出到调度计划
4.1 用模拟数据搭一个可复现的闭环
为了让整个流程跑起来不依赖业务数据,我先构造一个小规模模拟场景:4 个站点,每个站点设定不同的潮汐方向,生成 96 个时段(15 分钟粒度)的借还车记录。注意,这里模拟数据的作用是把链路跑通,真实场景把数据源替换成订单表即可。
import numpy as np import pandas as pd np.random.seed(42) # 站点潮汐方向:office 早高峰车流出,residential 晚高峰车流出 config = { "S1": {"cap": 20, "type": "office"}, "S2": {"cap": 20, "type": "office"}, "S3": {"cap": 18, "type": "residential"}, "S4": {"cap": 18, "type": "residential"}, } periods = 96 rows = [] for tid in range(periods): hour = tid // 4 for sid, cfg in config.items(): if cfg["type"] == "office": if hour in (8, 9): b, r = 3, 0 elif hour in (17, 18): b, r = 0, 3 else: b, r = 0, 1 else: if hour in (8, 9): b, r = 1, 3 elif hour in (17, 18): b, r = 2, 0 else: b, r = 1, 1 # 叠加泊松噪声,模拟真实随机波动 rows.append([sid, tid, hour, max(0, int(np.random.poisson(b))), max(0, int(np.random.poisson(r)))]) df = pd.DataFrame(rows, columns=["station", "tid", "hour", "borrow", "ret"])这是模拟数据的核心逻辑:办公区早高峰借出量高、归还量低,居住区反过来。这样刻意制造出「车往办公区堆积」的失衡场景,方便后面观察调度模型是否起作用。
4.2 用历史均值做需求预测,接口和 LightGBM 兼容
第 2 章的 LightGBM 模型在真实场景替换,这里先用「历史前 72 个时段均值」做预测,让代码最短。预测接口统一留成predict_net(station, hour),生产环境换成模型推理即可。
# 前 75% 数据作为训练窗口,后 25% 作为模拟当前时刻 train_df = df[df.tid < 72] test_df = df[df.tid >= 72] # 预测每个站点每个小时的借出/归还均值 pred = train_df.groupby(["station", "hour"])[["borrow", "ret"]].mean() test_df = test_df.merge(pred, on=["station", "hour"], suffixes=("_true", "_pred")) # 计算测试时段内每个站点的累计净流量(借出-归还) net = test_df.groupby("station").apply( lambda g: g["borrow_pred"].sum() - g["ret_pred"].sum(), include_groups=False ).to_dict() print(net) # 输出示例: {'S1': 24.5, 'S2': 19.0, 'S3': -18.5, 'S4': -14.0}这段代码输出的net代表未来一段时间内各站点预测的车辆净流出量。正值表示该站点预测会净流出车,即库存减少,需要补车;负值表示净流入,即库存增加,需要抽车。
4.3 构建运输问题并求解最优调度指令
拿到每个站点的净流量后,结合当前库存计算供给与需求,再交给线性规划求解器。这里用 PuLP,它比 scipy.optimize.linprog 表达约束更直观。
import pulp current_inv = {"S1": 10, "S2": 10, "S3": 9, "S4": 9} upper = {sid: cfg["cap"] * 0.85 for sid, cfg in config.items()} lower = {sid: cfg["cap"] * 0.20 for sid, cfg in config.items()} # 预测后的预期库存 expected_inv = {sid: current_inv[sid] - net.get(sid, 0) for sid in config} # 计算供应量和需求量 supply = {} demand = {} for sid in config: if expected_inv[sid] > upper[sid]: supply[sid] = int(expected_inv[sid] - upper[sid]) elif expected_inv[sid] < lower[sid]: demand[sid] = int(lower[sid] - expected_inv[sid]) # 站点间距离矩阵,模拟数据中用编号差值代替 dist = {("S1", "S3"): 5, ("S1", "S4"): 8, ("S2", "S3"): 6, ("S2", "S4"): 4, ("S1", "S2"): 3, ("S3", "S4"): 3} prob = pulp.LpProblem("rebalance", pulp.LpMinimize) x = pulp.LpVariable.dicts( "move", [(i, j) for i in supply for j in demand], lowBound=0, cat="Integer" ) # 目标函数:最小化总搬移距离 prob += pulp.lpSum(dist.get((i, j), 10) * x[i, j] for i in supply for j in demand) # 约束1:从盈余站点搬出的总数不能超过供应量 for i in supply: prob += pulp.lpSum(x[i, j] for j in demand) <= supply[i] # 约束2:搬入亏空站点的总数必须满足需求量 for j in demand: prob += pulp.lpSum(x[i, j] for i in supply) >= demand[j] prob.solve() print("状态:", pulp.LpStatus[prob.status]) for (i, j), var in x.items(): if var.value() and var.value() > 0: print(f"从 {i} 搬到 {j}: {int(var.value())} 辆")目标函数里的距离值直接用两点间行驶距离,映射函数返回的是每辆车每公里的成本。约束 1 保证盈余站点不会超发车辆;约束 2 保证亏空站点至少补到安全线。cat="Integer"是因为车辆是离散物体,不能搬半辆。
4.4 参数表与结果解读
运行上面的代码,输出大致会是这样:
| 站点 | 当前库存 | 预测净流量 | 预期库存 | 动作 |
|---|---|---|---|---|
| S1 | 10 | +24.5 | 0 以下 | 补入 4 辆 |
| S2 | 10 | +19.0 | 0 以下 | 补入 4 辆 |
| S3 | 9 | -18.5 | 超上限 | 抽出 4 辆 |
| S4 | 9 | -14.0 | 超上限 | 抽出 4 辆 |
调度指令会把 S3 和 S4 的车搬到 S1 和 S2。这是最优解吗?在这个规模下是,因为运输问题的约束矩阵是全幺模的,线性松弛解天然满足整数性。
实际生产环境里,求解规模上千站点、上百辆调度车时,PuLP 的 CBC 求解器可能不够快。这时我会换成 OR-Tools 的 min cost flow 模块,它专门针对网络流结构做了优化,万级节点能在秒级出解。框架层面的接口不用改,只是把 PuLP 的变量和约束替换成 OR-Tools 的SimpleMinCostFlow,返回结果仍然是「从哪个站点搬几辆到哪个站点」。
5. 用「库存健康度」校准调度结果,抵御预测误差
5.1 预测误差如何传导到调度误差
需求预测的误差不会均等地传给调度模型。当真实净流量和预测值方向一致但幅度偏小时,调度计划只是多搬了几辆车,成本受损但服务不受影响;当真实值和预测值方向相反时,调度指令彻底错误,该搬走的站点没有搬,不该搬的站点被搬空。
传导路径是:预测净流量 → 预期库存 → 供应量/需求量 → 搬移指令。每一层都在放大误差。所以调度模型不能直接吃预测均值,需要做一层平滑。常见做法是把预测值乘一个缩水系数,比如 0.85,让模型「看得保守一些」;同时把目标区间从 20%–85% 收窄到 30%–70%,留出更多误差缓冲。这两个参数不是拍脑袋定的,用历史数据回放网格搜索即可。
5.2 调度执行偏差的保守化处理
调度指令执行不是精确的。调度司机可能少装两辆车,可能路上堵车晚到 40 分钟,可能站点临时被私家车占位无法泊入。如果优化模型假设「说了搬 5 辆就能搬 5 辆、半小时准到」,系统会在真实执行时严重偏离。
处理办法是在模型输出的基础上做执行缓冲:每个站点的目标库存不再是一个点,而是一个区间。比如某站点容量 20 辆,目标区间是 [4, 17],调度模型按区间下界执行时,多出的车辆不急着搬走,等达到 18、19 辆时再触发。同时,把调度车容量从 6 辆下调到 5 辆来执行计划,多出的 1 辆容量余量用于吸收司机装载误差。这样的保守化会让调度频率略降,但服务水平的稳定性明显上升。
5.3 快速基线:按健康度差值排序
优化模型上线前,需要先有一个不依赖求解器的快速基线,用来验证优化到底有没有带来增量。我常用的是「库存健康度差值调度法」。
库存健康度定义为:
health(i) = expected_inv(i) / capacity(i)理想区间是 0.2 到 0.85。低于 0.2 的站点按健康度从低到高排序,是补车的优先目标;高于 0.85 的站点按健康度从高到低排序,是抽车的优先来源。然后按「距离最近的来源优先」贪婪匹配,直到亏空站点都补上车或盈余站点都抽完。
这个基线不需要任何求解器,10 行代码就能跑完。优化模型的调度结果如果连这个基线都比不过,说明目标函数权重、惩罚系数或预测平滑参数有问题,先别急着上线。我见过不少项目,优化模型用复杂的时序图网络配混合整数规划,结果被一个简单排序基线打得毫无还手之力,问题就出在预测误差被优化模型过度放大。
5.4 离线验证调度效果的三个指标
调度方法上线前,用历史数据回放来验证效果,而不是依赖在线 AB。回放方式是:把历史订单切成两段,前段用于预测,后段模拟真实订单流,跑一遍调度闭环,统计三个指标:
第一个是缺车率,即真实时段内用户到达但站点无车的订单占比;第二个是满桩率,即用户想还车但站点满桩的次数占比;第三个是平均搬移距离,反映调度成本。缺车率和满桩率直接决定用户体验,搬移距离决定成本,三者放在同一张对比表里看:无调度基线、排序基线、优化模型。
优化模型相对排序基线的缺车率改善,一般在 10% 到 25% 之间属于合理范围;超过 40% 需要怀疑预测是否泄漏了未来信息。调度参数的调整顺序也有讲究:先调预测分位数,再调惩罚权重,最后才调调度车数量,因为预测质量决定了调度效果的上限。
本文还有配套的精品资源,点击获取