数学建模竞赛实战:基于XGBoost与滚动优化的蔬菜定价补货决策
2026/8/22 11:26:59 网站建设 项目流程

1. 项目概述:从赛题到实战的完整闭环

去年带队参加全国大学生数学建模竞赛,C题“蔬菜类商品的自动定价与补货决策”给我留下了深刻印象。这道题之所以经典,是因为它完美地模拟了一个真实商业场景的核心决策问题:如何在需求波动、损耗约束和利润目标之间找到最优平衡。这不仅仅是数学,更是对供应链管理、数据分析和商业逻辑的综合考验。很多同学拿到题目后,往往一头扎进模型构建,却忽略了问题本身的业务背景和数据的现实含义,导致模型“好看不好用”。今天,我就结合当时的解题思路、代码实现以及后续的复盘思考,把这个项目的完整脉络拆解清楚,从问题理解、数据清洗、模型构建到决策输出,提供一个可以直接参考复现的实战指南。无论你是正在备赛的学生,还是对数据驱动决策感兴趣的分析师,这篇文章都能帮你建立起解决此类问题的系统性框架。

2. 核心问题拆解与建模思路

面对“自动定价与补货决策”这样一个复合型问题,第一步不是急着写代码,而是要把大问题分解成一系列可量化、可建模的子问题。这道题的核心矛盾在于:定价影响销量,销量决定补货,补货又涉及库存成本和损耗,最终共同影响总收益。这是一个典型的动态优化问题。

2.1 问题一:销量与定价的关系建模

这是所有决策的基石。题目通常会提供历史销售数据,包含不同蔬菜品类在不同日期的销量和售价。我们的目标是建立一个函数,描述“价格如何影响需求”。这里最常用的方法是需求弹性模型。但直接套用经济学教科书上的公式往往行不通,因为蔬菜销售还受到季节性、星期效应、促销活动等多重因素影响。

实操要点:

  1. 数据分层处理:不要对所有蔬菜用一个模型。叶菜类(如菠菜)和根茎类(如土豆)的损耗特性和需求弹性天差地别。必须按品类分别建模。
  2. 特征工程是关键:除了价格,必须引入其他特征。
    • 时间特征:是否为周末、节假日、月份、季节。周末的客流量和购买习惯与工作日不同。
    • 滞后特征:前一天的销量、前一周同期的销量。很多蔬菜需求有很强的惯性或周期性。
    • 交叉特征:其他相关蔬菜的价格(替代品或互补品)。例如,菠菜价格飙升,可能会带动生菜销量上涨。
  3. 模型选型:线性回归简单但可能无法捕捉非线性关系。可以尝试:
    • 带正则化的线性模型(如Lasso):自动进行特征选择,防止过拟合。
    • 树模型(如XGBoost、LightGBM):能很好地处理特征间的非线性关系和交互效应,且对缺失值不敏感,是当前竞赛中的主流选择。
    • 集成思路:用线性模型捕捉趋势,用树模型捕捉残差中的复杂模式。

注意:评估模型时,不要只看R²,更要关注在“价格变动”区间内的预测准确性。因为我们的核心是用模型来模拟调价后的销量变化。

2.2 问题二:考虑损耗的补货模型

蔬菜有保质期,这是区别于一般商品的核心约束。补货决策必须回答:每天每类菜进多少货?这需要平衡缺货损失(机会成本)和过剩损耗(报废成本)。

建模思路:这本质上是一个报童模型的扩展。经典报童模型求解一个使期望利润最大化的单周期订货量。但蔬菜问题是多品类、多周期的。

  1. 定义成本结构

    • 进货成本:蔬菜的采购单价。
    • 持有成本:主要为资金占用成本,对于蔬菜而言相对较低,有时可忽略。
    • 缺货成本:未满足需求导致的利润损失。这不仅是单笔利润,还可能包括顾客满意度下降的长期损失,可以设定为一个惩罚系数。
    • 损耗成本:未售出且过期蔬菜的处置成本,即进货成本的一部分或全部。
  2. 建立目标函数: 对于单个品类单日决策,期望利润可表示为:期望利润 = ∑(销量 * 售价 - 进货量 * 进价 - 损耗量 * 单位损耗成本 - 缺货量 * 单位缺货惩罚)其中,销量是随机变量,依赖于我们预测的需求分布。

  3. 从单周期到多周期: 今天的剩余库存可以影响明天的决策,因此这是一个动态规划问题。但竞赛时间有限,通常采用滚动时域优化:每天根据当前库存和未来若干天的预测需求,求解一个有限时间窗(如3-7天)内的优化问题,只执行第一天的补货决策,第二天再根据新状态重新优化。

