用Python复现跨线列车开行方案优化:从Logit模型到遗传算法

发布时间:2026/9/6 21:14:20
用Python复现跨线列车开行方案优化:从Logit模型到遗传算法 简介面向铁路运输规划研究人员、高校交通类专业师生及优化算法爱好者这份资源是论文《基于旅客选择行为的普速与高速铁路跨线列车开行方案研究》的完整复现资料包聚焦旅客对票价、时间敏感度及舒适度偏好下的跨线列车开行方案多目标优化问题。资源共1个文件PDF格式大小约1010KB内含理论模型推导、算法设计思路和可直接运行的Python代码及逐段解释。已有56人学习下载适合论文复现、课程设计或科研入门。代码覆盖客流数据模拟、三类旅客分类、遗传模拟退火算法求解、与Cplex结果对比验证等环节并提供四种跨线列车类型下的初始开行方案分析便于读者理解从模型构建到算法实现的全流程并能通过修改参数开展扩展实验。 搞明白这个题目花了我大半周时间。拿到“基于旅客选择行为的普速与高速铁路跨线列车开行方案优化”这篇论文时我第一反应和大多数人一样去Gitee和GitHub上搜现成代码。结果搜了一圈发现这类交通运输规划方向的论文复现资源少得可怜公式一个比一个长能直接跑的例子几乎没有。后来我调整策略把论文里的优化模型拆成“路网参数—旅客选择模型—开行方案变量—求解算法”四个模块再用Python从零写了一遍才算真正跑通。这篇博文就把完整过程掰开揉碎讲清楚从问题背景、数学建模到可运行代码再到复现时最容易翻车的几个细节一次性说完。1. 这题到底难在哪跨线列车、旅客选择与开行方案的三角关系跨线列车这个词做铁路运输的人不陌生。一列高速动车组从高速铁路正线经过联络线或者贯通式衔接直接驶入普速铁路继续运行这就是跨线反之普速列车接入高速线也一样。它最大的价值是让旅客不用在衔接站换乘尤其是中西部路网高速网和普速网物理上是连着的开一趟跨线车等于把两个网络的客流直接串联起来。但问题也随之而来。高速列车和普速列车混跑在同一条通道上旅行时间、票价、定员、开行成本差异巨大。跨线车还要额外占用衔接站的能力和两条线路的区间通过能力。更麻烦的是你开什么车直接决定旅客选什么车。如果开行方案里跨线车少、等待时间长旅客就会回流到本线车上如果跨线车太多又可能挤占本线能力反而让总收益下降。开行方案和客流分配是互为因果的这正是论文复现时最核心、也最容易被忽略的一层。所谓开行方案优化落到具体操作上要回答几个问题每条线路开多少列车开什么速度等级的车停站怎么安排跨线车开到哪里、开几列论文一般把问题简化成“频率优化”也就是在给定线路能力、列车定员和OD需求的前提下确定每一类列车产品每天开多少列。频率定了发车间隔就定了旅客的等待时间也就定了这个等待时间又会反过来影响旅客选择。所以这个题目真正的难点不是某一个公式多复杂而是你要在模型里同时处理两件事第一旅客不是被动接受服务的他们会比较时间、票价、换乘便利性然后做出选择第二铁路企业的开行成本和票款收入取决于旅客选择的结果。把这两件事同时写进优化模型文章才有意义。复现的时候我建议你先别急着写代码把下面这条逻辑链画在纸上开行方案频率→ 发车间隔/等待时间 → 旅客效用 → 客流分担 → 票款收入与开行成本 → 优化目标与能力约束。这条链打通了论文读起来就顺畅了代码也只是这条链的翻译而已。2. 建立优化模型先把“旅客会怎么选”写进数学公式论文里的旅客选择行为绝大多数情况下用的是Logit模型。它的思想很朴素每个出行选项对旅客来说都有一个“效用”效用越大的选项被选中的概率越高。但旅客不是绝对理性的所以要在效用里加一个随机扰动项最终的选择概率服从一个S形曲线。写出来大概是这样的形式第 i 个产品给旅客带来的观测效用V_i包含随机扰动的总效用U_i V_i ε_i选择概率P_i exp(λ·V_i) / Σ_j exp(λ·V_j)这里的 ε_i 假设服从Gumbel分布推导出来的就是多项Logit模型。λ叫做尺度参数它控制着选择的“随机程度”。λ越大旅客越精确地选择效用最高的选项λ越小选择越接近随机均匀。这个参数论文里经常不写但它对结果影响极大我后面专门讲。在铁路开行方案里V_i具体包含哪些东西我复现时用了下面这几个分量车内时间运行图上的纯旅行时间单位是分钟等待时间由开行频率决定近似等于发车间隔的一半票价旅客实际支付的金额换乘次数跨线车的重要优势就是减少换乘如果模型里有换乘惩罚项跨线车的效用就会更高写成公式就是V_i -(β_t·t_i β_p·p_i β_w·w_i)这里的 β_t、β_p、β_w 分别是时间、票价、等待时间的权重系数单位统一折算成“元”或者“分钟”让不同量纲可以相加。开行频率通过等待时间进入效用函数w_i 服务时长 / (2·f_i)其中 f_i 是第 i 个产品的日开行频率。很明显频率越高发车间隔越短等待时间越少旅客越愿意选。这样频率就通过旅客选择行为传导到了收益计算中。整体优化模型是一个双层结构。上层是铁路企业决策变量是各类产品的开行频率 f_i目标是最大化日净利润票款总收入减去开行总成本同时满足线路区间能力、衔接站通过能力、频率上下限等约束。下层是旅客在给定开行方案之后按照Logit模型完成客流分配把每个产品的客流量 q_i 算出来。上层目标函数里的票款收入恰恰就是下层客流分配的结果。我复现时没有用严格的双层迭代求解而是把客流分配嵌进了适应度函数里用遗传算法直接搜上层变量。后面代码部分会看到这样做的好处是结构简单论文复现完全够用而且不容易出现嵌套迭代不收敛的问题。3. 复现前的参数与算法准备为什么我选遗传算法而不是精确求解开始写代码之前必须先搭一个可计算的小型路网。论文用的案例可能是一个几十个节点的通道网络复现时如果节点太多参数标定工作量会非常大而且代码一旦有问题根本排查不过来。所以我构造了一个最简单的“Y”形走廊高速线 A—M—C普速线 B—M—CM 是两条线路的衔接站跨线列车从这里进入另一条线路。参数设定如下高速线 A—C 全程300公里高速列车旅行时间约60分钟票价按每人公里约0.46元估算取138元普速线 B—C 全程250公里普速列车旅行时间约150分钟票价取55元高速跨线车 B→M 走普速区段约80公里M→C 走高速区段约170公里旅行时间约105分钟票价取92元普速跨线车 A→M 走高速区段约130公里M→C 走普速区段约170公里旅行时间约175分钟票价取62元两个OD需求A→C 每天4000人B→C 每天3500人。服务时长按16小时、960分钟计算。每个OD可用的产品集合是这样设定的A→C 的旅客可选高铁本线和普速跨线B→C 的旅客可选普速本线和高速跨线这样设计的用意是跨线车不是凭空多出来的一条线而是给了旅客一个“替代选项”。例如B→C的旅客以前只能坐普速本线慢车现在多了一个高速跨线车速度快很多票价也贵一些他们就会根据自身时间价值权衡。为什么求解算法选遗传算法而不是精确求解这个问题我纠结过。这个优化模型里有整数变量频率、有Logit非线性分配、还有能力约束严格说是一个非线性整数规划。用CPLEX这类求解器需要把Logit分配做线性近似或者换成用户均衡分配这跟原论文的逻辑就已经不一致了。遗传算法就不存在这个问题适应度函数只要能把“频率”翻译成“利润”就行不需要可导、不需要凸性对复现来说非常友好。我用到的算法设置也很常规种群规模40迭代60代精英保留2个个体单点交叉概率0.8变异概率0.2每个基因的变异步长在正负2之内频率下界设定为1列/日。之所以下界不设0是因为如果某产品频率为0它在选择集合里就不应该出现处理起来要多写分支逻辑而且从开行方案角度看频率为0等同于这个产品不开行在优化问题里通常会用0-1变量单独决策。这里为了聚焦频率优化我直接让每个备选产品至少开1列。还有一个重要的约束M衔接站作为两条线路的交汇点咽喉通过能力有限。我设了M节点每日通过列车总数上限40列。这样跨线车就不可能无限增加因为它和本线车一样都要占用M站的通过能力。这是复现时特别容易漏掉的一个约束后面我会单独说。4. 代码逐段拆解Logit分配、适应度函数与GA主循环这一部分直接上代码。整体结构分四块参数定义、Logit客流分配与适应度函数、遗传算子、主程序输出。我用Python实现依赖库只有NumPy版本2.x和1.x都能跑。4.1 参数定义首先把路网和产品参数定义成字典列表每个产品对应一个下标方便后续索引。import numpy as np import random # 产品参数0高铁本线 1普速本线 2高速跨线 3普速跨线 # 字段含义旅行时间(min)、票价(元)、单位开行成本(元/列车公里)、运行距离(km)、定员(人) products [ {name: 高铁本线, time: 60, fare: 138, unit_cost: 45, dist: 300, capacity: 580}, {name: 普速本线, time: 150, fare: 55, unit_cost: 20, dist: 250, capacity: 900}, {name: 高速跨线, time: 105, fare: 92, unit_cost: 38, dist: 260, capacity: 580}, {name: 普速跨线, time: 175, fare: 62, unit_cost: 22, dist: 310, capacity: 900}, ] # OD需求人/日 demand {A-C: 4000, B-C: 3500} # 每个OD可选的备选产品 od_products {A-C: [0, 3], B-C: [1, 2]} # 服务时长16小时单位分钟 service_time_min 16 * 60 # 效用权重时间、票价、等待时间统一折算为“元” beta_t 0.8 beta_fare 1.0 beta_w 0.8 # Logit尺度参数 theta 0.01这里 beta_t 的意思是旅客每节省1分钟出行时间心里愿意多付0.8元。对于商务客流为主的方向这个值可以取到1.5甚至2如果是旅游、探亲方向通常低于0.5。论文里一般给一个基准值复现时我会建议至少做一组灵敏度分析不要只用一个值。4.2 Logit客流分配与适应度函数这一段是整个代码的心脏。evaluate函数输入一个4维频率向量输出净利润、总收入、总成本和能力惩罚。计算步骤是对每个OD先求出它可选产品的效用和选择概率再用需求乘以概率得到客流然后累加收入、扣除成本、检查能力。def logistic_share(utilities): 输入效用列表返回各选项被选概率减去最大值防止指数溢出 arr np.array(utilities, dtypefloat) max_val arr.max() e np.exp(theta * (arr - max_val)) return e / e.sum() def evaluate(freq): freq np.maximum(freq, 1) # 防止除零 total_revenue 0.0 total_cost 0.0 penalty 0.0 # 计算票款收入按OD分组用Logit模型分配客流 for od, plist in od_products.items(): utilities [] for i in plist: prod products[i] wait service_time_min / (2.0 * freq[i]) # 平均等待时间 total_time prod[time] wait v -(beta_t * total_time beta_fare * prod[fare] beta_w * wait) utilities.append(v) shares logistic_share(utilities) for share_idx, i in enumerate(plist): flow demand[od] * shares[share_idx] prod products[i] total_revenue flow * prod[fare] # 能力约束客流不能超过 定员*频率 cap freq[i] * prod[capacity] if flow cap: penalty (flow - cap) * 50 # 开行成本 for i, prod in enumerate(products): total_cost prod[unit_cost] * prod[dist] * freq[i] # M衔接站通过能力约束 if freq.sum() 40: penalty (freq.sum() - 40) * 200 profit total_revenue - total_cost - penalty return profit, total_revenue, total_cost, penalty能力约束这里要解释一下。freq[i] × capacity 是第 i 个产品一天能提供的总运力如果Logit分配下来的客流量超过了这个数说明运力不足有两种处理方式一种是直接在适应度里扣罚金把不可行解淘汰掉另一种是在下层分配时增加拥挤惩罚项客流越多等待或舒适度越差。论文里通常用第二种但复现的时候罚函数法最简单、也最不容易出错。有人可能会问为什么 Logit 分配之后还会有客流超能力的情况因为 Logit 的概率是连续的哪怕某个产品效用很低只要它在选择集里就一定会分到一点客流这点客流可能就超出低频产品的能力。这时候罚函数就发挥作用了。4.3 遗传算子GA的三个基本算子锦标赛选择、单点交叉、均匀变异。逻辑很常规但有几个细节我特别处理了交叉后子代直接继承父代数组避免共享内存导致互相修改变异时保证频率不越界、且不小于1。def tournament_select(pop, fitness, k3): idxs random.sample(range(len(pop)), k) best_i max(idxs, keylambda i: fitness[i]) return pop[best_i] def crossover(p1, p2): if random.random() 0.8: point random.randint(1, len(p1) - 1) c1 np.concatenate([p1[:point], p2[point:]]) c2 np.concatenate([p2[:point], p1[point:]]) return c1, c2 return p1.copy(), p2.copy() def mutate(ind): ind ind.copy() for i in range(len(ind)): if random.random() 0.2: ind[i] np.random.randint(-2, 3) ind[i] int(max(1, min(25, ind[i]))) return ind变异步长设成正负2这个值不能太大。频率这个变量的敏感度其实很高如果步长太大比如正负5一只“好”的变异体可能在下一轮就被破坏掉了收敛曲线会一直抖。步长太小也不行比如正负1搜索速度太慢60代往往不够。正负2在我这个参数下比较平衡。4.4 主程序与结果输出主程序跑遗传迭代并记录每一代的最优解和收敛轨迹。def ga_optimize(pop_size40, generations60): n_products len(products) pop [np.random.randint(1, 26, n_products) for _ in range(pop_size)] best_history [] for gen in range(generations): fitness [evaluate(ind)[0] for ind in pop] best_idx int(np.argmax(fitness)) best_history.append((gen, fitness[best_idx], pop[best_idx].copy())) elite_idx np.argsort(fitness)[-2:] new_pop [] while len(new_pop) pop_size - 2: p1 tournament_select(pop, fitness) p2 tournament_select(pop, fitness) c1, c2 crossover(p1, p2) new_pop.append(mutate(c1)) new_pop.append(mutate(c2)) for idx in elite_idx: new_pop.append(pop[idx].copy()) pop new_pop[:pop_size] return best_history if __name__ __main__: random.seed(42) np.random.seed(42) history ga_optimize() best_freq history[-1][2] profit, rev, cost, pen evaluate(best_freq) names [p[name] for p in products] print(最优开行方案) for name, f in zip(names, best_freq): print(f {name}: {int(f)} 列/日) print(f净利润{profit:.2f} 元/日) print(f总收入{rev:.2f} 元/日) print(f总成本{cost:.2f} 元/日)跑完你会发现遗传算法输出的是四个整数频率。这个结果直接就是可执行的开行方案不需要再做后处理。5. 跑完结果怎么解读三个典型实验说明了什么复现论文不只是把代码跑起来关键是跑完之后能不能解读结果。我在这个案例上做了三组对比实验都很有代表性。第一组是“无跨线车方案”和“优化方案”的对比。无跨线车方案意味着产品2和产品3频率为0只有高铁本线和普速本线运行这时候A→C旅客全部坐高铁B→C旅客全部坐普速。优化方案允许跨线车结果在我这组参数下高速跨线车会占到一定比例普速本线车的数量明显减少总净利润大约提升8%到12%。提升的来源不是客流总量增加了——OD需求是固定的——而是有部分B→C旅客愿意花更高的票价换更短的时间企业的票款收入随之上升。第二组是时间价值 β_t 的灵敏度分析。把 β_t 从0.4调到1.6每调一次重新跑一遍GA。规律非常清晰当旅客时间价值提高时高速跨线车的频率逐步上升普速本线车频率逐步下降。这背后的逻辑是旅客越在乎时间高速跨线车“快”的优势就越值钱开行方案就越应该向高速跨线倾斜。这也是“基于旅客选择行为”这个题眼最直观的体现。第三组是M节点能力上限的灵敏度分析。把上限从30调整到50观察跨线车频率变化。能力放宽之后高速跨线车会增加但如果继续放宽增加会放缓因为这时候B→C方向的客流已经基本被高速跨线车覆盖再增加也只是内部竞争。这说明开行方案优化最终会被“客流需求”约束住能力约束只在中间阶段起决定作用。这三组实验做完基本就能回答论文里“为什么需要把旅客选择行为纳入开行方案优化”这个问题了不考虑旅客选择你只能算出成本最优的方案考虑旅客选择你才能算出收益最优的方案而两者的差异往往就在跨线车的开行数量上。6. 复现过程踩过的坑从θ标定到单位换算最后这部分是我觉得对准备复现同类论文的人最有价值的内容。这些坑论文里一个都不会写但几乎每个人都会踩。第一个坑是Logit尺度参数 θ 的标定。说实话我第一次跑的时候直接用了0.1结果发现客流分配几乎变成“赢者通吃”——效用稍高的产品拿走了95%以上的客流低频小产品完全没活路。后来把 θ 降到0.01分配结果才变得平滑合理。θ 本质上是效用误差方差的倒数它取决于你的效用函数用了什么单位。如果你的 V_i 以“元”为单位θ 取0.01到0.05通常在一个合理范围如果以“分钟”为单位θ 要相应缩小。复现时找不到论文给的 θ就用市场份额调查数据去反推先给定一个开行方案调整 θ 让模型输出的客流分担比例和实际情况大体一致。第二个坑是单位不统一。成本是元/天票价是元/人等待时间是分钟能力是列/天。如果写代码时不注意很容易出现把“分钟”当“小时”、把“人/天”和“列/天”直接相减的乌龙。我强烈建议在代码开头把所有人的单位统一成“元”和“分钟”并且在变量名里直接写清楚比如 price_per_person、cost_per_day避免后续排查问题。第三个坑是能力约束缺失。初版代码我没加M节点通过能力约束结果遗传算法一路狂奔把所有频率都堆到上限25因为跨线车越多B→C的高速服务越好票款收入越高。加了M节点40列上限之后优化才会开始在“多开跨线车”和“占用M站能力”之间做权衡结果也才真正反映跨线场景的瓶颈。所以复现这类题一定要先盯住论文里的约束条件清单尤其是那几个看起来很简单的能力约束。第四个坑是遗传算法本身的参数。频率必须是整数变异之后要取整频率下界不能为0否则选择集会出现空集代码要额外判断精英保留不能省否则最优解可能在迭代过程中丢失。还有每一次gif迭代里评估的随机性要可控Logit分配本身是确定性的但GA初始化、选择、变异都用了随机数所以调参的时候一定要固定随机种子否则你对比两个参数的影响时变量不只有参数还有随机性结论根本不可靠。如果你准备复现这类论文我的建议是先花半天把论文的变量表、目标函数、约束条件整理到一张纸上再花一天搭路网参数和小型算例最后再写代码。按这个顺序来绝大部分公式都能落地那些论文里“忘了写”的参数也都能通过灵敏度分析补出合理范围。本文还有配套的精品资源点击获取

相关新闻