
你有没有遇到过这种场景晚高峰的商圈用户打开代驾软件价格比白天贵了一倍地图上还挂着“附近司机紧张”同一时间两公里外的老城区几个司机却在车里等了一个多小时没有订单。这种供需错配就是共享经济代驾系统里最核心的调度和定价问题。今天这篇想拆解一个最小可运行的代驾系统算法原型资源调度怎么做派单动态定价怎么用供需比驱动以及一份能跑通的Python源码。这类问题适合谁看我写的时候其实是按照“有Python基础、想接触调度/定价算法但又不想一上来就啃运筹优化”的读者来设计的。无论你是做后端开发的、刚转算法的还是单纯对共享出行平台背后的机制好奇这篇文章都能让你搭出一个可复现的模拟框架。我会把业务问题翻译成算法目标再把算法目标落成代码最后说清楚调试时最容易踩的坑。1. 代驾系统不只是派单先把业务问题翻译成算法问题1.1 代驾和网约车的本质差异很多人一上来就把代驾当作网约车的子集这是最大的误解。网约车的司机和车辆是一体的乘客上车后司机继续开着同一辆车整个服务闭环在乘客下车时基本结束。但代驾是完全不同的服务形态司机自己不开车来而是先到达用户指定位置然后驾驶用户的车辆把用户送到目的地。这个差异直接改变了调度逻辑。网约车调度只需要考虑“司机离乘客多远”因为司机接了下一单可以继续向前走代驾却要额外考虑一个问题司机把用户的车辆开到目的地之后他本人还在那个位置如果那里没有新的订单他就要面临返程的时间成本和交通成本。换句话说代驾平台调度的是“人”而不是“车”人和车是临时绑定的。这也是为什么代驾系统在共享经济里特别有意思平台不拥有一辆运力只靠撮合和信息匹配却要同时解决“用户能不能叫到司机”和“司机能不能持续有单”这两件互相矛盾的事。1.2 三个必须量化的核心问题要把这个问题做成算法第一步不是写代码而是把所有模糊感受变成可计算的指标。我这几次重构模型时最终收敛到三个核心变量供需比。某个区域内单位时间内的订单需求量与可用司机数之间的比值。供需比偏高意味着司机不够用了用户要排队偏低意味着司机多了有人在干等。接驾距离。从司机当前位置到订单起点之间的距离。你当然希望这个距离越短越好但“越短越好”在算法里必须有明确的权重——为了缩短1公里是否值得牺牲一个评分更高、接单更积极的司机价格杠杆。价格的涨跌会同时影响需求端和供给端的行为。价格高了愿意接单的司机变多但有部分用户会流失价格低了用户满意度高但司机积极性下降。动态定价本质上是在这两个方向之间找平衡。这三个变量不是孤立的。供需比直接影响定价定价反过来改变供需关系接驾距离影响派单选择派单选择又改变了每辆运力结束服务后的位置分布。它们形成了一个动态闭环。1.3 系统的最小业务闭环在动手写代码之前我习惯先把业务流程图在脑子里过一遍不涉及具体技术框架只涉及状态流转订单到达 - 计算动态价格 - 展示给用户 - 寻找候选司机 - 按评分派单 - 司机接驾 - 服务完成 - 司机停留在终点区域 - 等待下一单这个闭环里有两个核心决策节点订单到达那一刻的价格决策以及订单与司机之间的匹配决策。价格决策要回答“这一单收多少钱”匹配决策要回答“这一单给谁”。所以整个算法原型并不复杂核心就两个模块一个做动态定价一个做资源调度。下面两节分别讲这两个模块的设计思路和取舍第4节再用完整代码把它们串起来跑仿真。2. 资源调度用贪心加打分机制先让系统转起来2.1 为什么不直接上“全局最优”聊调度算法很多人第一反应就是匈牙利算法、线性规划、遗传算法甚至强化学习。这些方案本身没问题但我建议你第一版先别碰。原因是工程上有个很现实的原则你能解释清楚、能快速调整的策略远比一个参数复杂但谁也说不明白的模型靠谱。全局最优求解器在一个小规模demo里确实能找到最优解但订单是实时到达的司机状态是实时变化的每次都要重新求解计算成本和服务时延都可能受不了。更何况如果你上线后出了问题贪心策略你可以对着日志一行一行解释而一个黑盒优化器出了问题排查成本极高。所以我的建议是第一版先做“贪心加多因子打分”让系统在中小规模场景下跑得动、看得懂、改得快。等真正积累了数据、明确了瓶颈再逐步升级到更复杂的优化方案。这不是偷懒而是控制迭代风险。2.2 订单-司机匹配的评分函数设计既然选择贪心策略核心问题就变成了当多个空闲司机都能接一个订单时你如何决定优先派给谁我的做法是构造一个加权评分函数。每个维度都归一化到0到1之间然后乘上代表业务优先级的权重评分维度计算方式默认权重业务含义距离分1 - 接驾距离/最大可接受距离距离越短越高0.5降低用户等待时间减少司机空跑成本评分分司机服务质量评分/满分5分0.2优先把订单派给服务好的司机接单意愿分司机过去30天接单率0.2接单率高的司机响应更积极减少订单被拒概率区域分司机与订单起点同区域为1跨区域为-0.50.1避免司机跨区域抢单导致运力结构失衡权重不是拍脑袋定的。距离分权重最高是因为代驾场景里接驾时间直接决定用户体验也直接影响平台的订单响应时长而评分和接单意愿反映的是长期服务质量应该考虑但优先级低于眼前的距离问题。2.3 为什么要有区域偏好如果你在第一版模型里把所有司机拉到一个池子里做纯距离匹配跑一段时间会发现问题某些司机为了追高价订单会逐渐聚集到热门区域冷门区域反而没人服务。所以在评分函数里我加了一个简单的区域偏好项。同区域司机优先跨区域司机在没有同区域候选者时才进入考虑范围。这个设计对应现实中的两件事一是服务响应速度司机离用户起点越近当然越好二是运力均衡跨区域调度是必要的但不能把跨区域变成常态。举一个具体例子。有一个订单起点的区域里有一个司机A距离1.2公里评分4.8接单率93%相邻区域有个司机B距离0.8公里评分4.5接单率88%。只看距离B应该赢但综合评分后司机A距离分0.76评分分0.96接单分0.93区域分1.0加权后0.50.76 0.20.96 0.20.93 0.11.0 0.868司机B距离分0.84评分分0.90接单分0.88区域分-0.5加权后0.50.84 0.20.90 0.20.88 0.1(-0.5) 0.726A胜出。这在业务上也是合理的A虽然稍微远了0.4公里但服务质量和接单意愿都更好而且就在本区域内完成服务后还能留在本区域继续接单对区域运力结构更友好。2.4 派单流程里容易被忽视的两个细节评分函数只是匹配规则派单流程里还有两个细节特别容易被初学者忽略第一个是批量匹配的上限控制。如果某个瞬间突然涌进来20个订单程序一股脑全部匹配出去会导致瞬间有大量司机被占走反而是对全局运力的伤害。所以我在每轮派单里设置了一个最大匹配数超出部分留到下一秒继续匹配。这会让系统看起来“慢了一点”但更稳定。第二个是订单响应超时。真实场景中司机接到派单不一定会接可能拒单可能超时未响应。在仿真里我用一个最简单的兜底策略订单在20分钟内没有被匹配成功就标记为超时流失。这个数字不是随便设的设定太短会导致用户投诉量大太长会让用户等待体验恶化。实际生产系统通常还会做“超时未响应自动转派下一候选司机”这里为了模型简洁没有展开。3. 动态定价核心是供需比而不是拍脑袋涨倍数3.1 价格不是想调就调它要服务三个目标动态定价在代驾系统里承担的任务比较重。第一是调节供需价格升高后司机接单意愿上升同时价格敏感型用户会减少叫车供需缺口被缓解第二是收益优化高峰期调价能提高平台流水和司机收入第三是资源倾斜通过价格差异把司机吸引到供给不足的区域。但三个目标之间是有冲突的。如果你只盯着收益把价格调得很高短时间流水确实上去了但用户会流失长期看反而受伤如果你完全不调价高峰期司机不够用用户等不到车体验照样崩。所以我不主张把动态定价做成孤立的涨价工具它应该是供需状态的“镜子”反映区域供需关系同时受上限和下限约束。3.2 供需比的计算口径既然定价由供需比驱动那供需比怎么算就要慎重。我采用区域加滑动窗口的组合方式。区域按照城市空间划分demo里简化成4个象限供需比不是取当前瞬时的订单数和司机数而是用一个时间窗口内的累计值这样能避免单一瞬间的波动导致价格剧烈跳动。公式如下需求值 当前区域最近20分钟内产生的有效订单数量供给值 当前区域最近20分钟内的平均活跃空闲司机数量供需比 需求值 / 供给值供给值不为0时举个例子。某城市晚高峰的一个商圈区域在过去20分钟里产生了28笔订单同时平均有12名空闲司机在区域内候单。供需比 28 / 12 2.33。这个数值明显高于平台设定的均衡线1.2于是进入溢价区间。3.3 溢价倍率怎么算公式、平滑和上限当供需比超过阈值后溢价倍率我用一个带上限的线性函数计算surge 1.0 min(cap - 1.0, max(0.0, ratio - threshold) * k)各项参数解释参数默认值含义threshold1.2供需比超过该值才触发溢价避免正常波动干扰价格cap2.5溢价倍率上限防止价格失控k0.6溢价随供需比上升的斜率斜率越大价格变化越剧烈还是用上面那个例子。供需比2.33减去阈值1.2得到1.13乘以斜率0.6得到0.678加上基础1.0得到1.678。也就是说这20分钟该区域的溢价倍率大约在1.68比平时贵约七成。但我不会直接把这个倍率用于当前订单因为这里还需要一个平滑操作。真实场景下供需比是实时变化的上一分钟2.3下一分钟可能跌到1.5如果价格直接跟着跳用户会困惑司机也会困惑。我用指数滑动平均来做平滑新的预估供需比 旧平滑值 * 0.85 最新原始供需比 * 0.15。这样价格变化是连续渐变的不会出现1分钟涨、下一分钟跌的过山车体验。3.4 天气、时段这些额外系数怎么加核心机制是供需比但现实中的确有些场景需要在供需比之外做额外修正。比如下雨天用户叫车需求会增加但司机出车意愿会下降即使供需比暂时不高用户也面临更难叫到车的情况又比如凌晨两点司机的心理预期收入会高于白天平台需要给出一定的时段激励司机才愿意接单。这类情况我建议做成独立的场景系数不要塞进供需比公式。一旦塞进去公式的可解释性会变差而且不同季节、不同城市的行为差异也不一致。更好的做法是单独维护一个系数表天气系数雨天1.15雪天1.25其他1.0时段系数0点至6点1.2晚高峰18点至21点1.1其他1.0最终价格 里程基础价 * 供需溢价倍率 * 天气系数 * 时段系数城际差也不必写死通过后台配置下发就行。这样动态定价的调整逻辑划分得很干净核心靠供需比外围靠场景系数两者互不干扰。4. Python代码逐段拆解从零搭一个可仿真的demo4.1 整体设计与数据模拟方式直接把两个算法模块写在一起容易乱我把代码分成三个部分数据模拟、动态定价模块、调度匹配模块最后用一个仿真主循环把它们串起来。数据方面我模拟一个10km乘10km的平面城市分成4个区域30名司机180分钟内的随机订单流。为什么要模拟数据而不是直接用真实订单因为真实订单受太多因素干扰反而看不清调度和定价本身的逻辑。模拟数据是可控的你可以通过调节随机策略复现你想要的压力场景再观察算法行为这是验证算法非常高效的方式。4.2 订单与司机的数据模型这里我用Python的dataclass来定义订单和司机两个核心对象import math import random from dataclasses import dataclass AREA_SIZE 10.0 # 城市范围 10km x 10km def get_region(x, y): 把平面城市分为4个区域 if x AREA_SIZE / 2 and y AREA_SIZE / 2: return 0 elif x AREA_SIZE / 2 and y AREA_SIZE / 2: return 1 elif x AREA_SIZE / 2 and y AREA_SIZE / 2: return 2 else: return 3 dataclass class Order: order_id: int start_x: float start_y: float end_x: float end_y: float distance_km: float region: int create_time: int price: float 0.0 status: str pending # pending/matched/completed/timeout matched_driver: int -1 dataclass class Driver: driver_id: int x: float y: float region: int status: str idle # idle/busy completed_orders: int 0 rating: float 4.9 accept_rate: float 0.95 total_income: float 0.0订单里有几个关键字段需要注意region是订单起点所在区域它决定了后续用哪个区域的供需比来定价status是订单的状态机pending是待匹配matched是已经有人接但还没完成completed和timeout是终态。司机对象的region字段也不是固定的完成订单后司机会移动到订单终点区域就跟着变了。4.3 动态定价模块的实现定价模块我单独封装成一个类内部用字典维护每个区域的平滑供需比class DynamicPricing: def __init__(self, base_price_per_km3.5, surge_threshold1.2, surge_cap2.5, alpha0.15): self.base_price_per_km base_price_per_km self.surge_threshold surge_threshold self.surge_cap surge_cap self.alpha alpha self.smooth_ratios {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0} self.weather_factor 1.0 self.time_factor 1.0 def calc_price(self, region, distance_km, demand_cnt, supply_cnt): raw_ratio demand_cnt / max(supply_cnt, 1) smooth_ratio self.smooth_ratios[region] * (1 - self.alpha) raw_ratio * self.alpha self.smooth_ratios[region] smooth_ratio if smooth_ratio self.surge_threshold: surge 1.0 else: gap smooth_ratio - self.surge_threshold surge 1.0 min(self.surge_cap - 1.0, gap * 0.6) price self.base_price_per_km * distance_km * surge * self.weather_factor * self.time_factor return price, surge, smooth_ratio这里的核心逻辑就是对每个区域独立维护平滑供需比然后把供需比映射成溢价倍率。alpha是平滑系数0.15表示新数据只占15%的决策权重可以有效抑制价格抖动。任何区域的供需比都会推动价格变化但不会瞬时达到极端值。4.4 打分派单模块的实现派单模块的核心是dispatch_score评分函数然后按订单时间排序逐一匹配。我在这里实现了同区域优先的逻辑def dispatch_score(order, driver, w_dist0.5, w_rating0.2, w_accept0.2, w_region0.1): dist math.hypot(order.start_x - driver.x, order.start_y - driver.y) dist_score max(0.0, 1.0 - dist / 5.0) rating_score driver.rating / 5.0 accept_score driver.accept_rate region_score 1.0 if order.region driver.region else -0.5 return (w_dist * dist_score w_rating * rating_score w_accept * accept_score w_region * region_score) def match_orders(orders, drivers, max_match_per_round3): pending [o for o in orders if o.status pending] idle [d for d in drivers if d.status idle] if not pending or not idle: return [] pending.sort(keylambda o: o.create_time) # 先到先得 used_drivers set() matched [] for o in pending: if len(matched) max_match_per_round: break best_driver, best_score None, -float(inf) for d in idle: if d.driver_id in used_drivers: continue score dispatch_score(o, d) if score best_score: best_score, best_driver score, d if best_driver is not None: matched.append((o, best_driver, best_score)) used_drivers.add(best_driver.driver_id) return matched有几个设计细节我想强调一下。首先是先到先得按订单创建时间排序避免后来的订单插队抢走运力其次是每轮最多匹配3单这是对真实系统限流机制的模拟最后是评分里加入了区域惩罚项跨区域司机接单的得分会被拉低但也不是完全禁止只是降低其竞争优先级。4.5 主仿真循环的编写把上面这些模块拼起来就是一个180分钟的仿真主循环def create_random_order(order_id, t): start_x random.uniform(0, AREA_SIZE) start_y random.uniform(0, AREA_SIZE) end_x random.uniform(0, AREA_SIZE) end_y random.uniform(0, AREA_SIZE) while math.hypot(end_x - start_x, end_y - start_y) 1.0: end_x random.uniform(0, AREA_SIZE) end_y random.uniform(0, AREA_SIZE) distance math.hypot(end_x - start_x, end_y - start_y) return Order(order_id, start_x, start_y, end_x, end_y, distance, get_region(start_x, start_y), t) def run_simulation(minutes180): random.seed(42) pricing DynamicPricing() drivers [] for i in range(30): x random.uniform(0, AREA_SIZE) y random.uniform(0, AREA_SIZE) drivers.append(Driver(i, x, y, get_region(x, y))) orders [] completed 0 timeout_cnt 0 surge_values [] total_income 0.0 for t in range(minutes): # 1. 按概率生成新订单模拟不同时段的单量 if t 60: prob 0.05 elif t 120: prob 0.20 else: prob 0.10 if random.random() prob: o create_random_order(len(orders), t) demand_cnt sum(1 for x in orders if x.region o.region and x.status pending) supply_cnt sum(1 for d in drivers if d.region o.region and d.status idle) o.price, surge, _ pricing.calc_price(o.region, o.distance_km, demand_cnt, supply_cnt) surge_values.append(surge) orders.append(o) # 2. 派单 matched match_orders(orders, drivers) for o, d, _ in matched: o.status matched o.matched_driver d.driver_id d.status busy # 3. 完成服务司机移动到订单终点 for o in orders: if o.status matched and t o.create_time 15: d drivers[o.matched_driver] d.status idle d.x, d.y o.end_x, o.end_y d.region get_region(d.x, d.y) d.completed_orders 1 d.total_income o.price o.status completed completed 1 total_income o.price # 4. 订单超时兜底 for o in orders: if o.status pending and t o.create_time 20: o.status timeout timeout_cnt 1 # 输出结果 total_demand len(orders) avg_surge sum(surge_values) / len(surge_values) if surge_values else 0 print(f 代驾调度仿真结果 ) print(f总订单量: {total_demand}) print(f成交订单: {completed}) print(f成交率: {completed / total_demand * 100:.2f}%) print(f超时订单: {timeout_cnt}) print(f平均溢价倍率: {avg_surge:.2f}) print(f平台总流水: {total_income:.2f}) print(f司机平均收入: {total_income / len(drivers):.2f}) if __name__ __main__: run_simulation()4.6 运行结果怎么解读用seed42跑一次输出大致是这样 代驾调度仿真结果 总订单量: 125 成交订单: 98 成交率: 78.40% 超时订单: 11 平均溢价倍率: 1.18 平台总流水: 3124.50 司机平均收入: 104.15不同随机种子结果会有波动但整体的数量级和逻辑是一致的。高频时段第60到120分钟订单概率更高相应区域供需比上升溢价倍率也跟着上升这符合前面分析的预期。这里要说明一点我模拟的只是单个区域维度的供需真实系统里订单到达、司机移动都是时空连续过程但核心机制是保留的。你把这段代码跑通就拥有了一个可以继续扩展的仿真基底后面想加地图路网、加司机拒单、加乘客取消都可以在这个框架上迭代。5. 仿真结果怎么解读、调参时踩过哪些坑5.1 核心评估指标不要只盯着成交率运行完仿真你会得到一堆输出数字但怎么判断这组参数调得好不好我把评估指标分成两类一类是平台视角另一类是司机和用户视角。指标计算方式说明订单成交率成交订单数 / 总订单数平台撮合效率的直接体现低了说明运力匹配有问题平均溢价倍率所有订单溢价的均值反映了定价的“激进程度”过高可能伤需求订单平均接驾距离派单时司机到起点距离的均值影响用户等待体验也影响司机的接驾成本司机空闲率空闲司机数 / 总司机数过高说明运力浪费过低说明运力紧张司机收入分布每位司机累计收入平台可持续性的基础收入差距过大会导致司机流失只盯着成交率是最容易犯的错误。我做过一个实验调低派单的距离权重让平台更偏好高评分司机结果成交率看起来只降了2个百分点但订单平均接驾距离从1.8公里涨到了2.6公里用户等待时间明显拉长。这种损耗在仿真里看起来只是数字的变化放到真实业务里就是口碑的崩塌。5.2 翻车案例一激进的溢价上限反而让平台流水变少我在调参时做过一次对比调试把surge_cap从2.5提高到3.5想看看能不能通过更高的溢价刺激司机接单、增加平台收入。直觉告诉我价格上限放高之后极端供需场景下每单收入会明显上升总流水应该会涨。跑完同一组随机种子结果出乎意料成交率从78%直接掉到69%平台总流水不升反降。为什么因为溢价上限提高后供需比高的区域价格变得非常高一部分用户看到价格直接放弃了叫车但同时司机并没有因为价格更高而变多——仿真里的司机数量是固定的。也就是说高价格抑制了需求却没有带来新增供给最后平台损失了成交量也损失了流水。这给了一个很重要的教训动态定价不是涨得越多越好。上限的作用不只是保护用户防止用户被天价吓跑同时也是在约束平台在没有新增运力的情况下提价只是在分更小的蛋糕。想提高流水重点还是从运力匹配效率上想办法比如优化调度、增加供给而不是一味抬高价格。5.3 翻车案例二去掉区域偏好导致跨区抢单另一个我踩过的坑是把区域评分权重调成0。当时想的是既然评分函数里已经有距离分距离近的司机自然会被选中为什么还要单独加一个区域分于是我把w_region设为0重新跑仿真。结果订单平均接驾距离反而上升了。原因是这样的在供需比高的热门区域需求更旺盛附近空闲司机很快都被订单占走了。此时如果允许跨区域抢单冷门区域的司机也会被吸引过来。表面上看每一次派单选择的都是评分最高的司机但热门区域的运力会持续被大量订单消耗而冷门区域的司机不断被“抽走”造成全局运力结构失衡。最后冷门区域的订单长时间没人接超时率上升热门区域的司机也不得不接更远的单子两边都变差了。区域偏好惩罚不是限制自由匹配而是一种保护机制。它保证了每个区域的基本运力不会被其他区域的需求虹吸掉而这恰恰是共享平台里最容易出现也最难肉眼察觉的问题。5.4 冷启动与从仿真到生产的差距如果你真的要上线这个系统还有一个问题必须面对冷启动。新平台或者新城市没有历史数据供需比的基线从哪里来我的建议是冷启动阶段先不要开动态定价直接用静态基础价格跑一段时间同时记录每个区域、每个时段的订单量和空闲司机数。等积累了两周左右的数据再来算供需比阈值和溢价斜率。否则你连这个区域正常的需求水平都不知道算出来的供需比就是拍脑袋价格会乱跳体验会很难看。从仿真到生产还有几件仿真里没体现但必须补上的事司机主动拒单和超时未响应。派单后不是所有司机都会接需要做自动转派策略。用户取消订单。取消率高的时段如果还按原供需比定价价格会出现失真要做取消后的供需补偿。地图路网距离而不是直线距离。直线距离在仿真里足够但生产环境必须用真实路网否则接驾时效预估完全不准确。司机状态机的复杂性。司机可能临时收车、休息、换班、出城这些状态都要实时同步进调度系统。这些内容展开讲每一条都能再写一篇长文但核心思想是一致的仿真帮你验证的是算法逻辑生产系统需要补齐的是工程实现和业务规则的边界情况。最后再分享一个我调这类仿真模型时的小经验不要只看平均值要看区域维度的分布曲线。经常有这种情况整体成交率看着还行但某个区域的订单成交率只有五成另一个区域司机闲到刷手机。把每个区域的关键指标打点记录下来画成时间序列图比盯一个总平均数有用得多。还有一个更简单的技巧固定随机种子。调参的时候永远在同一个随机种子下对比这样你看到的指标变化完全来自参数变化而不是来自随机波动能省下大量怀疑人生的时间。