2.3 问题三:定价与补货的联合优化

这是最难的部分,定价和补货相互耦合。更高的价格可能降低销量,从而减少所需补货量和潜在损耗;但更低的价格可能刺激需求,需要更多补货,同时增加损耗风险。

求解策略:

  1. 分步迭代法(实用首选)
    • 步骤1:给定一组初始价格,用问题二的补货模型求解最优补货量。
    • 步骤2:固定补货量,微调价格,利用需求模型计算销量变化,评估总利润是否提升。
    • 步骤3:重复步骤1和2,直到利润增长小于某个阈值或达到迭代次数上限。这种方法虽然不是全局最优,但在计算复杂度和效果之间取得了很好的平衡。
  2. 联合优化模型:将价格和补货量作为决策变量,构建一个大规模的非线性规划或混合整数规划模型。这需要强大的求解器(如Gurobi, Cplex)和较高的建模技巧,计算量大,但理论更优美。

3. 数据预处理与特征工程实战

拿到竞赛数据,第一步不是跑模型,而是“看”数据。一份典型的蔬菜销售数据可能包含:商品编码、日期、销量、售价、进货量、损耗量等字段。数据质量直接决定模型天花板。

3.1 数据清洗核心步骤

  1. 异常值处理
    • 销量为0或极低:需区分是“真无销售”还是“数据缺失”。结合进货量判断,如果进货量正常但销量为0,可能是数据记录问题,需谨慎处理或视为缺失。
    • 价格异常波动:计算每日价格的Z-score或使用IQR方法,剔除远超正常范围的价格点(可能是录入错误)。
    • 负库存或销量大于进货量:明显的数据错误,需根据前后数据插值或直接剔除。
  2. 缺失值填补
    • 对于连续的销量、价格数据,可采用前后均值、线性插值或基于同类商品趋势的插值。
    • 对于分类特征(如是否促销),如果缺失,通常按“否”处理。
  3. 数据一致性检查:确保“销量 + 当日剩余库存 = 前日库存 + 进货量 - 损耗量”大致成立。虽然实际数据可能有误差,但大的偏差需要回溯。

3.2 针对本题的特征构建

以下特征需要在数据集中提前构建好:

import pandas as pd import numpy as np # 假设 df 包含 `date`, `category_id`, `sales`, `price`, `purchase` 等字段 df['date'] = pd.to_datetime(df['date']) # 1. 时间特征 df['day_of_week'] = df['date'].dt.dayofweek # 周一=0, 周日=6 df['is_weekend'] = df['day_of_week'].apply(lambda x: 1 if x >= 5 else 0) df['month'] = df['date'].dt.month df['season'] = df['month'].apply(lambda m: (m%12 + 3)//3) # 1:春, 2:夏, 3:秋, 4:冬 # 2. 滞后特征 (以品类为单位分组后创建) df = df.sort_values(['category_id', 'date']) for lag in [1, 7, 30]: # 滞后1天、7天(周度)、30天(月度) df[f'sales_lag_{lag}'] = df.groupby('category_id')['sales'].shift(lag) df[f'price_lag_{lag}'] = df.groupby('category_id')['price'].shift(lag) # 3. 滚动统计特征 df['sales_rolling_mean_7'] = df.groupby('category_id')['sales'].transform(lambda x: x.rolling(7, min_periods=1).mean()) df['price_rolling_std_7'] = df.groupby('category_id')['price'].transform(lambda x: x.rolling(7, min_periods=1).std()) # 4. 品类相关特征 (例如,叶菜类标识) leafy_categories = [101, 102, 105] # 假设的叶菜类ID df['is_leafy'] = df['category_id'].apply(lambda x: 1 if x in leafy_categories else 0) # 处理滞后特征产生的缺失值 df = df.fillna(method='bfill') # 或用其他策略

