简介数学建模2021年C题资料包面向全国大学生数学建模竞赛参赛者及建模学习者提供完整解题代码与配套数据重点解决供应商选择与生产转运优化问题。资源共27个文件以MATLAB脚本为核心共13个.m文件覆盖各小问的模型实现另有10个.mat数据文件用于存储中间计算结果4个Excel表格包含近5年402家供应商与8家转运商原始数据以及最终订购方案和转运方案结果。整套压缩包仅996KB体积小巧便于下载与反复运行。通过研读这些代码与数据可系统复盘2021年C题从问题理解、模型构建、算法求解到结果验证的完整流程学习将现实供应链场景转化为数学模型的方法。目前已有2310人学习下载适合建模竞赛选手及希望提升算法实现能力的中高级学习者。1. 先看懂这题在算什么某制造企业的原材料订购困局如果你参加过数学建模竞赛大概率对这类题不陌生一家生产制造企业生产线每天不能停原料仓库又不可能无限堆摆在决策者面前的就是三个连环问题——从几百家供应商里选出靠谱的长期伙伴按未来需求定出每周的订购量再找损耗最小的转运商把货搬进厂。2021年数学建模竞赛C题就是这样一个典型场景402家供应商、240周的供货记录、若干家转运商的选择权全都要用数据说话。这份资料的核心价值就是把这三层决策从赛题文字变成了一套可运行的代码先做供应商评价再做订购规划最后做转运优化。适合正在备赛的队伍也适合想练手供应链数据建模的开发者——你拿到的不是解题答案而是一套能换数据、能改参数、能复用思路的建模流水线。2. 赛题拆解与整体方案供应商筛选、订购规划、转运优化三层递进2.1 第一问从402家供应商中量化评价并选出50家C题的第一问本质是一个多属性决策问题。赛题给了每家供应商近5年每周的供货量以及供货原材料的质量数据合格率要求对供应商的供货特征做量化分析建立评价指标体系确定最重要的50家供应商。光看“谁供得多”远远不够——供货量大的供应商可能忽高忽低合格率高的小供应商可能产能跟不上。常见的评价维度是供货总量、供货稳定性方差、变异系数、供货中断情况断供周数、原材料合格率以及供货趋势是否上升。在资料中第一步代码就是把240周的面板数据按供应商ID聚合把每个维度算成一张特征表再用熵权法或层次分析法确定权重。从建模角度这套方案把“评价供应商”从拍脑袋变成了可复算的流程。熵权法在这里尤其合适它根据各指标在402家供应商之间的离散程度自动分配权重数据越分散、区分度越高的指标权重越大避免了“人为打分偏主观”的质疑。资料里的代码分两段第一段做特征工程输出一张supplier_feature.csv特征表第二段做综合评价给出每家供应商的总分排名。2.2 第二问满足产能前提下确定最少供应商数量第二问换了个角度已知企业每周产能是2.82万立方米为了保证正常生产需要多少家供应商、每周从哪些供应商订多少货。这不能简单地把第一问的TOP50直接拿来——因为第一问评价的是“综合价值”第二问更关心“保供”在满足产能需求的前提下选的供应商越少管理成本越低供应链越稳定。这是典型的整数规划问题决策变量是“是否选择第i家供应商”0或1约束条件是所选供应商的历史供货能力之和要覆盖周产能需求目标函数是供应商数量最少。这一小问的难点在于“供货能力”怎么定义。直接用历史平均供货量会有风险旺季供货多、淡季供货少取平均会高估保障能力。稳妥的做法是取历史供货量的某个低分位数比如25%分位数作为该供应商的保守周供货量再累加约束这样求出来的供应商数量在真实场景下不容易翻车。2.3 第三问转运商的损耗最小化调度第三问引入了转运商这个角色。企业不直接从供应商收货而是由转运商从供应商处提货再运到企业每家的转运损耗率不同每周的转运能力也有限。需要制定未来24周从哪些供应商订多少货、每周分配哪几家转运商运多少货使得总损耗最小。这一问是全题的决策高峰目标函数变成了“转运损耗最小化”约束条件增加了转运商的能力上限决策变量从一维选不选这家供应商变成二维第t周从供应商i订多少、第t周分配给转运商j多少。资料对这一问的代码处理是先把问题拆成两个子问题先用第二问的模型确定供应商集合和订购量再在给定订购量的前提下用线性规划求解每周的转运商分配方案。两层求解串行迭代代码组织清晰也方便在答辩时解释每一步的假设与改进空间。整体上三个小问的递进关系是先评价筛选再保供定商最后降本调运每一层的输出都是下一层的输入。3. 核心代码拆解数据预处理与供应商评价全流程3.1 读表与对齐把供货量表和合格率表合并成一张长表拿到手的赛题数据通常是两张Excel表一张是供应商供货量行是供应商ID列是第1周到第240周的供货量另一张是供应商原材料的合格率记录按批次给出同样带供应商ID。第一步就是把这两张表读进来按供应商ID对齐。这一步看起来简单实际上最容易翻车两张表的供应商个数可能不一致同一个ID在两套表里的列名格式比如带不带引号、是不是文本型数字可能不同。import pandas as pd df_supply pd.read_excel(supply.xlsx, index_col0) # 行供应商ID, 列第1~240周的供货量 df_quality pd.read_excel(quality.xlsx) # 含供应商ID、批次合格率等字段 df_supply.index df_supply.index.astype(str).str.strip() df_quality[supplier_id] df_quality[supplier_id].astype(str).str.strip() # 按供应商ID合并供货量长表和合格率表 df_supply_long df_supply.reset_index().melt( id_vars[supplier_id], var_nameweek, value_namesupply_qty ).sort_values([supplier_id, week]) merged df_supply_long.merge( df_quality, onsupplier_id, howleft ) print(merged.shape, merged[supplier_id].nunique())这里的关键操作有两个一是把供应商ID统一转成字符串并去掉首尾空格避免“ID以数字形式读入导致和字符型ID匹配不上”二是用melt把宽表转成长表方便后续按周、按供应商分组做滚动统计。howleft保证保留全部供货记录某个供应商没有合格率数据时对应列填空值后续再单独处理。3.2 特征工程把240周的原始序列压缩成6个评价指标供应商评价不能直接用每周的原始数值因为240个维度的向量既没法直接比较又充满噪音。资料里把原始序列压缩成几个有业务含义的标量指标总供货量、平均周供货量、周供货量标准差、变异系数、断供周数、加权合格率。其中变异系数标准差/均值是最能刻画“供货稳定性”的指标比单纯看标准差更公平——因为大供应商的绝对波动天生就比小供应商大。import numpy as np def supplier_features(grp): 对单个供应商的供货序列计算评价特征 qty grp[supply_qty].astype(float) qty qty.fillna(0) # 缺货周按0处理 total qty.sum() mean_q qty.mean() std_q qty.std() cv std_q / mean_q if mean_q 0 else 999 # 变异系数供货量接近0时给一个大数惩罚 zero_weeks (qty 0).sum() # 断供周数 return pd.Series({ total: total, mean: mean_q, std: std_q, cv: cv, zero_weeks: zero_weeks }) feat merged.groupby(supplier_id).apply(supplier_features).reset_index() feat.to_csv(supplier_feature.csv, indexFalse)fillna(0)的选择值得说明赛题供货量表里的空格通常代表“本周没有供货”按0处理而不是按缺失值丢弃否则求均值时会严重高估实际供货水平。cv是这套特征体系里最敏感的指标在答辩时要用一张散点图把它和总供货量的关系展示出来解释“大而不稳”和“小而稳”两种供应商形态。3.3 熵权TOPSIS打分50家供应商如何排序出来特征算出来后就要给402家供应商一个综合分。资料用的是熵权法定权重、TOPSIS算排序的组合方案。熵权法先从数据中计算每个指标的信息熵某项指标在402家供应商之间差异越大信息熵越小权重越高。然后TOPSIS把每家供应商看成高维空间里的一个点求它到“正理想解”所有指标最优值和“负理想解”所有指标最差值的距离得分负理想解距离 /正理想解距离负理想解距离。from sklearn.preprocessing import MinMaxScaler X feat[[total, mean, cv, zero_weeks]].values X MinMaxScaler().fit_transform(X) # 指标方向cv和zero_weeks是越小越好做正向化处理 X[:, 2] 1 - X[:, 2] X[:, 3] 1 - X[:, 3] # 熵权法求权重 eps 1e-12 P X / X.sum(axis0, keepdimsTrue) E -(P * np.log(P eps)).sum(axis0) / np.log(len(X)) w (1 - E) / (1 - E).sum() # TOPSIS打分 ideal_best X.max(axis0) ideal_worst X.min(axis0) d_best np.sqrt(((X - ideal_best) ** 2 * w).sum(axis1)) d_worst np.sqrt(((X - ideal_worst) ** 2 * w).sum(axis1)) score d_worst / (d_best d_worst) feat[score] score feat.sort_values(score, ascendingFalse).head(50)这个代码里最容易忽略的是指标方向处理TOPSIS默认认为所有指标都是越大越好但cv和zero_weeks是负向指标必须先做正向化否则“供货越不稳定”反而得分越高。用1 - x做正向化有一个坑如果某个值恰好是1正向化后会变成0导致该家供应商在这个维度被归为最差这是合理的但MinMaxScaler对异常值敏感如果原始表里有一个极端大的供货量其他供应商在“total”维度的差异会被压缩。稳健做法是先用分位数截尾再归一化或改用RobustScaler资料后面的代码里也做了这个修正。4. 规划求解模块需求预测、整数规划与损耗最小化4.1 GM(1,1)灰色预测从历史供货量推未来需求第二问和第三问都需要“未来24周的需求量”作为输入。赛题没有直接给出未来需求需要从历史数据推。资料里用灰色预测GM(1,1)来预测周需求量原因是样本量小、数据带明显趋势时灰色预测比回归更稳。GM(1,1)的思路是对原始序列做一次累加生成让累加序列看起来接近指数增长然后拟合一阶线性微分方程再累减还原出预测值。def gm11(x, n_pred24): x np.asarray(x, dtypefloat) x1 np.cumsum(x) # 一次累加生成序列 z1 (x1[:-1] x1[1:]) / 2 # 紧邻均值生成 B np.vstack([-z1, np.ones(len(z1))]).T Y x[1:] # 最小二乘估计参数 a, b a, b np.linalg.lstsq(B, Y, rcondNone)[0] x1_pred np.zeros(len(x) n_pred) x1_pred[0] x[0] for k in range(1, len(x1_pred)): x1_pred[k] (x[0] - b / a) * np.exp(-a * k) b / a x_pred np.diff(x1_pred, prependx1_pred[0]) # 累减还原 return x_pred[-n_pred:] future_demand gm11(demand_series, 24)这段代码的核心是np.linalg.lstsq这一步它用最小二乘同时估计出发展系数a和灰作用量b。a为负数代表序列有上升趋势绝对值越大增长越快a为正数代表衰减趋势。预测未来24周时必须把累加序列继续外推再累减还原不能直接在原始序列上外插。需要注意灰色预测适合短期预测且要求原始数据平滑如果直接把240周的周供货量全塞进去预测效果很差——资料里的做法是先按4周或8周聚合求平均平滑后再做预测预测结果作为“周均需求”的基准。4.2 整数规划满足产能的最少供应商选择有了需求预测量D和每家供应商的保守供货能力cap_i第二问就化成一个标准的0-1整数规划在总共402家中选择最少数量的供应商使得所选供应商每周供货能力之和大于等于D。资料用scipy.optimize.milp求解决策变量是0-1向量x长度为402。from scipy.optimize import milp, LinearConstraint, Bounds import numpy as np D future_demand.mean() # 周均需求预测值 cap feat[cap_week].values # 每家供应商保守周供货能力 # 目标min sum(x) c np.ones(402) A -cap.reshape(1, -1) # 约束转换为 形式 constraints LinearConstraint(A, ub-D) integrality np.ones(402) # 全部为整数变量 bounds Bounds(0, 1) # 0-1变量 res milp(cc, constraintsconstraints, integralityintegrality, boundsbounds) selected np.where(res.x 0.5)[0] print(f最少需要 {len(selected)} 家供应商)这里把原约束“cap_i * x_i之和 D”转成“-sum -D”才能塞进milp的LinearConstraint。integrality1的含义是要求所有变量只能取整数值配合Bounds(0,1)就限定了0-1。注意milp对大规模问题可能求解很慢402个0-1变量在标准赛题数据下几秒内能解出来如果换成几千家供应商就应该先用第二问的TOPSIS得分截断到Top100再求解减少变量规模。cap_week字段来自3.2特征表中“周供货量25%分位数”这是在写约束时最需要强调的假设。4.3 转运损耗最小化双层决策的迭代求解第三问把时间维度展开到24周每周有供货量S_t和转运商分配矩阵X_{t,j}两层决策。资料把问题拆成两步迭代第一步固定供应商集合和每周订购量第二步在给定订购量下求解每周的最优转运分配。每周分配子问题是一个线性规划目标函数是最小化总损耗运输量×该转运商损耗率约束是每家转运商周转运能力上限和各供应商周供货量不超过实际产能。from scipy.optimize import linprog def weekly_transport(qty_t, loss_rate, capacity, n_suppliers, n_carriers): qty_t: 第t周各供应商的订购量, shape (n_suppliers,) loss_rate: 各转运商损耗率, shape (n_carriers,) capacity: 各转运商周转运能力上限, shape (n_carriers,) c np.kron(np.ones(n_suppliers), loss_rate) # 每单位货的损耗成本拍平 A_ub, b_ub [], [] # 每家转运商能力约束 for j in range(n_carriers): row np.zeros(n_suppliers * n_carriers) row[j::n_carriers] 1 # 第j家转运商承接的所有运输量 A_ub.append(row) b_ub.append(capacity[j]) # 每个供应商当周供货量约束 for i in range(n_suppliers): row np.zeros(n_suppliers * n_carriers) row[i * n_carriers:(i 1) * n_carriers] 1 # 第i家供应商发出的所有货 A_ub.append(row) b_ub.append(qty_t[i]) res linprog(c, A_ubnp.array(A_ub), b_ubnp.array(b_ub), bounds(0, None)) return res.x.reshape(n_suppliers, n_carriers)代码里两个np.zeros加赋值的循环是线性规划建模时最常见的操作分别按列转运商和按行供应商拼约束矩阵。c用kron(ones, loss_rate)拍平本质是把二维的运输变量拉成一维后让每个决策变量的代价正好等于对应转运商的损耗率。解出来x再reshape回去就是一个n_suppliers x n_carriers的运输矩阵每行之和等于该供应商当周订购量每列之和不超过该转运商能力。第三问如果遇到无解通常是某周订购量总和超过了全部转运商能力之和这时候要把订购量整体下调代码里需要加一个可行性检查分支。5. 常见问题与避坑数据对齐、求解超时与预测翻车的五个案例5.1 周次索引错位240周数据合并后少了几个月现象看起来很奇怪——按供应商ID合并供货量表和合格率表之后merged.shape里的行数比预期的少了几百行画时间序列图时在某个位置断档。原因是两张表的周次列类型不一致一张表里周次是整数1到240另一张表里周次是字符串第1周。直接用列名匹配时整数3和字符串3匹配不上某几周的数据全部落空。解决方法是把两边的周次全部转成统一格式df[week] df[week].astype(int)或者干脆用str.extract(r(\d))把中文里的数字抽出来再转整数。从那以后凡是做多表合并我都会先打印两个dtypes再决定对齐键。5.2 合格率直接取平均导致排名失真特征工程里对“原材料质量”指标最初直接用了所有批次合格率的算术平均。跑完TOPSIS后发现有些供货量大、供货稳定的供应商排名反而靠后细查发现其中一家有十几个批次的合格率偏低但那些批次根本没被采用只是记录了检测数据。原因是合格率数据里包含“所有检测批次”而采购方只关心“实际供货批次的质量”。正确做法是先按供应商和周次对齐到供货记录上只统计“发生过供货的那些周”对应的合格率再用供货量加权平均。修改后权重落在质量和供应量之间排名才符合直觉。5.3 milp求解超时402个变量在答辩前卡死了第二问的milp在自测时跑得好好的加快数据准备后突然卡了十几分钟没有结果。检查发现约束矩阵里有一行写错导致可行域变成非凸混合整数空间求解器进入了深水区。另一个常见原因是Bounds忘了上界变量不只是0-1而是任意非负整数搜索空间爆炸。解决方法是先给milp一个可行的初始解把第二问得分排名前50的供应商直接设为1再设options{time_limit: 60}保底。初始解能让分支定界从一个可行点出发快速收敛即使算不完也能返回当前最好解。5.4 灰色预测出现负值供货量预测变成负数GM(1,1)预测某供应商未来24周供货量时后几期预测值出现了负数——无论如何都解释不通。原因是该供应商历史供货量波动太大累加序列不再近似指数曲线最小二乘拟合出来的a为正且很大外推时累减还原就把小值减成了负值。解决方法是先用4周平均把序列平滑再对负值做下限截断np.clip(x, 0, None)同时在答辩材料里注明“该供应商不做长期预测只用于产能估算”。短期、平滑、单调趋势明显的序列才适合灰色预测数据乱跳时老老实实用移动平均更稳。5.5 第三问每周独立求解时好时坏转运方案每周都对不上第三问按周循环求解24个子问题单独每周都算得出来但把方案汇总一看某些供应商某一周的分配量超过它当周的实际供货能力甚至出现上一周订了货、下一周才配送的错位。原因是按周独立求解时把“供应商当周供货能力”当成了无上限只在转运商侧加了约束。解法是在周循环里把上一周已经发出但未到达的库存量加进位缓冲或者更简单——把供应商当周订购量qty_t直接作为该周分配的上界传入weekly_transport保证进多少出多少不产生跨周倒挂。6. 二次开发把这份代码改造成你自己的建模流水线6.1 参数化改造从C题迁移到同类供应链题拿到这套资料后最关键的第一步是把它改成“配置驱动”而不是改代码。我会在项目根目录建一个config.yaml把赛题中的所有已知数抽出来周产能2.82万立方米、供应商总数402家、转运商能力上限数组、预测周数24、质量不合格率阈值等。这样遇到其他赛题只需要改配置不动算法代码。比如某年某赛区出过“多供应商、多仓库、多配送中心”的题把weekly_transport里的转运商换成仓库再叠加一层配送约束主体框架完全能复用。6.2 换数据跑通接口设计和输出标准化资料里的核心函数都是“输入DataFrame输出DataFrame”没有print满天飞。我在改造时沿用了这个思路所有模块只通过函数参数和返回值通信中间结果落CSV最终结果统一输出成三张表——supplier_selection.csv选中的供应商ID及周供货量、order_plan.csv24周订购计划、transport_plan.csv每周各转运商的分配量。这样换一届数据只需要把新数据转成同样的长表格式函数完全不用动。验证方式也简单拿上一届未被选中的供应商做回顾性检查跑一遍历史数据看逻辑是否自洽。6.3 答辩前必做的一件事灵敏度分析脚本这个项目最容易在答辩时被追问的是“你的结论对参数敏感吗”。我后来养成了一个习惯也是踩过坑之后的后悔药在项目里多加一个sensitivity.py对三个关键参数做扫描——需求预测的平滑窗口、保守供货能力的分位数25%换成10%和50%、转运商损耗率上下浮动2%。扫描结果画成曲线答辩时直接展示“供应商数量对分位数选择的敏感性不高始终在45到55家之间”这比任何口头解释都有说服力。从那以后我每次拿到赛题都强制先写一页config.yaml再动手建模跑批量实验时改参数效率直接翻倍希望这套方法和这份资料一起能让你少走几段弯路。好的这份C题资料把从数据、算法到最终方案输出的完整链路都打通了代码可以直接在自己的环境里跑起来也可以按上面的思路改造成你下次建模的脚手架。本文还有配套的精品资源点击获取