数学建模国赛C题:基于Holt-Winters与库存策略的原材料订购计划实战

📅 2026/7/30 7:59:47 👁️ 阅读次数 📝 编程学习
数学建模国赛C题:基于Holt-Winters与库存策略的原材料订购计划实战

1. 项目概述:从赛题到解题的完整拆解

2021年的全国大学生数学建模竞赛C题,题目是“生产企业原材料的订购与运输”,这是一个典型的运筹优化与数据分析结合的题目,对参赛者的建模能力、编程实现和论文写作都提出了很高的要求。问题一作为整个赛题的起点和基础,其核心是要求我们基于附件中给出的企业过去五年的原材料进货和消耗数据,建立数学模型,来预测未来24周(即2021年接下来的两个季度)每周原材料的订购量。这听起来像是一个时间序列预测问题,但实际做起来,你会发现它远不止“预测”那么简单。它考察的是你对业务逻辑的理解、对数据波动性的处理,以及如何将一个现实问题抽象为严谨数学模型的综合能力。很多队伍在这里就栽了跟头,要么模型过于简单导致预测不准,要么模型过于复杂难以求解和解释。

我当年带队时,在这个问题上花了大量时间进行思路梳理和模型试错,最终找到了一套相对稳健且易于实现的方案。这篇文章,我就把自己在解决“问题一”时的完整思考过程、模型构建细节、代码实现关键点以及那些容易踩坑的地方,毫无保留地分享出来。无论你是正在备赛的同学,还是对数学建模感兴趣的朋友,相信这份从实战中沉淀下来的“解题笔记”,都能给你带来实实在在的启发和帮助。我们会从最根本的数据分析入手,一步步推导出模型,并用Python代码将其实现,最后还会讨论如何将模型结果优雅地整合进论文。

2. 核心思路解析:为什么不是简单的“预测”

拿到题目和数据,第一反应往往是:这不就是根据历史销量预测未来需求吗?直接用ARIMA、LSTM或者Prophet这类时间序列模型不就行了?如果你这么想,那很可能已经走偏了。国赛C题历来强调“应用背景”,我们必须深入理解题目描述的生产企业场景。

注意:附件中的数据是“进货量”和“消耗量”,而不是“市场需求量”。这是第一个关键点。企业的消耗量并不完全等同于市场需求,它受到企业库存策略、生产计划、供应商供货能力等多种内部因素影响。直接预测消耗量,忽略了企业作为决策主体的能动性。

因此,我们的核心思路不是“预测消耗”,而是“制定订购计划”。这个计划的目标是在满足生产需求的前提下,尽可能优化成本(虽然问题一没明确要求成本最小化,但合理的计划必然隐含成本考量)。所以,模型应该是一个库存管理模型,或者更具体地说,是一个需求驱动下的订购量决策模型

2.1 数据层面的深度观察与处理

附件中的数据通常包含每周的原材料进货量、消耗量,可能还有初始库存、供应商信息等。我们的第一步不是急着建模,而是彻底“读懂”数据。

  1. 数据清洗与探索性分析(EDA):这是所有建模工作的基石。你需要检查缺失值、异常值。对于时间序列,特别要关注是否存在明显的周期性(如季度性、月度性)、趋势性以及突变点(如节假日、促销活动)。绘制时间序列图、自相关图(ACF)、偏自相关图(PACF)是基本操作。
  2. 理解“消耗量”的构成:消耗量由两部分构成:一部分是满足当周生产需求的“刚性消耗”;另一部分可能是由于库存策略调整(如降低安全库存)而产生的“调节性消耗”。我们需要通过数据分析,尝试剥离出相对稳定的需求部分。
  3. 分析“进货量”与“消耗量”的关系:进货量通常滞后于消耗量,并且会受到库存水平的影响。计算两者的交叉相关性,可以了解大致的补货延迟周期。同时,观察库存水平(需要根据进货和消耗计算得出)的变化范围,可以初步判断企业的安全库存和最大库存策略。