4. 模型构建与代码实现详解

我们以XGBoost需求预测模型滚动时域补货优化为例,展示核心代码框架。

4.1 基于XGBoost的需求预测模型

import xgboost as xgb from sklearn.model_selection import TimeSeriesSplit, GridSearchCV from sklearn.metrics import mean_absolute_error, mean_squared_error import warnings warnings.filterwarnings('ignore') # 假设预处理后的DataFrame为 `df`,特征列表为 `feature_cols`,目标变量为 `'sales'` feature_cols = ['price', 'day_of_week', 'is_weekend', 'month', 'sales_lag_1', 'sales_lag_7', 'sales_rolling_mean_7', 'is_leafy'] target_col = 'sales' # 按时间排序,确保时间序列不泄露 df = df.sort_values('date').reset_index(drop=True) # 划分训练集和测试集(按时间划分) split_date = '2023-08-01' # 假设的划分日期 train = df[df['date'] < split_date].copy() test = df[df['date'] >= split_date].copy() X_train, y_train = train[feature_cols], train[target_col] X_test, y_test = test[feature_cols], test[target_col] # 初始化模型 model = xgb.XGBRegressor(objective='reg:squarederror', n_estimators=500, learning_rate=0.05, max_depth=6, subsample=0.8, colsample_bytree=0.8, random_state=42) # 使用时间序列交叉验证进行参数微调(可选但推荐) tscv = TimeSeriesSplit(n_splits=5) param_grid = { 'max_depth': [4, 6, 8], 'learning_rate': [0.01, 0.05, 0.1], 'subsample': [0.7, 0.8, 0.9] } # grid_search = GridSearchCV(model, param_grid, cv=tscv, scoring='neg_mean_absolute_error', verbose=1) # grid_search.fit(X_train, y_train) # best_model = grid_search.best_estimator_ # 直接训练 model.fit(X_train, y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=50, verbose=False) # 预测与评估 y_pred = model.predict(X_test) mae = mean_absolute_error(y_test, y_pred) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) print(f"测试集 MAE: {mae:.2f}, RMSE: {rmse:.2f}") # 特征重要性分析 importance = pd.DataFrame({ 'feature': feature_cols, 'importance': model.feature_importances_ }).sort_values('importance', ascending=False) print(importance)

实操心得

  • early_stopping_rounds是防止过拟合的神器,务必使用。
  • 时间序列数据绝对不能用随机划分的交叉验证,必须用TimeSeriesSplit
  • 特征重要性分析能帮你理解哪些因素真正驱动销量,有时比模型精度更重要。

4.2 滚动时域补货优化模型

这里我们简化问题,假设已知未来几天的需求预测(由上述模型给出),并采用线性规划求解单日的补货量。

