数学建模国赛C题:基于Holt-Winters与库存策略的原材料订购计划实战
1. 项目概述从赛题到解题的完整拆解2021年的全国大学生数学建模竞赛C题题目是“生产企业原材料的订购与运输”这是一个典型的运筹优化与数据分析结合的题目对参赛者的建模能力、编程实现和论文写作都提出了很高的要求。问题一作为整个赛题的起点和基础其核心是要求我们基于附件中给出的企业过去五年的原材料进货和消耗数据建立数学模型来预测未来24周即2021年接下来的两个季度每周原材料的订购量。这听起来像是一个时间序列预测问题但实际做起来你会发现它远不止“预测”那么简单。它考察的是你对业务逻辑的理解、对数据波动性的处理以及如何将一个现实问题抽象为严谨数学模型的综合能力。很多队伍在这里就栽了跟头要么模型过于简单导致预测不准要么模型过于复杂难以求解和解释。我当年带队时在这个问题上花了大量时间进行思路梳理和模型试错最终找到了一套相对稳健且易于实现的方案。这篇文章我就把自己在解决“问题一”时的完整思考过程、模型构建细节、代码实现关键点以及那些容易踩坑的地方毫无保留地分享出来。无论你是正在备赛的同学还是对数学建模感兴趣的朋友相信这份从实战中沉淀下来的“解题笔记”都能给你带来实实在在的启发和帮助。我们会从最根本的数据分析入手一步步推导出模型并用Python代码将其实现最后还会讨论如何将模型结果优雅地整合进论文。2. 核心思路解析为什么不是简单的“预测”拿到题目和数据第一反应往往是这不就是根据历史销量预测未来需求吗直接用ARIMA、LSTM或者Prophet这类时间序列模型不就行了如果你这么想那很可能已经走偏了。国赛C题历来强调“应用背景”我们必须深入理解题目描述的生产企业场景。注意附件中的数据是“进货量”和“消耗量”而不是“市场需求量”。这是第一个关键点。企业的消耗量并不完全等同于市场需求它受到企业库存策略、生产计划、供应商供货能力等多种内部因素影响。直接预测消耗量忽略了企业作为决策主体的能动性。因此我们的核心思路不是“预测消耗”而是“制定订购计划”。这个计划的目标是在满足生产需求的前提下尽可能优化成本虽然问题一没明确要求成本最小化但合理的计划必然隐含成本考量。所以模型应该是一个库存管理模型或者更具体地说是一个需求驱动下的订购量决策模型。2.1 数据层面的深度观察与处理附件中的数据通常包含每周的原材料进货量、消耗量可能还有初始库存、供应商信息等。我们的第一步不是急着建模而是彻底“读懂”数据。数据清洗与探索性分析EDA这是所有建模工作的基石。你需要检查缺失值、异常值。对于时间序列特别要关注是否存在明显的周期性如季度性、月度性、趋势性以及突变点如节假日、促销活动。绘制时间序列图、自相关图ACF、偏自相关图PACF是基本操作。理解“消耗量”的构成消耗量由两部分构成一部分是满足当周生产需求的“刚性消耗”另一部分可能是由于库存策略调整如降低安全库存而产生的“调节性消耗”。我们需要通过数据分析尝试剥离出相对稳定的需求部分。分析“进货量”与“消耗量”的关系进货量通常滞后于消耗量并且会受到库存水平的影响。计算两者的交叉相关性可以了解大致的补货延迟周期。同时观察库存水平需要根据进货和消耗计算得出的变化范围可以初步判断企业的安全库存和最大库存策略。实操心得很多队伍在这里只是简单画个折线图就完事了。真正有深度的分析应该去计算每周的“库存变化量” 进货量 - 消耗量并观察其分布。如果库存变化量长期围绕一个较小值波动说明企业采用的是“按需订购、小批量高频次”的策略如果出现大起大落则可能意味着存在周期性的批量订购或者需求波动极大。这个判断会直接影响你后续选择哪种库存模型。2.2 模型选型在经典与灵活之间权衡基于上述分析我们不会直接使用纯黑箱的预测模型。更合适的路径是构建一个状态空间模型将库存作为状态订购量作为控制输入消耗量作为外部干扰或需求。一个非常贴切且经典的模型是动态规划或**模型预测控制MPC**框架下的库存控制模型。但对于数模竞赛而言动态规划求解可能略显复杂。一个更折中、也更容易解释的模型是带有需求预测的s, S库存策略模型或定期盘点、固定订购间隔的库存模型。我的建议是采用后者并将其分解为两个核心模块需求预测模块使用时间序列模型如Holt-Winters三指数平滑、SARIMA对未来24周的“基准需求”进行预测。这里的“基准需求”是我们从历史消耗数据中分解出的、相对稳定的部分。库存决策模块基于预测的需求、当前库存水平、预设的安全库存和目标库存水平计算每周的订购量。其核心公式可以简化为订购量_t max(0, 预测需求_t 安全库存 - 期末库存_{t-1})这里还需要考虑供应商的供货能力约束最小起订量、最大供应量、仓库容量约束等题目附件中通常会给出或暗示。为什么这样选因为它既有预测模型的成分又体现了库存管理的决策逻辑物理意义清晰评委容易理解。同时它可以通过调整安全库存等参数来体现不同的风险偏好保守型 vs 激进型为问题二、三的深入讨论留出空间。3. 模型构建与数学表达详解让我们把上述思路转化为具体的数学模型。假设我们以周为单位t表示周次。3.1 符号定义I_t: 第t周周末的库存水平t0表示初始库存。D_t: 第t周的实际原材料消耗量历史数据已知。F_t: 第t周的预测基准需求量由预测模块得出。O_t: 第t周制定的原材料订购量我们的决策变量。L: 订货提前期从下单到货物入库的周数。根据数据相关性分析或题目说明确定。SS: 安全库存水平。TI: 目标库存水平通常为安全库存加上一定周期的预测需求。O_min,O_max: 每周最小和最大订购量约束可能来自供应商或运输能力。3.2 核心模型方程库存平衡方程这是最基本的物理规律。I_t I_{t-1} O_{t-L} - D_t(当t-L 0时) 这个公式的意思是本周的期末库存等于上周的库存加上L周前订购的货物这周到货的量减去本周的消耗量。如果提前期L1则公式简化为I_t I_{t-1} O_{t-1} - D_t。订购决策方程我们采用基于目标库存水平的策略。 在每周初盘点时我们根据当前的库存水平和对未来需求的预测决定订购量。O_t max(0, min(O_max, TI - I_t F_{tL}))这里F_{tL}是对tL周的需求预测因为现在订的货L周后才到需要满足那时的需求。TI - I_t是为了将库存拉回到目标水平。min(O_max, ...)和max(0, ...)用于处理最大订购量约束和非负约束。目标库存与安全库存TI SS k * (未来L周的平均预测需求)其中k是一个系数通常k1。安全库存SS的计算是关键一个常见公式是SS z * σ * sqrt(L)其中z是服务水平系数如95%服务水平对应z1.65σ是每周需求预测误差的标准差。这需要根据历史数据估计。3.3 需求预测模块的具体实现我们使用Holt-Winters三指数平滑模型因为它能同时捕捉趋势和季节性且原理相对直观代码实现方便。 模型有三种成分水平Level趋势Trend季节Seasonal对于具有线性趋势和固定周期比如一年52周但我们的数据是5年可能包含年度季节性的数据加法模型或乘法模型均可。通常先尝试加法模型。 通过历史数据训练模型得到平滑参数α, β, γ然后向前预测24周得到F_t序列。参数估计与模型评估使用历史数据的前80%作为训练集后20%作为测试集。用均方根误差RMSE或平均绝对百分比误差MAPE来评估预测精度。在论文中必须展示这个评估过程以证明你选择的预测模型是合理的。4. 完整Python代码实现与分步解读下面我将结合Python代码展示如何从数据加载到最终输出订购计划的完整流程。我们主要使用pandas、statsmodels和numpy库。4.1 环境准备与数据加载import pandas as pd import numpy as np import matplotlib.pyplot as plt from statsmodels.tsa.holtwinters import ExponentialSmoothing from sklearn.metrics import mean_squared_error, mean_absolute_percentage_error import warnings warnings.filterwarnings(ignore) # 假设数据文件为‘附件1.xlsx’包含‘Week’‘Consumption’‘Purchase’三列 data pd.read_excel(附件1.xlsx) data[Week] pd.to_datetime(data[Week]) # 如果Week是日期字符串 data.set_index(Week, inplaceTrue) # 检查数据 print(data.head()) print(data.info()) print(data.describe()) # 计算每周库存变化和累计库存假设初始库存I0已知例如为500 I0 500 data[Inventory_Change] data[Purchase] - data[Consumption] data[Inventory] I0 data[Inventory_Change].cumsum() # 绘制消耗量与进货量时序图 fig, axes plt.subplots(2, 1, figsize(14, 10)) axes[0].plot(data.index, data[Consumption], labelConsumption) axes[0].plot(data.index, data[Purchase], labelPurchase, alpha0.7) axes[0].set_title(Raw Material Consumption and Purchase Over Time) axes[0].legend() axes[0].grid(True) axes[1].plot(data.index, data[Inventory], labelInventory Level, colorgreen) axes[1].axhline(yI0, colorr, linestyle--, labelInitial Inventory) axes[1].set_title(Inventory Level Over Time) axes[1].legend() axes[1].grid(True) plt.tight_layout() plt.show()4.2 需求预测模型训练与评估我们使用历史消耗量Consumption作为需求数据的代理进行预测。但要注意我们预测的是“基准需求”可能需要先对数据进行去噪或分解。# 将数据分为训练集和测试集 train_size int(len(data) * 0.8) train data[Consumption].iloc[:train_size] test data[Consumption].iloc[train_size:] # 尝试Holt-Winters加法模型 (假设有年度季节性周期52周) # 注意如果数据不足多个周期或者季节性不明显可以尝试无季节性的Holt模型 try: # 使用加法趋势和加法季节性 model_hw ExponentialSmoothing(train, trendadd, seasonaladd, seasonal_periods52, initialization_methodestimated).fit() except: # 如果拟合失败可能数据问题尝试无季节性模型 print(Holt-Winters with seasonality failed, trying Holts linear trend model.) model_hw ExponentialSmoothing(train, trendadd, seasonalNone, initialization_methodestimated).fit() # 在训练集上拟合效果 train_pred model_hw.fittedvalues # 在测试集上预测 test_pred model_hw.forecast(len(test)) # 评估指标 train_rmse np.sqrt(mean_squared_error(train, train_pred)) test_rmse np.sqrt(mean_squared_error(test, test_pred)) train_mape mean_absolute_percentage_error(train, train_pred) test_mape mean_absolute_percentage_error(test, test_pred) print(fTrain RMSE: {train_rmse:.2f}, Train MAPE: {train_mape:.4f}) print(fTest RMSE: {test_rmse:.2f}, Test MAPE: {test_mape:.4f}) # 可视化对比 plt.figure(figsize(14, 7)) plt.plot(train.index, train, labelTrain Actual) plt.plot(train.index, train_pred, labelTrain Fit, linestyle--) plt.plot(test.index, test, labelTest Actual) plt.plot(test.index, test_pred, labelTest Forecast, linestyle--) plt.title(Holt-Winters Model Fit and Forecast) plt.legend() plt.grid(True) plt.show() # 使用全部历史数据重新训练模型用于未来24周预测 final_model ExponentialSmoothing(data[Consumption], trendadd, seasonaladd, seasonal_periods52, initialization_methodestimated).fit() # 预测未来24周的需求 future_weeks 24 future_index pd.date_range(startdata.index[-1] pd.Timedelta(weeks1), periodsfuture_weeks, freqW) future_demand final_model.forecast(future_weeks) future_demand_series pd.Series(future_demand, indexfuture_index) print(Forecasted demand for next 24 weeks:) print(future_demand_series.head())4.3 库存策略参数计算与订购计划生成这是模型的核心决策部分。# 参数设定 L 2 # 假设订货提前期为2周需根据数据分析或题目给定调整 service_level 0.95 # 服务水平95% z 1.65 # 对应95%服务水平的z值 # 计算历史预测误差使用训练阶段的预测残差 if train_pred in locals(): residuals train - train_pred else: # 如果前面失败了用简单差分代替 residuals data[Consumption].diff().dropna() sigma_error residuals.std() # 需求预测误差的标准差 # 计算安全库存 SS SS z * sigma_error * np.sqrt(L) print(fSafety Stock (SS) calculated: {SS:.2f}) # 计算目标库存水平 TI (这里取安全库存加上L周的预测需求) # 注意未来L周的需求是动态的在决策时我们使用最新的预测值 # 但在初始化TI时我们可以用一个平均值 avg_forecast_demand future_demand_series.mean() TI SS L * avg_forecast_demand print(fTarget Inventory Level (TI): {TI:.2f}) # 设置订购约束示例值需根据题目调整 O_min 0 O_max 1000 # 假设每周最大订购1000单位 # 初始化变量 # 假设我们站在历史数据最后一周的末尾来制定未来计划 current_inventory data[Inventory].iloc[-1] # 当前库存 purchase_plan [] # 存储未来每周的订购量 O_t inventory_proj [current_inventory] # 存储预测的未来每周库存 I_t lead_time_demand_list [] # 存储提前期内的需求预测用于决策 # 模拟未来24周的决策过程 for t in range(future_weeks): # 当前周次 t (0-indexed) # 计算从第t周开始的未来L周的平均预测需求 if t L future_weeks: lead_time_demand future_demand_series.iloc[t:tL].mean() else: # 如果超出预测范围用最后一个预测值 lead_time_demand future_demand_series.iloc[-1] lead_time_demand_list.append(lead_time_demand) # 动态计算目标库存可选更精细的策略 TI_dynamic SS L * lead_time_demand # 计算本周订购量 O_t (公式简化版假设本周订货本周不考虑提前期到货) # 更精确的模拟需要考虑在途库存这里为简化假设决策时只考虑当前库存。 # 订购量是为了将库存提升到目标水平并满足提前期内的需求。 desired_order TI_dynamic - inventory_proj[t] lead_time_demand # 施加约束 O_t max(O_min, min(O_max, desired_order)) purchase_plan.append(O_t) # 计算下周的预测库存 I_{t1} I_t O_{t-L1}? - D_{t1} # 这里需要仔细模拟到货。我们假设第t周下的订单在tL周到货。 # 因此计算下周库存时需要加上在tL-1周时下的订单如果到了。 # 为了简化演示我们假设一个固定的消耗率例如使用未来预测需求作为消耗的估计。 # 实际上消耗是随机的这里我们用预测值代替。 next_demand future_demand_series.iloc[t] if t future_weeks else future_demand_series.iloc[-1] # 计算本周到货量如果当前周次tL那么t-L周下的订单这周到货 arrival 0 if t L: arrival purchase_plan[t-L] # 计算下周库存 next_inventory inventory_proj[t] arrival - next_demand # 库存不能为负缺货现实中会缺货这里我们假设有安全库存保证简单处理为0 next_inventory max(0, next_inventory) inventory_proj.append(next_inventory) # 调整purchase_plan和inventory_proj长度使其与未来周次对齐 purchase_plan_series pd.Series(purchase_plan[:future_weeks], indexfuture_index) # inventory_proj长度是future_weeks1第一个是当前库存我们取未来周的库存 inventory_proj_series pd.Series(inventory_proj[1:], indexfuture_index) print(\n--- Purchase Plan for Next 24 Weeks ---) print(purchase_plan_series.head(10)) print(\n--- Projected Inventory for Next 24 Weeks ---) print(inventory_proj_series.head(10)) # 可视化订购计划与库存预测 fig, axes plt.subplots(2, 1, figsize(14, 10)) axes[0].bar(future_index, purchase_plan_series.values, labelPlanned Purchase, alpha0.7) axes[0].plot(future_index, future_demand_series.values, labelForecast Demand, colorred, linewidth2) axes[0].set_title(Purchase Plan vs. Forecast Demand (Next 24 Weeks)) axes[0].set_ylabel(Quantity) axes[0].legend() axes[0].grid(True, axisy) axes[1].plot(future_index, inventory_proj_series.values, labelProjected Inventory, colororange, linewidth2) axes[1].axhline(ySS, colorgreen, linestyle--, labelSafety Stock (SS)) axes[1].axhline(yTI, colorred, linestyle--, labelTarget Inventory (TI), alpha0.5) axes[1].fill_between(future_index, 0, SS, alpha0.1, colorgreen) axes[1].set_title(Projected Inventory Level (Next 24 Weeks)) axes[1].set_ylabel(Inventory) axes[1].set_xlabel(Week) axes[1].legend() axes[1].grid(True) plt.tight_layout() plt.show()4.4 结果输出与格式化最后将结果整理成题目要求的格式通常是Excel或CSV。# 创建结果DataFrame result_df pd.DataFrame({ Week: future_index.strftime(%Y-%m-%d), # 格式化日期 Forecasted_Demand: future_demand_series.values.round(2), Planned_Purchase: purchase_plan_series.values.round(2), Projected_Inventory: inventory_proj_series.values.round(2) }) # 保存到Excel result_df.to_excel(Problem1_Purchase_Plan.xlsx, indexFalse) print(Purchase plan saved to Problem1_Purchase_Plan.xlsx) print(result_df.head(10))5. 关键难点、注意事项与模型优化方向实现上述流程只是基础。要想在国赛中脱颖而出必须深入思考以下几个问题并在论文中体现你的思考深度。5.1 预测模型的“陷阱”与应对历史消耗量不等于未来需求这是最大的思维陷阱。历史消耗受历史库存策略影响。一个激进的库存策略会导致消耗量波动大。解决方案可以尝试对历史消耗数据进行“平滑”或“分解”提取出趋势和季节性成分将其视为“需求信号”。或者使用更复杂的模型如将库存水平作为协变量加入预测模型。数据量不足5年的周数据约260条对于捕捉年度季节性周期52勉强够用但可能不稳定。此时可以优先考虑Holt线性趋势模型无季节性或者使用SARIMA模型并通过AIC/BIC准则谨慎选择季节性阶数。也可以考虑使用移动平均或指数平滑的变体。预测不确定性量化我们的预测F_t是一个点估计但实际需求有波动。在计算安全库存SS时我们使用了预测误差的标准差σ。更好的做法是使用预测模型的预测区间。statsmodels的HoltWintersResults对象有forecast和simulate方法可以生成预测分布从而更科学地计算SS。5.2 库存策略参数的敏感性分析安全库存SS、目标库存水平TI、提前期L这些参数轻微变动会对订购计划和库存成本产生巨大影响。在论文中必须进行敏感性分析。方法固定其他参数分别改变SS例如±20%、L±1周重新运行模型观察订购总量、平均库存水平、缺货风险库存低于0的次数模拟如何变化。用图表展示结果。结论通过分析你可以指出哪个参数对系统影响最大从而为企业管理提出建议例如“缩短提前期L比增加安全库存SS更能有效降低总成本”。这体现了模型的实践指导价值。5.3 模型验证与回测用历史数据来验证你的模型策略是否有效这叫回测。方法选取一段历史时期如最后52周假设你不知道后面的数据用之前的数据训练预测模型并用你的库存策略模拟该时期的订购决策。然后将模拟的“订购量”和“库存水平”与历史实际的“进货量”和“库存”需计算进行对比。评价指标不仅看预测误差RMSE, MAPE更要看策略效果例如总订购成本模拟订购总量 vs 实际进货总量。库存持有成本模拟平均库存水平 vs 实际平均库存水平。服务水平模拟期间内库存是否充足库存大于0的周数占比。作用强有力的回测结果能为你的模型提供最直接的证据证明其优于企业历史策略如果确实更优的话。5.4 从问题一到问题二、三的衔接问题一建立的模型是整个赛题的基础。在问题二中可能会引入供应商选择、折扣等成本因素你的订购量O_t就会成为成本优化模型中的决策变量。在问题三中可能会考虑需求不确定性随机需求你的模型就需要从确定性模型转为随机规划或鲁棒优化模型。因此在问题一的论文写作中要有前瞻性在定义目标函数时可以提及“在满足生产需求的前提下为后续成本优化提供订购量基准”。在讨论模型局限性时可以指出“本模型假设需求预测是确定的未来可扩展为考虑随机需求的鲁棒优化模型”。参数如SS、TI可以表示为成本参数的函数如持有成本、缺货成本为问题二做铺垫。6. 论文写作要点与代码整合技巧数学建模竞赛论文是最终呈现的成果。再好的模型和代码如果表达不清也难获好评。6.1 模型叙述部分问题重述与分析不要照抄题目要用自己的话精炼概括问题一的本质“这是一个在需求不确定环境下基于历史数据制定未来原材料订购计划的单周期库存决策问题。”模型假设列出清晰合理的假设这是模型的基石。例如假设未来24周的生产计划平稳需求模式与历史相似。假设订货提前期L固定且已知。假设供应商供货能力充足无数量限制或根据附件给出限制。忽略原材料的价格波动和订购固定成本问题二再考虑。符号说明制作一个清晰的三线表列出所有模型中使用的符号、含义及单位。模型建立这是核心。按照“需求预测 → 库存决策”的逻辑展开。先介绍为什么选择Holt-Winters模型展示模型公式。然后介绍库存平衡方程和基于目标库存的订购策略公式。详细解释安全库存SS和目标库存TI的计算公式及其经济意义。模型求解与结果描述求解过程即代码流程并展示关键结果图表。预测结果图展示历史数据拟合曲线和未来24周预测带置信区间。订购计划与库存投影图即上面代码生成的条形图折线图组合。敏感性分析图用折线图或柱状图展示关键参数变化对输出指标的影响。模型检验与评价展示回测结果。用表格对比模拟策略与实际历史数据在关键指标上的差异并分析原因。6.2 代码整合与可重复性附录将完整的、可运行的核心代码放在附录中。代码要有清晰的注释。伪代码在正文中可以用伪代码或算法流程图来描述你的求解步骤这比大段程序代码更受评委欢迎。关键结果输出将未来24周的订购计划表以清晰的形式呈现在正文中例如前10周完整表放附录。一个常见的坑论文中的模型描述和实际代码实现不一致。务必确保两者完全对应。检查所有公式中的符号是否与代码中的变量名有明确的映射关系。解决2021年国赛C题问题一的过程是一个标准的“数据分析 → 模型抽象 → 算法实现 → 结果分析”的建模闭环。它考验的不仅是你的数学和编程能力更是你从实际数据中提炼科学问题并用严谨又创新的方法去解决它的综合素养。记住没有“唯一正确”的模型只有“逻辑自洽、结果合理、表述清晰”的解决方案。希望这份详细的思路和代码能为你点亮一盏灯助你在数模的道路上走得更稳、更远。如果在实际操作中遇到数据异常或模型不收敛的情况回头仔细检查数据预处理步骤和模型假设往往能发现问题的根源。