实操心得:很多队伍在这里只是简单画个折线图就完事了。真正有深度的分析,应该去计算每周的“库存变化量”(= 进货量 - 消耗量),并观察其分布。如果库存变化量长期围绕一个较小值波动,说明企业采用的是“按需订购、小批量高频次”的策略;如果出现大起大落,则可能意味着存在周期性的批量订购或者需求波动极大。这个判断会直接影响你后续选择哪种库存模型。

2.2 模型选型:在经典与灵活之间权衡

基于上述分析,我们不会直接使用纯黑箱的预测模型。更合适的路径是构建一个状态空间模型,将库存作为状态,订购量作为控制输入,消耗量作为外部干扰(或需求)。一个非常贴切且经典的模型是动态规划或**模型预测控制(MPC)**框架下的库存控制模型。

但对于数模竞赛而言,动态规划求解可能略显复杂。一个更折中、也更容易解释的模型是:带有需求预测的(s, S)库存策略模型定期盘点、固定订购间隔的库存模型

我的建议是采用后者,并将其分解为两个核心模块:

  1. 需求预测模块:使用时间序列模型(如Holt-Winters三指数平滑、SARIMA)对未来24周的“基准需求”进行预测。这里的“基准需求”是我们从历史消耗数据中分解出的、相对稳定的部分。
  2. 库存决策模块:基于预测的需求、当前库存水平、预设的安全库存和目标库存水平,计算每周的订购量。其核心公式可以简化为:订购量_t = max(0, 预测需求_t + 安全库存 - 期末库存_{t-1})这里还需要考虑供应商的供货能力约束(最小起订量、最大供应量)、仓库容量约束等,题目附件中通常会给出或暗示。

为什么这样选?因为它既有预测模型的成分,又体现了库存管理的决策逻辑,物理意义清晰,评委容易理解。同时,它可以通过调整安全库存等参数来体现不同的风险偏好(保守型 vs 激进型),为问题二、三的深入讨论留出空间。

3. 模型构建与数学表达详解

让我们把上述思路转化为具体的数学模型。假设我们以周为单位,t表示周次。

3.1 符号定义

  • I_t: 第t周周末的库存水平(t=0表示初始库存)。
  • 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周前订购的货物这周到货的量,减去本周的消耗量。如果提前期L=1,则公式简化为I_t = I_{t-1} + O_{t-1} - D_t

订购决策方程:我们采用基于目标库存水平的策略。 在每周初(盘点时),我们根据当前的库存水平和对未来需求的预测,决定订购量。O_t = max(0, min(O_max, TI - I_t + F_{t+L}))这里F_{t+L}是对t+L周的需求预测(因为现在订的货L周后才到,需要满足那时的需求)。TI - I_t是为了将库存拉回到目标水平。min(O_max, ...)max(0, ...)用于处理最大订购量约束和非负约束。

目标库存与安全库存TI = SS + k * (未来L周的平均预测需求)其中k是一个系数,通常k>=1。安全库存SS的计算是关键,一个常见公式是:SS = z * σ * sqrt(L)其中z是服务水平系数(如95%服务水平对应z=1.65),σ是每周需求预测误差的标准差。这需要根据历史数据估计。

3.3 需求预测模块的具体实现

我们使用Holt-Winters三指数平滑模型,因为它能同时捕捉趋势和季节性,且原理相对直观,代码实现方便。 模型有三种成分:

  • 水平(Level)
  • 趋势(Trend)
  • 季节(Seasonal)

对于具有线性趋势和固定周期(比如一年52周,但我们的数据是5年,可能包含年度季节性)的数据,加法模型或乘法模型均可。通常先尝试加法模型。 通过历史数据训练模型,得到平滑参数(α, β, γ),然后向前预测24周,得到F_t序列。