from scipy.optimize import linprog import numpy as np def daily_replenishment_optimization(current_inventory, predicted_demands, purchase_cost, selling_price, shortage_penalty, spoilage_cost, holding_cost_rate=0.001): """ 单日补货优化(报童模型思想扩展) 参数: current_inventory: 当前库存列表(每个品类) predicted_demands: 未来T天每个品类的需求预测列表(T x N) purchase_cost: 每个品类的进货单价列表 selling_price: 每个品类的销售单价列表 shortage_penalty: 单位缺货惩罚成本(通常大于利润率) spoilage_cost: 单位损耗成本(通常等于或小于进货成本) holding_cost_rate: 每日库存持有成本率 返回: 建议的补货量列表 """ num_categories = len(current_inventory) T = len(predicted_demands) # 优化时域 # 构建线性规划问题 # 决策变量:补货量 q_i (i=1..num_categories) # 目标函数:最大化期望利润(最小化负利润) c = [] # 目标函数系数 # 约束:Ax <= b A = [] b = [] # 简化:我们只优化第一天的补货,并近似考虑未来T天的期望影响 for i in range(num_categories): # 目标函数系数:对于补货量q_i,其成本是立即发生的 (purchase_cost[i]) # 其收益和成本取决于未来需求,这里做一个简化计算 # 我们假设补货量用于满足未来T天的需求,平均每日持有成本和损耗风险 expected_daily_demand = np.mean([predicted_demands[t][i] for t in range(T)]) # 一个非常简化的系数:增加一单位补货的边际净收益期望 # 净收益 = 售价 - 进价 - 缺货惩罚风险 - 损耗风险 - 持有成本 # 这里用启发式方法估算: prob_shortage = max(0, expected_daily_demand - current_inventory[i]) / (expected_daily_demand + 1e-5) prob_spoilage = max(0, current_inventory[i] - expected_daily_demand) / (current_inventory[i] + 1e-5) marginal_profit = selling_price[i] - purchase_cost[i] \ - shortage_penalty * prob_shortage \ - spoilage_cost * prob_spoilage \ - holding_cost_rate * purchase_cost[i] * T/2 # 平均持有时间估算 c.append(-marginal_profit) # linprog默认最小化,所以取负 # 约束示例:总补货预算限制、单个品类补货上限、非负约束等 # 1. 非负约束 (q_i >= 0) 已由 bounds 参数定义 # 2. 总采购预算约束 (假设总预算为B) total_budget = 10000 # 示例预算 A.append(purchase_cost) # sum(purchase_cost[i] * q_i) <= total_budget b.append(total_budget) # 3. 单个品类最大补货量约束(基于仓储空间或供应商限制) max_replenish = [200, 150, 300] # 示例,每个品类的上限 for i in range(num_categories): constraint = [0] * num_categories constraint[i] = 1 A.append(constraint) b.append(max_replenish[i]) # 求解线性规划 bounds = [(0, None) for _ in range(num_categories)] # 补货量非负 result = linprog(c, A_ub=A, b_ub=b, bounds=bounds, method='highs') if result.success: return np.round(result.x).astype(int) # 返回整数补货量 else: print("优化失败:", result.message) return np.zeros(num_categories, dtype=int) # 示例调用 current_inv = [50, 30, 20] # 品类A、B、C的当前库存 pred_demands = [ # 未来3天的需求预测 [品类A, 品类B, 品类C] [60, 35, 25], [55, 40, 22], [65, 30, 28] ] purchase_cost = [2.0, 3.0, 1.5] selling_price = [5.0, 7.0, 4.0] shortage_penalty = 2.0 # 缺货一单位的惩罚 spoilage_cost = 1.0 # 损耗一单位的成本 opt_order = daily_replenishment_optimization(current_inv, pred_demands, purchase_cost, selling_price, shortage_penalty, spoilage_cost) print(f"建议补货量: {opt_order}")

注意:这是一个高度简化的示例。真实的联合优化模型要复杂得多,可能需要用到cvxpyPuLP等建模工具,或者启发式算法(如遗传算法)来求解。

5. 结果分析与决策输出

模型跑出结果只是第一步,如何解读和呈现这些结果,使其成为可执行的商业决策,是赢得比赛的关键。

5.1 定价决策表的生成

定价决策不应是单个数字,而应是一个策略表。例如,对于每个蔬菜品类,模型可以输出一个“价格-预期销量-预期利润”的对照表,或者一个基于当前库存和未来预测的实时定价函数。

