拓冰建站拓冰建站
首页 / 资讯中心 / 正文

从数据清洗到蒙特卡洛:2021年国赛C题供应链优化全解析

简介面向数学建模竞赛参赛者及希望系统提升建模能力的高年级本科生这份2021年C题资料包以完整算法代码为核心涵盖从题目解读、模型构建、数值求解、验证评估到结果分析的完整链条非常适合备赛复盘与专项突破。包内共27个文件含13个MATLAB脚本.m、10个MATLAB数据文件.mat及4个Excel表格.xlsx。脚本对应各小题的具体求解逻辑数据文件用于保存中间计算结果Excel表格则整理原始数据、订购及转运方案等关键信息整体压缩包约996KB目录结构简洁便于按需查阅。目前已有2303人学习下载。资料提供可直接运行的MATLAB代码和配套数据读者可对照输出文件复盘每一步建模决策理解数学理论如何落地为可计算模型对备赛冲刺和日常能力提升均有较高参考价值。 2021年高教社杯的C题《生产企业原材料的订购与运输》是我见过最像真实业务题的国赛题也是当时那一届做完之后大家吐槽最少的一道题。它不像A题那么吃物理直觉也不像B题那样拼化学机理而是把“数据清洗、评价建模、整数规划、不确定性分析”串成一条完整链路几乎等于用三天时间模拟了一次真实的供应链决策。这篇文章我就拿2021年C题为底把我自己从读题到出结果的完整套路拆开讲把当年写的代码逻辑和踩过的坑一并交代清楚给准备国赛、想系统过一遍“数据优化代码”流程的同学做个参考。1. 题目拆解与整体思路1.1 题目到底在说什么先把这个题的业务背景说透。题目里有一家生产制造企业每周要消耗固定量的原材料原材料不能凭空出现要从402家供应商那里采购采购完之后再委托8家转运商把原材料运到厂里。运输过程有损耗每家转运商的损耗率还不一样供应商的供货也不稳定经常这周多、下周少甚至连续几周不供货。这就产生了几个很现实的问题402家供应商太多了没法全都合作得选一批重点供应商选完供应商之后每周给谁下多少单、让哪家转运商运才能又省钱又少损耗原材料短缺会停产所以还得想办法降低缺货风险。整道题的核心矛盾就是在“成本、稳定性、供货保障”三者之间找一个平衡点。国赛题目一般很少这么贴近企业真实采购场景所以这道题对建模能力的要求反而不高对数据处理和工程实现能力的要求非常高。1.2 四问之间的递进逻辑2021年C题四个小问是明显的层层递进关系这点很多新手没看出来。第一问是“评价问题”让你从402家供应商里选出最重要的50家。第二问是“确定性问题优化”在已经定好50家供应商的前提下给出每周的订购方案和转运方案让成本或损耗最小。第三问是“参数变动问题”把企业的产能需求提高一定比例重新优化一遍对比供应商和转运商的选择需不需要调整。第四问则从确定性走向不确定性考虑到实际供货会有波动要为未来24周制定尽量不缺货的订购方案还要想办法压一压转运总成本。看到这个结构第一反应就该知道第一问只是个前置筛选真正拉开差距的是第二问和第四问。整体技术链路也比较固定数据读入与清洗、构造供应商评价特征、用综合评价方法选商、建0-1整数规划模型求解订购与转运方案、用随机模拟处理未来不确定性。下面我按这条链路把每个环节掰开讲。2. 数据处理与供应商特征工程2.1 把Excel整理成能直接计算的表C题的原始数据是几个Excel文件包含402家供应商近5年每周的供货数据、8家转运商近5年每周的运输损耗数据以及供应商的供货能力、原材料分类等信息。国赛Excel最常见的问题就是多级表头和合并单元格直接用pd.read_excel读进来容易翻车第一步一定要先看结构再动手。import pandas as pd df pd.read_excel(附件1_近5年402家供应商的相关数据.xlsx, sheet_name0, header0) print(df.shape) print(df.head(10)) print(df.columns.tolist())如果列名是“供应商编号、周次、供货量”这种明细结构就省事如果是宽表比如每一列是一个供应商编号、每一行是一周那就把它熔成长表再聚合。实操里我一般先转成(supplier_id, week, supply_qty)三元组后面所有特征都基于这个长表计算速度也快。2.2 供应商的四大类特征怎么构造选商不能只看谁供得多得从规模、稳定性、趋势、能力上限四个维度刻画。这套特征思路不只在C题里好用放到真实的供应商评价场景同样适用。供货规模类总供货量、平均每周供货量、近两年平均供货量。这类指标反映供应商体量是正向指标。供货稳定性类每周供货量的变异系数CV、零供货周数占比、连续断供最大周数。变异系数等于标准差除以均值用来消除不同供应商量纲差异。只看标准差会误判A供应商周均供货1000、标准差200B供应商周均供货100、标准差50B的绝对波动小但相对波动大得多评判稳定性必须用变异系数而不是标准差。供货趋势类把240周分成前后两段后半段周均供货量除以前半段周均供货量比值大于1说明供货能力在增长。这类特征能区分“越供越多”和“越供越缩水”的供应商很多队伍会漏掉。供货能力上限类历史周供货量的95%分位数。这个特征在第二问优化模型里当约束上限用比直接用最大值更稳因为历史最大值可能是异常周直接把约束撑爆会导致模型无解。feat df.groupby(supplier_id)[supply_qty].agg( total_supplysum, mean_supplymean, std_supplystd ) feat[cv] feat[std_supply] / (feat[mean_supply] 1e-9) feat[zero_ratio] (df.groupby(supplier_id)[supply_qty] .apply(lambda x: (x 0).mean())) feat[cap_p95] df.groupby(supplier_id)[supply_qty].quantile(0.95)2.3 缺失值与异常值处理缺失值一律填0原材料领域这一周没数据就是没供货不要用均值去插补。异常值处理要区分两种情况供货量为0是正常业务信号不能当异常值剔除而某些周供货量突然飙升到平时的好几倍大概率是统计口径问题可以用99%分位数裁剪或者直接把超过供应商历史95%分位数的值压缩到分位数水平。3. 第1问用熵权TOPSIS给402家供应商排序3.1 为什么选熵权TOPSIS第一问没有标准答案核心是自圆其说。最简单的做法是给每个指标加权求和但权重怎么定是个问题。有人直接拍脑袋这个“主观赋权法”在评委那里不好过。我当时用的组合是熵权法定权重、TOPSIS算排序本质上是把“指标权重”这个问题交给数据本身回答。熵权法的逻辑非常直观某个指标在所有供应商之间的差异越大说明它携带的区分信息越多给它的权重就越高。比如“供货总量”这个指标不同供应商之间差别悬殊熵权法会认为它很重要反之如果某个指标大家表现都差不多那它对排序几乎没有贡献权重自然低。TOPSIS做的事情则是把每个供应商看成一个高维空间里的点计算它与“理想最优解”和“理想最劣解”的距离用相对贴近度排序。这两者结合算是综合评价里的经典搭配。3.2 熵权法和TOPSIS的代码实现实现前先做正向化和标准化。所有指标要么越大越好要么越小越好不能混着算。像变异系数、零供货周数占比这类负向指标要做取倒数或者取负处理。完整代码如下import numpy as np import pandas as pd def entropy_weight(X): # X: n个供应商 x m个指标已正向化 X X / X.sum(axis0) k 1 / np.log(X.shape[0]) entropy -k * (X * np.log(X 1e-12)).sum(axis0) weight (1 - entropy) / (1 - entropy).sum() return weight def topsis(X, weight): norm X / np.sqrt((X ** 2).sum(axis0)) weighted norm * weight ideal_best weighted.max(axis0) ideal_worst weighted.min(axis0) d_best np.sqrt(((weighted - ideal_best) ** 2).sum(axis1)) d_worst np.sqrt(((weighted - ideal_worst) ** 2).sum(axis1)) return d_worst / (d_best d_worst 1e-12), weighted跑完TOPSIS会得到每个供应商的贴近度分数按分数降序排前50家入选。熵权法的权重结果建议打印出来写进论文评委很吃这一套因为这说明权重是有数据依据的不是拍脑袋拍的。3.3 前50家不是简单取TopN直接按综合分数取前50是个新手坑。TOPSIS算的是“综合贴近度”如果某个供应商供货量大但稳定性极差它的得分可能会被规模指标拉上去混进前50。更稳的套路是加一道硬性门槛先剔除总供货量过低的供应商比如近5年总供货量排在后30%的直接出局再剔除变异系数过大的供应商比如CV超过2倍的基本可以判断它供货像“过山车”纳进方案里只会让第四问缺货风险爆炸。我当时处理完之后前50名单和直接TopN相比替换了大概8家这8家的共同特点是“偶尔暴击供货、平时长期躺平”。这道门槛加完后面整数规划求解的可行性和稳定性都好很多这个细节在论文里写清楚也很加分。4. 第2、3问订购与转运联合优化4.1 决策变量与约束条件第2问的目标是在给定每周原材料需求的前提下决定向哪些供应商订购多少原材料同时决定让哪家转运商承运多少量使总成本或损耗最小。这个问题的数学表达不难难的是约束要列全。决策变量分三组x[i,t]表示第t周向第i家供应商的订购量y[j,t]表示第t周分配给第j家转运商的运输量z[i]是0-1变量表示供应商i是否被选用。因为它是在第一问筛出的50家里做选择z[i]在决策前就固定为1了真正需要求解的其实是x[i,t]和y[j,t]。核心约束有四个一是供应商供货上限约束任意一周的订购量不能超过该供应商历史95%分位数对应的供货能力二是需求满足约束所有原材料的实际到货量扣除运输损耗后必须不低于当周需求三是转运量守恒约束所有转运商承运的总量必须等于所有供应商订购的总量四是转运商运力约束任意转运商每周承运量不能超过历史最大运输量。目标函数在题目给定的成本口径下最小化订货成本、运输成本和损耗成本的总和如果题目明确要求“损耗最少”就把目标简化为损耗量。4.2 模型怎么解才不让求解器崩溃很多队伍死在“一上来就建全量模型”。50家供应商、240周、8家转运商如果直接把所有周和所有供应商揉成一个超大整数规划问题0-1变量加上连续变量上万普通求解器半小时都跑不出可行解。我的做法是按周拆解。因为题目假设每周需求独立、原材料不跨周库存240个周的子问题在结构上完全同构只需要求解一次典型周模型再对每周带入当周数据和当周损耗率循环求解就行。即使拆开每个子问题也有50个供应商变量和8个转运商变量用ortools的SCIP求解器就能跑动追求效率可以上Gurobi。核心代码框架如下from ortools.linear_solver import pywraplp solver pywraplp.Solver.CreateSolver(SCIP) x [solver.NumVar(0.0, cap[i], fx{i}) for i in range(50)] y [solver.NumVar(0.0, trans_cap[j], fy{j}) for j in range(8)] # 需求约束考虑损耗后的到货量 需求D loss_expr solver.Sum(x[i] * (1 - loss_rate[j]) for i in range(50) for j in range(8)) solver.Add(loss_expr demand) # 守恒约束转运总量 订购总量 solver.Add(solver.Sum(y) solver.Sum(x)) # 供应商能力约束 for i in range(50): solver.Add(x[i] cap[i]) # 目标总损耗最小或总费用最小 objective solver.Sum(x[i] * loss_ref for i in range(50)) # 简化示意 solver.Minimize(objective) status solver.Solve()这里有个非常重要的实操细节x[i]和y[j]之间的对应关系严格来说要做成“第i家供应商的货由第j家转运商运”的二维变量x[i,j]而不是我上面简化的两个独立变量。二维变量会让模型规模再膨胀8倍所以我当年是先假设“某一家供应商的货统一交给评分最高的转运商之一承运”先用一阶段简化模型算订购量再把订购量作为已知输入单独优化转运商分配。这样分两个小模型交替迭代速度和稳定性都远优于一次性求解二维整数规划。4.3 第3问产能提升只改一个参数第3问本质上是参数敏感性分析。题目会给出一个产能提升比例比如需求从D提高到(1α)D模型完全不用重构只需要把需求约束里的D替换成新需求重新求解。对比的重点不是“订购量变了多少”而是“选的供应商集合和转运商集合变没变”。我当时对比之后发现产能提高后部分供货能力上限低的供应商会被替换成能力上限更高的同时损耗率低的转运商占比会提升。这一问的报告重点应该是“哪些供应商被挤出了名单、为什么被挤出”而不是贴一堆订购量表格。5. 第4问把不确定性做进订购方案5.1 供应商的“兑现率”怎么定义第4问才是C题真正的灵魂。前两问默认“订多少就来多少”但真实世界里供应商经常“答应1000只送800”。所以得先量化每家供应商的兑现能力。定义供货兑现率r_i 实际供货量 / 计划订购量。如果数据里没有明确的订单记录就用这家供应商某周供货量与历史平均供货量的比值来近似刻画。对每家供应商统计它所有有供货周次的兑现率取P10、P50、P90三个分位数就能得到一组合理的“未来供货情景”。这样处理比随便猜一个正态分布要更有说服力因为分位数来自历史真实行为。5.2 未来24周的订购策略第4问要做的是未来24周的订购方案但未来哪家供应商能供多少、兑现率多少全是未知数。直接对未来的每周期望供货量做点预测再套第二问模型是最容易翻车的做法因为点预测会忽略波动导致缺货率偏高。更稳的做法是给需求乘一个安全系数。安全系数怎么定可以先从“只从历史P10兑现率最差的那批供应商采购”这个悲观情景出发算出需要多订多少才能满足未来需求再把多出的比例作为订购余量。实际操作中我先对每家供应商未来24周的供货能力取P50分位数作为基准能力然后在订购目标里加入“需求×5%~10%”的安全余量用整数规划算出行程。如果安全余量设太高库存积压、成本飙升方案也不合理所以这个系数要后续用模拟调。5.3 蒙特卡洛验证方案方案合不合理不能靠嘴说要放到蒙特卡洛模拟里检验。我当年写了一个简单的模拟器对每一周每家供应商的实际供货量按它历史的兑现率分布随机抽样然后累加计算库存和缺货周数跑1000次统计缺货概率。import numpy as np np.random.seed(42) outage_count 0 for sim in range(1000): stock 0 for t in range(24): delivered 0 for i in range(active_num): ratio np.random.uniform(0.85, 1.0) # 实际代码用各家分位数 delivered min(order[i][t] * ratio, cap[i]) stock max(stock delivered - demand[t], 0) if stock demand[t]: outage_count 1 if outage_count 0: break outage_prob outage_count / 1000 print(f缺货概率: {outage_prob:.2%})如果缺货概率高于题目允许水平就调大安全系数重跑一遍。这个“优化—模拟—调整”的闭环是第四问论文拿分的关键。我当时调了三轮第一轮安全系数3%缺货率偏高第二轮调到8%缺货率降到可控范围第三轮加了对转运次数的控制总成本进一步下降。这个调参过程写进论文里评委能直观看到你的方案是经过验证的而不是拍脑袋定的。6. 常见问题与避坑指南6.1 读Excel时列名错乱数据整体错位国赛数据表经常有合并单元格、多级表头、第一行是注释等情况。读进来之后一定要先打印列名和前几行核对如果列名变成了“Unnamed: 0”或者“1”这种多半是表头层级没有处理对。解决办法是skiprows跳过注释行或者手动指定header所在行再df.columns [supplier_id, week, supply_qty]重命名。6.2 整数规划求解器跑不动或者报无解先检查约束条件是否过紧。最常见的情况是需求约束总量大于所有选入供应商的能力上限之和说明第一问选的50家供应商整体供给能力不足要么放宽能力上限的分位数口径要么重新调整第一问评价指标权重。另一个常见问题是目标函数里出现了0-1变量 × 连续变量的非线性项模型退化成MINLP普通求解器直接罢工这种情况要把耦合项线性化拆成两个阶段求解。6.3 论文结果和代码复现对不上国赛评委很看重可复现性。所有随机过程记得固定随机种子所有分位数口径全程统一不要一会用95%分位数一会用最大值。我建议把每个阶段产出的中间结果都存成CSV比如“供应商特征表、熵权权重表、TOPSIS得分表、每周订购方案表”答辩时即使被追问也能随手调出中间过程比临时重跑代码靠谱得多。6.4 常见问题速查表现象可能原因处理办法TOPSIS得分全接近0.5指标选择区分度不足删除相关性高的冗余指标增加趋势类特征整数规划无可行解供应商能力上限之和小于需求提高能力分位数或放宽选商门槛求解时间过长变量含非线性耦合项拆两阶段迭代避免一次性求解大模型蒙特卡洛缺货率过高安全余量不足增加订购余量系数重新模拟验证所选供应商名单不稳定熵权权重受异常值影响先做异常值裁剪再跑熵权最后再说个题外话。2021年C题让我感触最深的一点是它不像传统数学建模题那样“考你知不知道某个算法”而是考“你能不能把一个不确定的现实问题翻译成一套数据、模型、代码、验证的闭环”。做这道题的时候我大部分时间其实花在看数据、洗数据、调约束上真正写模型反而很快。如果你也在准备国赛我建议别一上来就套各种高大上算法先把数据摸清楚把每个特征为什么这么构造想明白后面每一步都会顺很多。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门