参数估计与模型评估:使用历史数据的前80%作为训练集,后20%作为测试集。用均方根误差(RMSE)或平均绝对百分比误差(MAPE)来评估预测精度。在论文中,必须展示这个评估过程,以证明你选择的预测模型是合理的。

4. 完整Python代码实现与分步解读

下面,我将结合Python代码,展示如何从数据加载到最终输出订购计划的完整流程。我们主要使用pandasstatsmodelsnumpy库。

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', inplace=True) # 检查数据 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'], label='Consumption') axes[0].plot(data.index, data['Purchase'], label='Purchase', alpha=0.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'], label='Inventory Level', color='green') axes[1].axhline(y=I0, color='r', linestyle='--', label='Initial 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, trend='add', seasonal='add', seasonal_periods=52, initialization_method='estimated').fit() except: # 如果拟合失败(可能数据问题),尝试无季节性模型 print("Holt-Winters with seasonality failed, trying Holt's linear trend model.") model_hw = ExponentialSmoothing(train, trend='add', seasonal=None, initialization_method='estimated').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(f"Train RMSE: {train_rmse:.2f}, Train MAPE: {train_mape:.4f}") print(f"Test RMSE: {test_rmse:.2f}, Test MAPE: {test_mape:.4f}") # 可视化对比 plt.figure(figsize=(14, 7)) plt.plot(train.index, train, label='Train Actual') plt.plot(train.index, train_pred, label='Train Fit', linestyle='--') plt.plot(test.index, test, label='Test Actual') plt.plot(test.index, test_pred, label='Test Forecast', linestyle='--') plt.title('Holt-Winters Model Fit and Forecast') plt.legend() plt.grid(True) plt.show() # 使用全部历史数据重新训练模型,用于未来24周预测 final_model = ExponentialSmoothing(data['Consumption'], trend='add', seasonal='add', seasonal_periods=52, initialization_method='estimated').fit() # 预测未来24周的需求 future_weeks = 24 future_index = pd.date_range(start=data.index[-1] + pd.Timedelta(weeks=1), periods=future_weeks, freq='W') future_demand = final_model.forecast(future_weeks) future_demand_series = pd.Series(future_demand, index=future_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(f"Safety Stock (SS) calculated: {SS:.2f}") # 计算目标库存水平 TI (这里取安全库存加上L周的预测需求) # 注意:未来L周的需求是动态的,在决策时我们使用最新的预测值 # 但在初始化TI时,我们可以用一个平均值 avg_forecast_demand = future_demand_series.mean() TI = SS + L * avg_forecast_demand print(f"Target 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:t+L].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_{t+1} = I_t + O_{t-L+1}? - D_{t+1} # 这里需要仔细模拟到货。我们假设第t周下的订单,在t+L周到货。 # 因此,计算下周库存时,需要加上在t+L-1周时下的订单(如果到了)。 # 为了简化演示,我们假设一个固定的消耗率,例如使用未来预测需求作为消耗的估计。 # 实际上,消耗是随机的,这里我们用预测值代替。 next_demand = future_demand_series.iloc[t] if t < future_weeks else future_demand_series.iloc[-1] # 计算本周到货量:如果当前周次t>=L,那么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], index=future_index) # inventory_proj长度是future_weeks+1,第一个是当前库存,我们取未来周的库存 inventory_proj_series = pd.Series(inventory_proj[1:], index=future_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, label='Planned Purchase', alpha=0.7) axes[0].plot(future_index, future_demand_series.values, label='Forecast Demand', color='red', linewidth=2) axes[0].set_title('Purchase Plan vs. Forecast Demand (Next 24 Weeks)') axes[0].set_ylabel('Quantity') axes[0].legend() axes[0].grid(True, axis='y') axes[1].plot(future_index, inventory_proj_series.values, label='Projected Inventory', color='orange', linewidth=2) axes[1].axhline(y=SS, color='green', linestyle='--', label='Safety Stock (SS)') axes[1].axhline(y=TI, color='red', linestyle='--', label='Target Inventory (TI)', alpha=0.5) axes[1].fill_between(future_index, 0, SS, alpha=0.1, color='green') 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', index=False) 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时,我们使用了预测误差的标准差σ。更好的做法是使用预测模型的预测区间statsmodelsHoltWintersResults对象有forecastsimulate方法可以生成预测分布,从而更科学地计算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就会成为成本优化模型中的决策变量。在问题三中,可能会考虑需求不确定性(随机需求),你的模型就需要从确定性模型转为随机规划或鲁棒优化模型。