def generate_pricing_strategy(category_id, current_inventory, demand_forecast, base_price, elasticity_estimate): """ 生成动态定价建议。 简化策略:基于库存水平调整价格。 """ days_of_supply = current_inventory / (demand_forecast + 1e-5) if days_of_supply < 1.0: # 库存低于1天需求量,可能缺货 # 适当提价以抑制需求,平衡利润和缺货风险 suggested_price = base_price * 1.15 action = "Raise Price (Low Stock)" elif days_of_supply > 3.0: # 库存超过3天需求量,损耗风险高 # 适当降价以促进销售,减少潜在损耗 suggested_price = base_price * 0.9 action = "Discount (High Stock)" else: suggested_price = base_price action = "Hold Price" # 确保价格在合理范围内 suggested_price = max(base_price * 0.7, min(suggested_price, base_price * 1.3)) return suggested_price, action # 为每个品类生成建议 strategy_table = [] for cat_id in df['category_id'].unique(): # 获取该品类最新数据 cat_data = df[df['category_id'] == cat_id].iloc[-1] inv = cat_data['current_inventory'] # 假设数据中有该字段 forecast = cat_data['predicted_demand_next_day'] # 假设预测字段 base_p = cat_data['price_rolling_mean_7'] # 以近期均价为基础 new_price, action = generate_pricing_strategy(cat_id, inv, forecast, base_p, -1.5) # 假设弹性为-1.5 strategy_table.append({ 'Category_ID': cat_id, 'Current_Inventory': inv, 'Demand_Forecast': forecast, 'Base_Price': round(base_p, 2), 'Suggested_Price': round(new_price, 2), 'Action': action }) strategy_df = pd.DataFrame(strategy_table) print(strategy_df)

5.2 补货计划的可视化

将补货计划与历史销量、库存水平一起可视化,能直观展示决策的合理性。

import matplotlib.pyplot as plt # 假设我们有一个包含日期、品类、建议补货量、预测销量、实际库存的DataFrame `plan_df` fig, axes = plt.subplots(2, 1, figsize=(14, 10)) # 图表1:销量、库存与补货量趋势 category = '菠菜' # 示例品类 cat_data = plan_df[plan_df['category'] == category] ax1 = axes[0] ax1.plot(cat_data['date'], cat_data['sales'], label='实际销量', marker='o', linewidth=2) ax1.plot(cat_data['date'], cat_data['predicted_sales'], label='预测销量', linestyle='--') ax1.bar(cat_data['date'], cat_data['replenishment'], alpha=0.5, label='建议补货量', color='orange') ax1.set_ylabel('数量') ax1.set_title(f'{category} - 销量、库存与补货决策') ax1.legend() ax1.grid(True, linestyle='--', alpha=0.7) # 图表2:库存水平变化 ax2 = axes[1] ax2.plot(cat_data['date'], cat_data['inventory'], label='库存水平', color='green', marker='s', linewidth=2) ax2.axhline(y=cat_data['safety_stock'].mean(), color='r', linestyle=':', label='安全库存线') ax2.fill_between(cat_data['date'], 0, cat_data['inventory'], alpha=0.3, color='green') ax2.set_xlabel('日期') ax2.set_ylabel('库存量') ax2.set_title(f'{category} - 库存水平变化') ax2.legend() ax2.grid(True, linestyle='--', alpha=0.7) plt.tight_layout() plt.show()

6. 常见问题与避坑指南

在实战和辅导学生过程中,我总结了几个最容易出错的地方。

6.1 模型评估的陷阱

问题:在训练集上R²很高,但一用于定价模拟,利润计算就出问题。原因:评估指标选错。回归模型常用的MSE、MAE衡量的是预测值与真实值的绝对误差。但对于定价决策,我们更关心需求弹性预测的准确性,即价格变动百分比引起的销量变动百分比是否预测得准。解决方案

  1. 划分一个“策略验证期”,不参与模型训练。
  2. 在验证期上,模拟你的定价策略:基于模型预测的需求弹性,调整价格,用真实的销量反应(或留出的测试数据)来计算模拟利润。
  3. 核心评估指标是模拟总利润,而不是预测误差。

6.2 数据泄露与过拟合

问题:使用了未来的信息来预测过去,导致模型在历史数据上表现虚幻的好。典型错误

  • 在构建“7日均价”特征时,使用了包含当天在内的滚动窗口。正确的做法是使用rolling(7).mean().shift(1),即只用历史信息。
  • 在特征工程后,对整个数据集进行标准化,然后再划分训练测试集。这会导致测试集信息“泄露”到训练集的标准化参数中。正确做法严格按时间顺序处理数据。任何特征生成、数据缩放,都必须在按时间划分的训练集上拟合参数,然后应用到测试集上。

