ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

数学建模出租车规划问题:从数据清洗到遗传算法优化全解析

数学建模出租车规划问题:从数据清洗到遗传算法优化全解析 简介这份电子版是全国第二届部分高校研究生数模竞赛“城市交通管理中的出租车规划”参赛作品编号1319面向数学建模竞赛参赛者、城市规划与交通运输方向研究者系统呈现了从问题分析、模型构建到方案提出的完整流程。资源为1个doc文档压缩包约1.57MB内含问题重述、条件假设、符号说明、模型推导与求解、数据采集处理及附录程序说明等完整章节可直接对照学习。已有57人学习下载。方案中综合运用了阻滞增长模型Logistic、增长率法与重力模型法预测出行总量借助层次分析法建立乘坐出租车人口预测模型并构建出租车数量线性规划模型进而引入满意度函数结合非线性规划通过Lingo软件求解油价变动前后的最优定价策略。此外还基于求解结果提出了“共用汽车”的新型出租车规划机制。该文适合用于备赛参考、建模方法复盘和城市交通管理案例研讨。1. 出租车规划问题为什么是数学建模赛题的常青树出租车规划问题几乎是数学建模竞赛里最「实」的一道题。它给的数据往往是一张订单表、一组上下客点、一段时间的 GPS 轨迹考题拆开无非三件事一个城市该配多少辆出租车、车怎么调度、计价规则怎么定。难不在模型而在把模糊的供需缺口变成可计算的量纲。这篇笔记按我自己带队伍参赛和帮企业做过运力方案的经验把「数学建模-出租车规划问题电子版参赛编号1319.doc」这类题背后最常见的解题路径拆开讲一遍——从数据规范化、供需匹配到优化模型、参数标定和避坑完整到新手能照做熟手能直接抄作业。2. 先建数据底座出租车供需数据规范化与四类缺失值处理出租车规划模型做得再漂亮数据不规范一样全军覆没。绝大多数参赛队翻车不是模型不会建而是第一晚就在数据清洗上耗掉了大半时间。这里说的「规范化」不是简单去重而是把时间、空间、供需口径全部统一到同一套坐标系和粒度上让后续每个模型拿到的输入都是同一份「翻译后的语言」。2.1 数据规范化的三个必做动作统一坐标系、统一时间粒度、统一空间栅格第一个动作是统一坐标系。国内出租车订单和 GPS 数据常见三套坐标WGS-84、GCJ-02火星坐标、BD-09百度坐标。题目原始数据可能混着给也可能只给一种但描述模糊。我一般先画散点图看轨迹是否在道路网格上再抽查几条坐标去和已知道路叠加判断。如果混用用coordinate_converter或pyproj做转换不要自己写公式硬转投影参数错了后面全错。第二个动作是统一时间粒度。供需匹配分析最常用的时间片是 15 分钟或 30 分钟。以 15 分钟为例一天 24 小时切成 96 个时间片每个订单落到它所属的片里再按片聚合。选粒度有个原则粒度太细比如 5 分钟多数网格是零值模型学不到信号粒度太粗比如 1 小时早高峰、晚高峰和凌晨的差异会被抹平。15 分钟是平衡点。第三个动作是统一空间栅格。城市区域按 500m × 500m 或 1km × 1km 切网格然后给每个订单标注它所在的网格编号。网格大小直接影响样本量和计算量下面这段代码就是做这件事的。2.2 用 Python 做 OD 数据清洗与供需指标计算import pandas as pd import numpy as np df pd.read_csv(taxi_order.csv) df[pickup_time] pd.to_datetime(df[pickup_time]) df[dropoff_time] pd.to_datetime(df[dropoff_time]) # 剔除上下车时间倒挂、行程过长的异常单 df df[df[dropoff_time] df[pickup_time]] df df[(df[dropoff_time] - df[pickup_time]).dt.seconds 7200] # 时间片floor 到 15 分钟 df[time_slot] df[pickup_time].dt.floor(15min) # 空间栅格假设 lng/lat 已统一到同一坐标系 # 用城市左下角坐标做偏移落到 500m 网格分辨率约 0.005 度 lon0, lat0 113.20, 23.00 grid_size 0.005 df[grid_x] ((df[pickup_lng] - lon0) / grid_size).astype(int) df[grid_y] ((df[pickup_lat] - lat0) / grid_size).astype(int) df[grid_id] df[grid_x].astype(str) _ df[grid_y].astype(str) # 聚合供需指标每个时间片、每个网格的订单量 supply df.groupby([time_slot, grid_id]).size().reset_index(nameorder_cnt) # 车辆运力按接单司机去重统计每个时间片可服务车辆数 capacity df.groupby([time_slot, grid_id])[driver_id].nunique().reset_index(nameactive_cars) # 合并成一张宽表 merged pd.merge(supply, capacity, on[time_slot, grid_id], howouter).fillna(0)这段代码的核心逻辑是把「每一笔订单」压成「每个时间片 × 每个网格的供需统计」。time_slot用floor而不是round保证订单只会落到它真实发生的那个片不会因为跨时间边界而移位grid_size0.005在低纬度地区约等于 500 米这是经验值高纬度城市要按纬度余弦修正否则网格面积不一致。异常值过滤这里只做了两道时间倒挂和超长行程。实际数据里还有坐标越界、经纬度为 0、同一司机同一秒出现在两个地点这几种典型脏数据建议在写入time_slot之前统一再滤一遍否则某个网格会因为一条脏数据被算出一个离谱的订单峰值。2.3 供需匹配的量化口径打车等待时间、空驶率与载客率怎么选数据规整完之后要用指标描述「供」和「需」的差距。常见三个口径打车等待时间、空驶率、载客率。三者的作用和适用场景很不同选错了评委一眼就能看出来。打车等待时间是从乘客发单到司机接单的间隔能直接反映乘客体验。它的缺点是订单数据里通常只有发单和接单时间缺少取消单所以等待时间会偏乐观。空驶率是空驶里程占总行驶里程的比例能反映司机端效率但题目数据给不到连续轨迹时很难算准。载客率是载客里程与总里程之比适合宏观判断运力余量但对网格级调度不够敏感。我一般把载客率作为主指标把等待时间作为验证指标空驶率作为辅助指标。主指标用来驱动后续优化模型的目标函数辅助指标用来做结果合理性校验。这样组合的好处是载客率数据容易算、业务含义清晰而且它天然和「车辆规模该增该减」挂钩等待时间则用来回答「这个方案对乘客友不友好」。两者的权重可以在灵敏度分析阶段再调。3. 出租车规划问题核心模型从回归基准到遗传算法与网格调度数据底座搭好后进入建模阶段。这里最容易犯的错是一上来就上复杂模型结果参数调不出来、跑出来的结果还不如平均分配。我的习惯是先建一个简单可解释的基准模型再逐步加复杂度每一步都在上一版基础上验证增益。3.1 为什么先跑线性回归再上启发式算法基线模型的价值出租车需求预测和规模配置的第一步适合用一个多元线性回归或者带正则项的回归模型打底。原因是题目给的因变量订单量、载客率与自变量时间片、区域属性、天气、节假日之间有明显的线性相关性回归能快速告诉你每个特征的系数方向对不对。比如夜间订单量和人口密度负相关、与商业区距离正相关这个方向错了后面的复杂模型大概率也不对。基线模型的另一个价值是给优化模型提供「比较基准」。规划方案好不好不是看目标函数值多小而是看比你最简单的基准方案好多少。我见过很多队伍直接跑遗传算法把最优解算出来了但没法回答「这个解为什么比现状好、好多少」评委一问就露馅。from sklearn.linear_model import Ridge from sklearn.model_selection import train_test_split # features: 时间片序号、是否工作日、是否高峰、网格内 POI 数量、天气编码 X merged[[time_slot_num, is_weekday, is_rush, poi_cnt, weather_code]] y merged[order_cnt] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) model Ridge(alpha1.0) model.fit(X_train, y_train) print(R2:, model.score(X_test, y_test)) print(coef:, dict(zip(X.columns, model.coef_)))alpha1.0是正则化强度控制模型不要过度拟合训练集里的噪声。如果系数出现符号与业务直觉相反先不要怀疑业务先回去查特征之间是不是有多重共线性。random_state42固定了训练测试集划分方便复现比赛文档里要写清楚这个参数这是评委看可复现性的一个点。3.2 基于遗传算法的出租车规模配置目标函数与约束规模配置问题的本质是在满足一定服务水平的前提下求最小车辆数。目标函数不能只写「车辆数最少」否则解出来是 0 辆车——必须把供需差、等待时间作为惩罚项写进去。常见做法是构造一个综合成本函数系数 λ 用来平衡车辆成本和乘客等待成本。遗传算法在这个问题上比穷举法和梯度下降都合适因为目标函数非凸、约束离散车辆数是整数、可行域大。下面给一个标准化框架目标函数按你的数据替换即可。import numpy as np # 决策变量每个时段每个网格的车辆数长度 时间片数 x 网格数 # 这里先用一个简化版本把全天的车辆配置压缩为每个网格一个常数 def objective(x, demand, cost_per_car100.0, lambda_wait0.5): # x: 每个网格的配置车辆数 # 供给 车辆数 x 平均服务次数 supply x * avg_trips_per_car # 缺口惩罚需求和供给的差距 shortage np.maximum(0, demand - supply) wait_cost lambda_wait * shortage.sum() car_cost cost_per_car * x.sum() return car_cost wait_cost def mutation(offspring, rate0.1): for i in range(len(offspring)): if np.random.rand() rate: offspring[i] np.random.randint(-2, 3) offspring[i] max(0, offspring[i]) return offspring # 遗传算法主循环 pop_size, n_genes 50, n_grids pop np.random.randint(0, 20, size(pop_size, n_genes)).astype(float) for gen in range(200): fitness np.array([objective(individual, demand) for individual in pop]) # 锦标赛选择 selected [] for _ in range(pop_size): idx np.random.choice(pop_size, 3, replaceFalse) winner idx[np.argmin(fitness[idx])] selected.append(pop[winner]) # 均匀交叉 offspring [] for i in range(0, pop_size, 2): mask np.random.randint(0, 2, sizen_genes).astype(bool) child1 np.where(mask, selected[i], selected[i1]) child2 np.where(mask, selected[i1], selected[i]) offspring.extend([mutation(child1), mutation(child2)]) pop np.array(offspring)这段代码把目标函数拆成两部分车辆持有成本和需求缺口惩罚。lambda_wait是这个模型里最关键的系数它代表「多等一分钟乘客」等价于「多花多少钱」。这个系数没有标准答案需要结合题目给的背景数据或行业经验来定取值太小模型会趋向少配车、等得久取值太大趋向多配车、空驶高。建议做 45 组对比实验展示给评委。3.3 网格化调度模型把分区与排队论结合起来规模配置回答「多少辆车」调度回答「车往哪去」。网格化调度把城市划分成多个区域每个区域内有需求和在途车辆跨区域的车辆移动会产生成本模型要求在满足区域需求的前提下最小化总空驶里程。常见做法是把每个网格视为一个排队系统订单到达率 λ 由历史数据算出来服务率 μ 由平均单程时长算出来那么网格的排队强度 ρ λ / μ。ρ 大于 1 的网格是供不应求的「热区」ρ 小于 0.5 的是运力浪费区。调度策略就是让车辆从 ρ 低的网格向 ρ 高的网格空驶移动但空驶本身有成本所以这是一个带约束的分配问题可以用线性规划或贪婪算法先求一个解再用模拟退火微调。这里有个容易被忽略的点跨网格调度的前提是网格不能切得太碎。500 米网格適合描述供需但不適合做调度因为一个路口堵车就会让一个网格瞬间失真。做调度时我一般把网格合并成 1.5km ~ 2km 的片区让调度决策落在片区之间网格只用来做需求热力展示。这个「展示用细粒度、决策用粗粒度」的分层思路在赛题文档里写清楚会很加分。4. 参数标定与灵敏度分析让模型结果经得起追问模型能把结果跑出来只是第一步。评委和实际项目方真正关心的是你的参数是拍脑袋定的还是标定出来的参数小幅度变化会不会导致结论翻盘这一章解决的就是这两件事。4.1 必调的五类关键参数及其初始标定方法出租车规划模型里参数可以分为五类需求预测参数、车辆配置参数、调度权重参数、计价参数、服务水平约束参数。下面这张表给出每类的典型参数、初始取值建议和标定方法。参数类别典型参数初始取值标定方法需求预测时间片长度15 分钟对预测误差做多粒度对比需求预测网格大小500 m看供需比空间分布是否平滑车辆配置每车日均服务次数25~35 单用载客率反推调度权重空驶惩罚系数0.3~0.5对比多次调度仿真结果服务水平最大可接受等待时间10 分钟参考行业标准或题目说明计价参数起步价、单价按当地标准直接采用题目给定值初始取值不要追求「正确」追求「合理且可解释」。比如每车日均服务次数用题目数据总订单量除以估计的在营车辆数就能算出一个初值这个初值比任何专家经验都更有说服力因为它是从数据里长出来的。4.2 灵敏度分析的三条路线单因子扫描、蒙特卡洛模拟、场景对比灵敏度分析是判断模型稳不稳的核心手段三种做法各有适用场景。单因子扫描最简单固定其他参数让一个参数在 ±20% 或 ±30% 范围内变化观察目标函数和最优解的变化幅度。如果某个参数变化 10% 导致车辆配置数变化超过 30%说明这个参数过于敏感需要在论文里重点讨论它的标定依据。蒙特卡洛模拟适合参数间存在相关性的情况。比如载客率和等待时间不是独立的两者都受天气、事件影响。做法是对每个关键参数按分布抽样比如正态分布或三角分布跑 200~500 次模型统计结果的中位数和 90% 置信区间。这样得到的结论不是「最优配置是 8000 辆」而是「最优配置有 90% 概率落在 7500~8600 辆」。评委更愿意看到后者。场景对比适合回答「节假日 vs 工作日」「晴天 vs 雨天」这类结构化问题。做法是对每个场景单独标定参数、单独跑模型、单独出结果然后横向对比差异并解释原因。这条路线最稳因为它不需要额外建模只需要把数据按场景切分重跑一遍。# 蒙特卡洛灵敏度分析框架 results [] for trial in range(200): lambda_wait np.random.triangular(0.3, 0.5, 0.8) cost_per_car np.random.normal(100, 10) demand_noise 1 np.random.normal(0, 0.05) sol run_ga(demand * demand_noise, cost_per_car, lambda_wait) results.append(sol) p10, p50, p90 np.percentile(results, [10, 50, 90]) print(f车辆数中位数: {p50}, 90% 区间: ({p10:.0f}, {p90:.0f}))run_ga是上一章的遗传算法主函数这里把它当成黑匣子调用每次给它不同的参数组合。注意demand_noise是对需求整体加噪声模拟预测误差如果你手头有分时段的需求误差数据可以按时间段分开加噪声结论会更精细。4.3 用残差图和置信区间判断模型选对了没有灵敏度分析解决参数问题残差分析解决模型结构问题。线性回归或任何预测模型的残差如果出现明显模式比如白天残差为正、夜晚残差为负说明缺了一个时间相关的特征这时候调参已经无效得加特征或换模型。画残差图时横轴用预测值纵轴用实际值减预测值。理想状态是残差点均匀分布在零线两侧没有喇叭口、没有弯曲。如果残差随预测值增大而发散说明模型在高峰时段系统性低估这时候可以考虑加入交互项比如「高峰 × 商业区」这种组合特征。置信区间方面用 bootstrap 重抽样 1000 次得到车辆配置数的分布直接写进文档比用理论区间公式更直观也更容易让非数学背景的读者理解。5. 出租车规划问题的 5 个翻车点数据泄漏、样本失衡与黑匣子调参这一章是从真实比赛和项目里踩坑踩出来的每一条都对应过一个队伍通宵返工。写出来让你绕开。5.1 数据泄漏用未来数据预测过去现象模型在训练集上表现极好测试集上一塌糊涂。 原因在构造特征时把不该出现在预测时刻的数据当成了输入。最常见的是用全天订单均值去预测早高峰某个网格的订单量——这个均值包含了早高峰本身的信息属于典型的数据泄漏。 解决所有特征必须在预测时刻之前可以取得。时间类特征只用「过去 N 个时间片」的滑动窗口不用全天聚合值。写代码时把特征构造和模型训练分成两个函数人为在中间加一道「时间截止」检查。5.2 昼夜样本失衡导致模型只管白天现象模型对凌晨时段的预测误差超过 100%但整体误差看着还行。 原因凌晨订单量低只占总样本的 5% 以下模型为了最小化整体误差会主动忽略凌晨。 解决分时段建模或者对训练样本按时段加权。我一般直接按「早高峰、日间平峰、晚高峰、夜间」四段分别训练模型每段单独报告误差而不是只报一个整体 R²。分段之后调参也更有针对性夜间模型的网格大小甚至可以调大因为订单量少细网格全是零值。5.3 网格大小一刀切导致热区失真现象同样的模型网格从 300m 换成 1000m最优车辆数差了 40%。 原因城市区域疏密不均商业区 300 米内订单量巨大郊区 3 公里都没一单。用一个固定网格尺寸必然顾此失彼。 解决先用 500m 网格跑一遍看网格订单量的分布。如果长尾严重改用自适应网格订单密度高的区域自动切小郊区自动合并。实现上可以用sklearn.cluster.KMeans对坐标做聚类按聚类结果生成不规则分区。分区效果要可视化检查网格边界不能切穿明显的地理屏障比如江、山体。5.4 目标函数量纲不统一导致 λ 调不出来现象遗传算法调了十几组 λ结果不是 0 辆车就是 50000 辆车中间没有平滑过渡。 原因车辆成本和等待成本量纲差距太大比如一辆车日均成本几百元一次等待惩罚却不到一元λ 要从 0.001 级别开始扫扫到 100 才有变化。 解决进入优化前先做量纲归一。把两个成本项分别除以各自的基准值比如车辆成本除以总车辆数预算、等待成本除以最大可接受等待时间。归一化后 λ 的搜索范围会落在 0.1~10 之间更容易收敛也更方便做灵敏度分析。5.5 黑匣子调参遗传算法跑了 2 小时不知道好坏现象每代的目标函数值时好时坏跑了 200 代看不到收敛趋势只能干等。 原因种群初始化范围太大、变异率太高或交叉后没有保证子代可行性。最常见的是决策变量是车辆数但变异算子生成了负数被钳到 0 之后种群多样性迅速丢失。 解决在遗传算法循环中打印每一代的最优值和均值格式很简单——gen0 best1234 mean2345。如果 best 在 50 代内不再显著下降就提前终止不用跑满预设代数。另外变异步长从randint(-2, 3)开始等后期收敛后再换成randint(-1, 2)这个「粗调再细调」的技巧能省一半时间。6. 用可视化验证模型结果四张图让评阅人 30 秒读懂你的方案模型和参数都到位了最后一步是把结果变成一张评审人扫一眼就能看懂的图。这里分享我每次都要画的四张图以及背后的验证逻辑。6.1 供需热力图与缺口分布图验证网格划分是否合理第一张图是分时段供需热力图横轴是时间片纵轴是网格 ID颜色表示供需比订单量/车辆数。这张图能一眼看出哪些网格在哪些时段是红的供不应求、哪些是蓝的运力过剩。如果热力图出现棋盘格一样的明暗交替大概率是网格划分问题回头调网格大小或偏移量。第二张图是缺口分布地图把供需比大于 1.5 的网格标红、小于 0.5 的标蓝叠加到城市底图上。验证标准是红色区块应该集中在 CBD、交通枢纽、商圈蓝色区块应该在郊区。如果红色出现在荒郊野岭说明数据里有脏点或者网格坐标换算有问题。6.2 收敛曲线与多方案对比柱状图证明优化过程是可信的遗传算法这类启发式算法评阅人最担心的就是「你运气好跑了一个好结果」。收敛曲线能证明不是运气横轴是迭代代数纵轴是目标函数值画出每一代最优值和种群平均值两条线。两条线都平滑下降并趋于水平这个结果才算可信平均值下降但最优值不动说明种群多样性丢失需要调高变异率。多方案对比柱状图用来回答「为什么不选另一组参数」。每个柱子是一个方案柱高是综合成本分成车辆成本和等待成本两段堆叠。评阅人能直接看到方案 A 车少但等待成本高方案 B 车多但等待成本低你选的方案是两者平衡的交叉点。这张图比任何文字解释都有说服力。6.3 用历史数据回测把你的方案放进「过去」检验一次最后的验证手段是回测。把模型应用到历史数据的前 80% 上算出最优车辆配置和调度策略然后扔到后 20% 的数据里看实际表现计算供需缺口、平均等待时间、空驶率三个指标和后 20% 的现实情况做对比。如果模型方案在回测期比现实情况好不到 5%有两种可能一是模型没学到东西二是现实已经接近最优。回测是会暴露问题的最典型的就是模型在训练期过拟合了某个特殊事件比如节假日大促、暴雨天。这时候不用慌把事件对应的时间段从回测样本里剔掉重跑一遍并在文档里说明剔除了哪些异常时段、为什么剔除。主动披露这个处理过程比被评阅人追问再承认要主动得多。把这些图排版进 DOC 格式的电子版参赛文档时记住一个原则每个图下面必须配 2~3 句解析写清楚「图中哪个结构对应哪个问题、为什么这个结构合理、如果数据变化了图会怎么变」。图是论据不是装饰品。我自己带队伍时最后一天晚上只做两件事跑一遍完整回测、检查每张图下方的解析是否说人话。模型可以不出彩但验证链条必须完整评阅人挑不出逻辑漏洞奖项就稳了一半。希望这些经验能帮你在下次拿到出租车规划问题时少熬两个通宵。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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