因此,在问题一的论文写作中,要有前瞻性

  • 在定义目标函数时,可以提及“在满足生产需求的前提下,为后续成本优化提供订购量基准”。
  • 在讨论模型局限性时,可以指出“本模型假设需求预测是确定的,未来可扩展为考虑随机需求的鲁棒优化模型”。
  • 参数如SSTI,可以表示为成本参数的函数(如持有成本、缺货成本),为问题二做铺垫。

6. 论文写作要点与代码整合技巧

数学建模竞赛,论文是最终呈现的成果。再好的模型和代码,如果表达不清,也难获好评。

6.1 模型叙述部分

  1. 问题重述与分析:不要照抄题目,要用自己的话精炼概括问题一的本质:“这是一个在需求不确定环境下,基于历史数据制定未来原材料订购计划的单周期库存决策问题。”
  2. 模型假设:列出清晰合理的假设,这是模型的基石。例如:
    • 假设未来24周的生产计划平稳,需求模式与历史相似。
    • 假设订货提前期L固定且已知。
    • 假设供应商供货能力充足,无数量限制(或根据附件给出限制)。
    • 忽略原材料的价格波动和订购固定成本(问题二再考虑)。
  3. 符号说明:制作一个清晰的三线表,列出所有模型中使用的符号、含义及单位。
  4. 模型建立:这是核心。按照“需求预测 → 库存决策”的逻辑展开。
    • 先介绍为什么选择Holt-Winters模型,展示模型公式。
    • 然后介绍库存平衡方程和基于目标库存的订购策略公式。
    • 详细解释安全库存SS和目标库存TI的计算公式及其经济意义。
  5. 模型求解与结果:描述求解过程(即代码流程),并展示关键结果图表。
    • 预测结果图:展示历史数据拟合曲线和未来24周预测带(置信区间)。
    • 订购计划与库存投影图:即上面代码生成的条形图+折线图组合。
    • 敏感性分析图:用折线图或柱状图展示关键参数变化对输出指标的影响。
  6. 模型检验与评价:展示回测结果。用表格对比模拟策略与实际历史数据在关键指标上的差异,并分析原因。

6.2 代码整合与可重复性

  • 附录:将完整的、可运行的核心代码放在附录中。代码要有清晰的注释。
  • 伪代码:在正文中,可以用伪代码或算法流程图来描述你的求解步骤,这比大段程序代码更受评委欢迎。
  • 关键结果输出:将未来24周的订购计划表以清晰的形式呈现在正文中(例如前10周,完整表放附录)。

一个常见的坑:论文中的模型描述和实际代码实现不一致。务必确保两者完全对应。检查所有公式中的符号是否与代码中的变量名有明确的映射关系。

解决2021年国赛C题问题一的过程,是一个标准的“数据分析 → 模型抽象 → 算法实现 → 结果分析”的建模闭环。它考验的不仅是你的数学和编程能力,更是你从实际数据中提炼科学问题,并用严谨又创新的方法去解决它的综合素养。记住,没有“唯一正确”的模型,只有“逻辑自洽、结果合理、表述清晰”的解决方案。希望这份详细的思路和代码,能为你点亮一盏灯,助你在数模的道路上走得更稳、更远。如果在实际操作中遇到数据异常或模型不收敛的情况,回头仔细检查数据预处理步骤和模型假设,往往能发现问题的根源。