6.3 对损耗处理的理想化

问题:将损耗简单地视为一个固定比例或与库存线性相关。现实:蔬菜损耗是非线性的。例如,叶菜类在库存第1天和第3天的损耗率截然不同,且受温度、湿度影响。改进思路

  1. 建立损耗率模型:将损耗率作为库存天数、品类、季节的函数来建模。可以使用历史损耗数据训练一个分类模型(如是否损耗)或回归模型(损耗比例)。
  2. 在优化模型中引入非线性损耗成本:将损耗成本表示为库存水平的凸函数,这样优化器会自动倾向于保持更低的库存水平以减少边际损耗风险。

6.4 忽略决策的实操性

问题:模型建议每天的价格和补货量剧烈波动,比如今天补货100kg,明天补货5kg,价格也从5元跳到8元。原因:模型只追求数学最优,忽略了实际操作成本(如采购最小起订量、价格标签更换频率、顾客价格感知)。解决方案:在目标函数中加入平滑性惩罚项

  • 补货平滑:在优化目标中增加λ * (今日补货量 - 昨日补货量)²,λ是平滑系数,惩罚补货量的大幅波动。
  • 价格平滑:约束相邻日期的价格变动幅度不超过一定百分比(如5%),或同样在目标函数中加入价格波动惩罚。

7. 竞赛论文写作与代码呈现要点

数学建模竞赛是“模型+论文+代码”的综合比拼。再好的模型,如果表达不清,也难获好评。

论文结构建议:

  1. 问题重述与分析:用你自己的话精炼概括问题,并画出系统流程图,清晰展示“数据输入->预测模型->优化模型->决策输出”的完整逻辑链。
  2. 模型假设与符号说明:明确列出所有假设(如“假设短期内蔬菜采购单价不变”),并用表格清晰定义所有使用的符号、变量。
  3. 模型建立:这是核心。分小节阐述需求预测模型、损耗模型、补货模型、定价模型以及最终的联合优化模型。每一个模型都要有数学公式,并解释其经济学或管理学含义。
  4. 模型求解与算法设计:说明你用了什么算法(如梯度提升树、线性规划)以及为什么选择它。给出算法的伪代码或流程图
  5. 结果分析:用图表说话。展示预测模型的精度、优化前后的利润对比、不同场景下的决策方案。分析结果的敏感性和鲁棒性(例如,当需求预测误差增大10%时,利润会下降多少?)。
  6. 模型评价与推广:客观评价自己模型的优缺点,并提出改进方向。简要说明模型如何推广到其他商品(如水果、鲜肉)。

代码提交技巧:

  • 模块化:将数据清洗、特征工程、模型训练、优化求解、结果输出分别写成独立的函数或脚本(data_preprocessing.py,train_model.py,optimization.py)。
  • 注释清晰:关键步骤、复杂逻辑必须有注释,解释“为什么这么做”。
  • README文件:在代码根目录提供详细的README.md,说明运行环境(Python 3.8+,所需库及版本requirements.txt)、数据存放路径、如何按顺序运行脚本。
  • 结果可复现:设置随机种子(np.random.seed(42),random.seed(42)),确保每次运行代码得到的结果一致。

这道赛题是一个绝佳的数据科学综合练兵场。它迫使你跳出单纯的算法调参,去思考业务逻辑、成本结构、决策闭环。真正的难点往往不在模型的复杂度,而在于对问题的深刻理解和将现实约束转化为数学语言的能力。我的建议是,在动手编码前,花足够多的时间和小组成员一起在白板上梳理清楚整个业务的逻辑图,定义好每一个输入和输出,讨论每一个假设的合理性。磨刀不误砍柴工,前期思考越深入,后期建模就越顺畅,论文写作也越有底气。最后,别忘了在优化目标里加上对“决策稳定性”的考量,这往往是让模型从“理论上最优”走向“实践中可用”的临门一脚。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询