ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数学建模实战:出租车供需匹配与需求预测全流程解析

数学建模实战:出租车供需匹配与需求预测全流程解析 简介全国第二届部分高校研究生数模竞赛参赛论文围绕城市交通管理中的出租车规划问题展开面向数学建模参赛队伍、交通运输规划专业学生及运筹学应用研究者。论文首先以阻滞增长模型预测城市人口结合增长率法与重力模型法估算出行强度与总量并利用层次分析法建立乘坐出租车人口预测模型预测未来二十年的出行需求随后通过线性规划参照类比城市数据计算影响权重给出出租车最佳数量最后引入满意度函数构建非线性规划模型借助Lingo求解油价变化前后的最优定价策略同时包含数据采集合理性分析和“共用汽车”机制构想构成从现状诊断到对策建议的完整研究链条。压缩包内为1个doc格式论文文档大小1.57MB已有56人学习下载。对于需要参考数模论文结构、模型选用与求解技巧的读者具有直接借鉴价值。1. 出租车规划问题为什么总在数学建模竞赛里出现看到“参赛编号1319”这个编号我第一反应是一篇参赛文档的归档。出租车规划问题从早年国赛B题到华为杯已经成了数学建模竞赛里一个稳定的热门方向。表面问题是“车够不够”实际要回答的是供需错配、空驶浪费、补贴效率三个问题。比赛通常给出一批打车订单、出租车GPS轨迹甚至平台派单记录要求设计资源配置方案。很多队伍第一反应就是套用复杂的深度学习或大模型结果数据清洗和评价体系反而一塌糊涂。我不会去复述查不到原文的电子版内容只讲一套任何队伍都能落地的完整链路时空网格化、需求预测、供需匹配优化、灵敏性分析最后按参赛论文的提交要求整理电子版。这套方案既适合第一次参加国赛、华为杯、五一杯的队伍也适合想用数据分析解决城市交通问题的工程师。2. 问题拆解与数据基础出租车订单数据的时空切片2.1 从订单轨迹到需求-供给矩阵拿到订单数据第一步不是建模而是把连续时空切成可计算的格子和时间段。常见做法是把城市划分成 500m × 500m 的网格时间按 15 分钟切片统计每个网格内的上车数作为需求载客车辆到达数作为供给。注意需求是乘客发起的叫车请求而供给要用出租车空载状态或订单承接状态来定义。如果只有订单数据没有轨迹状态就用“有承接的订单数”近似供给在论文里必须写清楚这个假设。下面是一段最小可运行的预处理代码import pandas as pd # 订单表字段order_id, pickup_time, dropoff_time, # pickup_lng, pickup_lat, dropoff_lng, dropoff_lat order pd.read_csv(order.csv, parse_dates[pickup_time]) # 时间按15分钟向下取整 order[time_slice] order[pickup_time].dt.floor(15min) # 简化网格ID经纬度乘10取整拼接演示用法 order[grid_id] ( (order[pickup_lng] * 10).astype(int) * 100000 (order[pickup_lat] * 10).astype(int) ) # 需求按时间和网格统计订单数 demand ( order.groupby([time_slice, grid_id]) .size() .reset_index(namedemand) ) # 供给按承接车辆数近似 supply ( order.drop_duplicates(order_id) .groupby([time_slice, grid_id]) .size() .reset_index(namesupply) )代码里floor(15min)避免了时间窗口重叠grid_id只是演示用的简化写法。实际项目我会用 Geohash 或者直接按经纬度分箱确保不同城市的坐标缩放不会失真。供给统计用了drop_duplicates(order_id)因为同一辆车可能在一单结束后立刻开始下一单按订单去重可以避免重复计数。如果你有出租车 GPS 状态字段应该过滤出“空车”状态这样供给的定义会更接近真实。2.2 构建供需指标的三个关键参数网格化之后最影响模型质量的是三个参数时间窗口、空间粒度、有效订单过滤规则。我用表格整理一下经验值方便你直接套用。参数推荐值对建模的影响时间窗口15 分钟过细会使矩阵稀疏过粗会掩盖早晚高峰的突变空间粒度500m1km市中心宜小郊区宜大按订单密度做自适应有效订单过滤行程时间 60s 或距离 100m 剔除去掉误匹配和测试订单防止异常值拉高需求我一般先做全样本统计查看每个网格的订单量分布。如果超过 30% 的网格在某时段订单数为 0说明空间粒度太细需要扩大到 1km。相反如果需求最大的几个网格已经覆盖全市 50% 以上订单就说明网格太大了。时间窗口也可以按“通勤高峰拆分”比如早高峰用 10 分钟平峰用 20 分钟但要保证同一份数据里时间片长度统一不能交替使用。提示需求和供给矩阵会非常稀疏后续建模前最好做最大最小归一化。不要直接用原始计数喂给聚类或回归模型。2.3 特征工程天气、时段、POI 密度出租车需求不只是时间和位置两个维度。雨雪天气会显著增加打车需求上下班高峰的流向也有明确规律。所以我通常会把天气、节假日和 POI 密度合并到训练集里。注意天气数据和订单数据的时间粒度不一致普通merge容易产生笛卡尔积最好用merge_asof做时间对齐。weather pd.read_csv(weather.csv, parse_dates[time]) # 将需求数据与天气按时间最近匹配 df pd.merge_asof( demand.sort_values(time_slice), weather.sort_values(time), left_ontime_slice, right_ontime, directionbackward ) df[hour] df[time_slice].dt.hour df[is_weekend] df[time_slice].dt.weekday 5 # poi_density: 每个网格内的兴趣点数量外部预聚合 df df.merge(poi_density, ongrid_id, howleft)merge_asof的directionbackward表示取订单时间之前最近一条天气记录这样不会用到未来数据。is_weekend是典型的二值特征比直接传星期几数字更稳定。POI 密度的意义在于区分商圈、车站、居民区同样的时空条件下车站附近的需求峰值会比普通居民区晚 12 个小时模型如果漏掉这个特征预测误差会集中在枢纽区域。3. 需求预测模型从 LightGBM 到时空交叉验证3.1 为什么先预测再优化出租车调度是一个动态问题只看历史当前时刻只能做“事后评估”。真正要用的方案是预测未来 3045 分钟的需求分布再把预测值作为调度模型的输入。如果需求预测偏差超过 20%后续优化模型算得再准也没有意义。所以我会把需求预测当作独立的基座单独评估调参不直接和调度目标耦合。预测分两层宏观网格预测和微观车辆分配。宏观层预测每个网格未来时间窗的订单数可以用机器学习回归微观层再把预测订单分配给具体车辆。两层分开建模的好处是宏观模型可以单独换算法比如从 LightGBM 换到 Prophet甚至图神经网络而不会影响底层的调度求解。3.2 LightGBM 基线模型LightGBM 是我在竞赛里最常用的基线训练快能处理缺失值特征交互也不用手工做。下面是一个带时序切分的标准训练流程import lightgbm as lgb features [hour, is_weekend, temp, precip, poi_density] X df[features] y df[demand] # 按时间顺序切分前80%训练后20%验证 cut int(len(df) * 0.8) train_idx df.index[:cut] val_idx df.index[cut:] model lgb.LGBMRegressor( n_estimators2000, learning_rate0.05, num_leaves31, subsample0.8, colsample_bytree0.8, random_state42 ) model.fit( X.loc[train_idx], y.loc[train_idx], eval_set[(X.loc[val_idx], y.loc[val_idx])], callbacks[lgb.early_stopping(100), lgb.log_evaluation(100)] )参数里early_stopping(100)是关键如果验证集损失连续 100 轮不下降就停止防止训练轮数过大而过拟合。num_leaves31对中小数据量比较安全数据量超过几十万行时可以适当增大到 63。subsample和colsample_bytree都是随机采样一个按行一个按列两者都开能让模型更稳定但会降低训练速度对几千行数据影响不大。提示不要用train_test_split随机划分。出租车需求有强时序相关随机划分会把同一天的相邻时间片拆进训练集和验证集得到虚高的 R²。3.3 时空交叉验证避免信息泄露要得到可信的误差估计我会用TimeSeriesSplit做时序交叉验证。它每次都保证验证集在训练集之后和真实预测场景一致。from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) scores [] for train_index, test_index in tscv.split(X): X_train X.iloc[train_index] X_val X.iloc[test_index] y_train y.iloc[train_index] y_val y.iloc[test_index] model.fit( X_train, y_train, eval_set[(X_val, y_val)], callbacks[lgb.early_stopping(100)] ) scores.append(model.score(X_val, y_val)) print(mean R2:, sum(scores) / len(scores))除了时序切分还有一种“空间留出法”按网格 ID 留出一个区域做验证测试模型泛化到新区域的能力。竞赛数据通常来自同一城市空间泛化并不总是必须但如果你要写进论文它能显著提升模型的说服力。我一般在国赛和华为杯里会同时报告“时间外推”和“空间外推”两组误差作为模型鲁棒性的证据。4. 供需匹配与空驶率优化混合整数规划加启发式4.1 单时段匹配的混合整数模型需求预测完成后我们得到某个时间段各网格的预测订单数。接下来要把这些订单分配给出租车目标函数一般是乘客等待时间和司机空驶距离的加权和。这个问题本质是任务分配可以用混合整数规划MIP建模。import pulp # I: 需求点集合J: 车辆集合 # distance[i][j]: 车辆j到需求点i的距离 # wait[i][j]: 车辆j到需求点i的预计等待时间 # 先构建候选匹配距离超过3000米直接排除 pairs [ (i, j, wait[i][j], distance[i][j]) for i in I for j in J if distance[i][j] 3000 ] prob pulp.LpProblem(taxi_dispatch, pulp.LpMinimize) x pulp.LpVariable.dicts( x, [(i, j) for i, j, _, _ in pairs], catBinary ) # 目标等待时间 0.1 * 空驶距离 prob pulp.lpSum( wait[i][j] * x[i, j] 0.1 * distance[i][j] * x[i, j] for i, j, wait_ij, dist_ij in pairs ) # 每个需求点最多被一个车辆响应 for i in I: prob pulp.lpSum( x[i, j] for j in J if (i, j) in [(p[0], p[1]) for p in pairs] ) 1 # 每辆车最多接一个需求 for j in J: prob pulp.lpSum( x[i, j] for i in I if (i, j) in [(p[0], p[1]) for p in pairs] ) 1 prob.solve()这里距离阈值 3000 米不是固定参数市中心交通拥堵时应该缩小到 1500 米。目标函数里0.1是距离权重含义是“1 分钟的等待时间等价于 0.1 公里的空驶”。这个系数不来自公式而是经验标定。你可以对比不同权重下的调度结果选指标最佳的一组作为论文参数。4.2 大规模场景用模拟退火当需求点数量超过几千MIP 求解时间会指数增长。竞赛数据往往覆盖整个城市直接解 MIP 容易超时。我一般先按需求点做 K-Means 聚类把空间上邻近的需求合并成热点再对每个热点内部用模拟退火求解。import random, math def sa_dispatch(candidates, max_iter5000, T0100, alpha0.995): # candidates: 候选车辆到需求点的匹配列表 # init_solution, evaluate, swap_two 需要根据业务实现 best init_solution(candidates) current best.copy() T T0 for it in range(max_iter): neighbor swap_two(current, candidates) delta evaluate(neighbor) - evaluate(current) if delta 0 or random.random() math.exp(-delta / T): current neighbor if evaluate(current) evaluate(best): best current.copy() T * alpha return best这段代码的核心是math.exp(-delta / T)它让算法在温度高时接受一部分更差的解避免卡在局部最优。T0设为 100表示初始可接受一个约 100 的代价增加alpha0.995表示每次迭代降温千分之五。迭代次数不是越多越好超过一定阈值后结果变化很小但耗时线性增加。下面是一个经验参数表参数推荐范围说明初始温度 T0100200根据初始解代价的 10% 量级设置降温系数 alpha0.990.999alpha 越大迭代越慢越接近全局最优最大迭代次数300010000与候选匹配数正相关建议先用小规模试跑邻域操作交换两个需求点对应的车辆比单点移动更容易跳出局部最优4.3 灵敏性分析空驶率、补贴与订单完成率调度模型跑通后评委最看重的是“你如何证明参数选得合理”。我会固定其他参数只改动补贴幅度观察空驶率和订单完成率的变化。下面是一组模拟结果不是真实比赛数据只用来展示输出格式补贴幅度空驶率下降订单完成率提升0%基线基线5%2.1%1.8%10%3.4%3.9%15%2.8%4.2%从这个模拟表能看出补贴在 10% 到 15% 之间时订单完成率仍有提升但空驶率从下降 3.4% 回落到 2.8%说明过高的补贴让司机在奖励区域空跑等待反而浪费运力。这种非单调关系是灵敏性分析里最值得写的结论。实际比赛中我还会加一个“补贴成本”列计算每提升 1% 订单完成率需要多少补贴预算这样论文的决策建议会更完整。5. 从模型到参赛论文电子版交付的五个检查点5.1 图表标题要能自解释评审翻论文时是先扫图表再决定要不要细看公式。我会把供需差距热力图放在模型摘要附近并且让每个子图的标题包含“时间窗 区域 指标”。用 matplotlib 绘图时坐标轴标注不要用demand这种变量名直接写“每 15 分钟订单量单/网格”。颜色映射优先选双色发散比如蓝到红中间白表示供需平衡评委一眼能看到缺口位置。5.2 参数表和代码版本附录参赛编号 1319 对应的电子版提交通常要求 PDF 和代码 ZIP 分开命名。我会在附录放一个参数表把 3.2 阶段的num_leaves、4.1 的距离阈值、4.2 的退火参数全部列出来并注明每个参数的含义和调试范围。这样评委如果尝试复现不需要从文本里猜。代码 ZIP 里按顺序编号1_data_preprocess.py、2_feature_engineer.py、3_train_predict.py、4_optimize.py、5_sensitivity.py每个文件开头写明输入输出路径。5.3 用 AI 辅助生成后的自查点现在很多赛区对 AI 生成内容有严格限制我的做法是AI 生成的算法描述必须用自己的语言重写一遍公式和数据量表示都统一用 LaTeX 格式。重点检查符号说明一致性比如前文用demand后文就不能突然出现need。代码里的注释也要删掉大段 AI 风格的解释只留关键参数说明。跑查重工具时最容易标红的是数据来源描述和算法步骤这部分最好写成“数据集包含 XX 条订单时间跨度 XX”这种原始事实描述不要套模板。提交前最后一遍打开导出的 PDF随机点开 5 个图表确认每个图表下方都有数据来源和参数取值。如果有多余的空行、乱码、公式编号断层立即修正。这一步比多跑一个模型更能保底。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进