1. 项目概述:从赛题到实战的完整拆解
2023年高教社杯全国大学生数学建模竞赛的C题“蔬菜类商品的自动定价与补货决策”,可以说是一道非常“接地气”的赛题。它没有去探讨那些高深莫测的理论前沿,而是把一个大型商超每天都在面临的、实实在在的经营决策难题,直接抛给了参赛者。这道题的核心,就是要求我们建立一个数学模型,去解决两个核心业务问题:每天该给每种蔬菜定什么价?以及每天该给每种蔬菜补多少货?
听起来像是超市经理的日常工作,对吧?但当你真正拿到题目附件里那几十个Excel表格,里面包含了数百种蔬菜品类过去四年多的日度销售数据、损耗数据、成本与售价数据时,你就会发现,这远不是拍脑袋能决定的事。数据量庞大,关系复杂,季节波动、节假日效应、品类间的替代与互补关系交织在一起,形成了一个典型的、高维的、带约束的时序预测与优化决策问题。这正是数学建模的魅力所在——将模糊的商业直觉,转化为清晰的数学语言和可计算的模型。
这道题非常适合有一定数据分析基础,特别是对时间序列预测和运筹优化感兴趣的同学来深入研究和复现。它不仅能锻炼你从海量数据中提取规律的能力,更能让你体会到建立一个“输入历史数据,输出明日决策”的自动化系统,需要经历怎样的思考过程与技术选型。接下来,我将以一名多次参与并指导数模竞赛的“老手”视角,带你彻底拆解这道赛题,并分享一套经过实战检验的、完整的解决思路与代码框架。我们的目标不是简单地给出答案,而是让你理解每一步“为什么这么做”,以及在实际编程中会遇到哪些“坑”。
2. 核心问题解析与建模总览
面对这样一个问题,我们首先要避免陷入数据的海洋而迷失方向。我们需要用结构化的思维,把大问题分解成几个关键的子问题,并理清它们之间的逻辑关系。
2.1 问题一:销量与损耗的预测
这是所有决策的基础。题目要求我们基于过去的数据,预测未来一周(2023年7月1-7日)6个单品和4个品类的日销量与损耗。这里有三个关键点:
- 预测目标:日销量(
sale_quantity)和日损耗(loss_quantity)。注意损耗数据可能是按成本或售价计算的金额,需要根据题目说明统一量纲。 - 预测对象:既有个别畅销单品(如
花叶类的水生根茎类下的某个具体蔬菜),也有聚合的品类(如所有花叶类蔬菜)。这要求模型既能处理微观波动,也能把握宏观趋势。 - 数据特性:明显的时序数据。我们必须考虑以下因素:
- 趋势性:长期是增长、下降还是平稳?
- 季节性:蔬菜销售有强烈的年度周期(淡旺季)、季度周期(时令菜)和月度周期(如月末采购高峰)。
- 周期性:以周为单位的周期(周末销量高,周中销量低)非常关键。
- 节假日效应:春节、国庆等长假对销量的巨大冲击。
- 外部因素:题目是否提供了天气、促销活动等信息?如果有,则是宝贵的特征。
- 序列相关性:昨天的销量大概率会影响今天。
实操心得:千万不要一上来就用复杂的深度学习模型(如LSTM)。对于这类有明显规律且数据量未必足够大的商业时序问题,传统时序模型(如SARIMA)或树模型(如LightGBM、XGBoost)结合精心构造的特征,往往是更稳健、可解释性更强的选择。我的首选方案是:LightGBM + 丰富的时序特征工程。因为它能很好地处理非线性关系,且对特征中的缺失值和异常值相对鲁棒。
2.2 问题二:单品的定价与补货决策
这是赛题的核心优化部分。在预测出未来销量和损耗的基础上,我们需要为单个商品制定未来一周(7月1-7日)的日补货量和日定价。目标是使得商超的收益最大化。这本质上是一个动态规划或带约束的优化问题。
我们需要定义以下核心要素:
- 决策变量:
price_t(第t天的价格),replenish_t(第t天的补货量)。 - 目标函数:最大化未来一周的总利润(或总收益)。利润 = 销售收入 - 采购成本 - 损耗成本 - 库存持有成本(如有)。
- 约束条件:
- 需求函数约束:销量预测
pred_sale_t是价格price_t的函数。通常,价格越高,需求越少。我们需要从历史数据中拟合出这个“价格-销量”弹性关系。一个常用的简化模型是线性需求函数:pred_sale_t = base_demand_t - k * price_t,其中base_demand_t是问题一预测的、在“基准价格”下的销量,k是价格弹性系数。 - 库存动态平衡:
inventory_t = inventory_{t-1} + replenish_t - actual_sale_t - loss_t。实际销量actual_sale_t不能超过当日库存与补货量之和,且通常等于预测销量与库存中较小者(即不能超卖)。 - 业务规则约束:补货量有上下限(如货架容量、供应商能力);价格有上下限(如不能低于成本价,不能高于市场承受范围);库存不能为负。
- 非负约束:所有决策变量均需非负。
- 需求函数约束:销量预测
注意事项:这里的最大难点在于需求与价格的耦合。我们预测销量时(问题一),假设价格是已知或固定的(如历史平均价)。但在优化时,价格是变量,销量随之变化。这是一个“先有鸡还是先有蛋”的循环。破解方法通常是两阶段法:第一阶段,在假设价格不变(或按某种规则变化)下预测基准销量;第二阶段,在优化模型中,将基准销量作为参数,引入价格弹性系数来动态调整销量预测。
2.3 问题三:品类的定价与补货决策
问题三将决策层面从单品提升到了品类(如所有茄类)。这带来了新的复杂性:
- 品类内单品汇总:品类的销量、损耗、成本、售价都是其下所有单品的汇总。但决策时,我们需要将品类的总补货量和平均定价,合理地分配到各个单品上。
- 品类间的关联:不同品类的蔬菜可能存在替代(西红柿贵了,消费者可能多买黄瓜)或互补(买了辣椒,很可能再买点蒜)关系。这要求模型不能孤立地看待每个品类,而需要考虑它们之间的交叉价格弹性。
建模思路需要升级:
- 预测层面:预测品类总销量时,除了考虑自身历史,还可以加入相关品类的历史价格或销量作为特征,以捕捉替代/互补效应。
- 优化层面:目标函数变为最大化所有品类的总利润。约束条件中,需要为每个品类引入一个需求方程组,其中某个品类的销量是其自身价格及其他相关品类价格的函数。这通常需要利用历史数据,通过回归方法估计出完整的交叉价格弹性矩阵。
- 分配问题:得到品类总补货量后,如何分配给单品?一个实用的方法是根据各单品在过去一段时间内的销售占比或利润贡献占比进行分配。定价也可类似处理,在品类平均价的基础上,根据单品的等级、成本进行微调。
3. 数据预处理与特征工程实战
拿到数据后,切忌直接跑模型。高质量的特征工程决定了模型性能的上限。我们以提供的销售数据为例,进行详解。
3.1 数据清洗与整合
通常数据包含多个表:sales_data.csv(销量),loss_data.csv(损耗),cost_price.csv(成本价),sale_price.csv(售价)。第一步是将其按日期、商品编码或品类编码进行关联,整合成一张“宽表”。
import pandas as pd import numpy as np # 假设读取数据 sales_df = pd.read_csv('sales_data.csv', parse_dates=['sale_date']) loss_df = pd.read_csv('loss_data.csv', parse_dates['loss_date']) cost_df = pd.read_csv('cost_price.csv') price_df = pd.read_csv('sale_price.csv') # 数据合并示例(以单品每日数据为例) # 1. 合并销量与损耗 df = pd.merge(sales_df, loss_df, how='left', left_on=['sale_date', 'prod_code'], right_on=['loss_date', 'prod_code']) # 2. 合并成本与售价(注意成本售价可能是时段性的,需要向前或向后填充) df = pd.merge(df, cost_df, how='left', on=['prod_code']) df = pd.merge(df, price_df, how='left', on=['prod_code']) # 处理合并后的缺失值 df['loss_quantity'].fillna(0, inplace=True) # 假设无损耗记录即为0 # 对于成本售价,可能需要用前一个有效值填充(ffill) df[['cost_price', 'sale_price']] = df.groupby('prod_code')[['cost_price', 'sale_price']].fillna(method='ffill') # 检查重复和异常值 print(df.duplicated().sum()) print(df.describe()) # 重点关注销量、损耗为负或极大的异常值,需结合业务判断处理或截断3.2 核心特征构造
这是提升预测准确性的关键。我们为目标变量y(明日销量)构造特征X。
1. 滞后特征 (Lag Features):这是时序预测的基石。利用过去几天的信息来预测未来。
for lag in [1, 2, 3, 7, 14, 21, 30]: # 分别取昨天、前天、大前天、上周同天、两周前等同天... df[f'sale_lag_{lag}'] = df.groupby('prod_code')['sale_quantity'].shift(lag) df[f'loss_lag_{lag}'] = df.groupby('prod_code')['loss_quantity'].shift(lag)2. 滚动统计特征 (Rolling Statistics):刻画近期趋势和波动。
for window in [3, 7, 14, 30]: df[f'sale_rollmean_{window}'] = df.groupby('prod_code')['sale_quantity'].transform(lambda x: x.rolling(window, min_periods=1).mean()) df[f'sale_rollstd_{window}'] = df.groupby('prod_code')['sale_quantity'].transform(lambda x: x.rolling(window, min_periods=1).std()) # 滚动总和、最小值、最大值、分位数等也很有用3. 时间特征 (Temporal Features):将日期信息转化为模型可理解的格式。
df['day_of_week'] = df['sale_date'].dt.dayofweek # 周一=0,周日=6 df['day_of_month'] = df['sale_date'].dt.day df['month'] = df['sale_date'].dt.month df['year'] = df['sale_date'].dt.year df['is_weekend'] = df['day_of_week'].isin([5,6]).astype(int) # 构造节假日标志(需要外部节假日列表) holiday_list = ['2023-01-01', '2023-01-22', ...] # 示例 df['is_holiday'] = df['sale_date'].isin(pd.to_datetime(holiday_list)).astype(int) # 距离最近节假日的天数(节前备货,节后淡季)4. 价格相关特征:
df['price_cost_ratio'] = df['sale_price'] / df['cost_price'] # 毛利率相关 df['price_change'] = df.groupby('prod_code')['sale_price'].diff() # 价格变动 df['price_rank_in_category'] = df.groupby(['sale_date', 'category_code'])['sale_price'].rank() # 品类内价格排名5. 品类/单品统计特征:
# 品类每日总销量(体现品类热度) category_daily_sale = df.groupby(['sale_date', 'category_code'])['sale_quantity'].sum().rename('category_daily_sale') df = df.merge(category_daily_sale, on=['sale_date', 'category_code']) # 单品在品类内的销量占比 df['sale_share_in_category'] = df['sale_quantity'] / df['category_daily_sale']踩坑实录:构造滞后和滚动特征时,必须严格按时间顺序、按组(单品/品类)进行,并使用
shift和rolling,确保不会发生“数据泄露”——即用未来的信息预测过去。在划分训练集和测试集之后,再在训练集上计算这些特征,然后用类似方法应用到测试集,是常见的错误。正确做法是在整个数据集上先构造特征,然后按日期划分数据集,这样测试集的特征也是基于其“过去”的历史数据计算的,虽然不完美,但更接近实际预测场景。更严谨的做法是使用sklearn的TimeSeriesSplit进行交叉验证。
4. 预测模型构建:LightGBM实战
我们选择LightGBM作为预测模型。它速度快、精度高、能自动处理缺失值,并且对特征缩放不敏感。
4.1 模型训练与验证
import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error, mean_squared_error # 假设df是经过特征工程后的完整数据框 # 1. 定义特征和目标 # 假设我们要预测‘单品A’的销量 target_item = '单品A编码' item_df = df[df['prod_code'] == target_item].copy() item_df = item_df.sort_values('sale_date').reset_index(drop=True) # 按时间排序 # 选择特征列(排除日期、目标列、以及可能泄露未来的列) feature_cols = [col for col in item_df.columns if col not in ['sale_date', 'prod_code', 'sale_quantity', 'loss_quantity', 'actual_sale']] X = item_df[feature_cols] y_sale = item_df['sale_quantity'] # 销量目标 y_loss = item_df['loss_quantity'] # 损耗目标 # 2. 时序交叉验证 # 注意:不能使用随机划分,必须按时间顺序 tss = TimeSeriesSplit(n_splits=5) # 5折时序交叉验证 mae_scores, rmse_scores = [], [] for train_idx, val_idx in tss.split(X): X_train, X_val = X.iloc[train_idx], X.iloc[val_idx] y_train, y_val = y_sale.iloc[train_idx], y_sale.iloc[val_idx] # 创建LightGBM数据集 train_data = lgb.Dataset(X_train, label=y_train) val_data = lgb.Dataset(X_val, label=y_val, reference=train_data) # 设置参数 params = { 'objective': 'regression', # 回归任务 'metric': 'l2', # 使用均方误差 'boosting_type': 'gbdt', 'num_leaves': 31, 'learning_rate': 0.05, 'feature_fraction': 0.9, 'bagging_fraction': 0.8, 'bagging_freq': 5, 'verbose': -1, 'seed': 42 } # 训练 gbm = lgb.train(params, train_data, num_boost_round=1000, valid_sets=[val_data], callbacks=[lgb.early_stopping(stopping_rounds=50), lgb.log_evaluation(100)]) # 预测与评估 y_pred = gbm.predict(X_val, num_iteration=gbm.best_iteration) mae = mean_absolute_error(y_val, y_pred) rmse = np.sqrt(mean_squared_error(y_val, y_pred)) mae_scores.append(mae) rmse_scores.append(rmse) print(f"CV平均MAE: {np.mean(mae_scores):.2f}, 平均RMSE: {np.mean(rmse_scores):.2f}") # 3. 使用全部数据训练最终模型(用于未来预测) final_train_data = lgb.Dataset(X, label=y_sale) final_model = lgb.train(params, final_train_data, num_boost_round=gbm.best_iteration)4.2 预测未来一周
训练好模型后,预测未来一周需要“滚动预测”,因为我们要预测的是多天,而有些特征(如滞后特征)依赖于前一天的预测值。
def rolling_forecast(model, last_known_data, future_dates, feature_cols): """ 滚动预测未来多天 :param model: 训练好的LightGBM模型 :param last_known_data: 包含最近一段时间数据的DataFrame,用于初始化滞后特征 :param future_dates: 需要预测的日期列表 :param feature_cols: 模型使用的特征列名列表 :return: 预测值的列表 """ predictions = [] forecast_df = last_known_data.copy() for forecast_date in future_dates: # 1. 为预测日构造一行数据 # 这里需要根据forecast_date设置时间特征(星期几、月份等) # 价格特征可能需要使用计划价或近期平均价,这里假设使用近期平均价 row = {} row['sale_date'] = forecast_date row['day_of_week'] = forecast_date.dayofweek row['month'] = forecast_date.month # ... 设置其他时间特征 # 2. 计算滞后特征:依赖于之前的真实值或预测值 # 例如,sale_lag_1 应该是昨天的销量(如果是第一天预测,则用历史最后一天的真实值) if len(predictions) == 0: # 预测第一天,滞后特征用历史最后一天的真实值 row['sale_lag_1'] = forecast_df.iloc[-1]['sale_quantity'] else: # 预测后续天,滞后特征用前一天的预测值 row['sale_lag_1'] = predictions[-1] # 同理设置 sale_lag_2, sale_lag_7等,需要回溯历史数据或预测值 # 3. 计算滚动统计特征:基于历史数据+已产生的预测值 # 这里需要维护一个不断延长的“历史序列” temp_series = pd.Series(list(forecast_df['sale_quantity']) + predictions) row['sale_rollmean_7'] = temp_series.tail(7).mean() # ... 计算其他滚动特征 # 4. 将这一行数据转换为模型输入格式,并确保列顺序与训练时一致 input_df = pd.DataFrame([row])[feature_cols] # 5. 进行预测 pred = model.predict(input_df)[0] predictions.append(pred) # 6. (可选)将本次预测值作为一个“临时真实值”加入序列,用于下一天的预测 # 注意:这里不修改原始forecast_df,只是在循环内维护一个临时列表 return predictions # 使用示例 last_30_days_data = item_df.tail(30) # 取最近30天数据作为已知基础 future_dates = pd.date_range(start='2023-07-01', periods=7, freq='D') future_sales = rolling_forecast(final_model, last_30_days_data, future_dates, feature_cols) print(f"未来一周销量预测: {future_sales}")注意事项:滚动预测的误差会累积。第一天的预测误差会影响第二天滞后特征的质量,从而可能放大后续预测的误差。为了缓解这个问题,可以尝试:
- 使用多步直接预测模型,一次性输出未来7天的预测值(但这对模型要求更高)。
- 在滚动预测中,使用历史真实值作为滞后特征的时间尽可能长(例如,
lag_7用上周的真实值,而不是上周的预测值),只在必要时才用预测值填充。- 对预测结果进行后处理校准,例如根据历史预测误差的分布进行调整。
5. 优化模型构建:定价与补货决策
预测完成后,我们进入优化阶段。这里以问题二(单品决策)为例,展示如何用Python的PuLP或SciPy库来建模求解。
5.1 定义优化问题
假设我们已经有了未来7天(t=1 to 7)的基准销量预测base_demand_t(来自问题一模型,假设是在历史平均价格下的预测),以及采购成本cost_t,初始库存I0,价格弹性系数k(通过历史数据回归估计得出)。
我们的目标是最大化总利润:
Maximize:Σ_{t=1}^{7} [ (price_t * sale_t) - (cost_t * replenish_t) - (loss_cost_t) ]
Subject to:
- 需求函数:
sale_t = base_demand_t - k * (price_t - base_price)。base_price是计算base_demand_t时所用的基准价格。 - 库存平衡:
I_t = I_{t-1} + replenish_t - sale_t - loss_t。其中loss_t可以用预测的损耗率乘以库存来估算。 - 销量上限:
sale_t <= I_{t-1} + replenish_t(不能超卖)。 - 销量非负:
sale_t >= 0。 - 补货量上下限:
replenish_min <= replenish_t <= replenish_max。 - 价格上下限:
price_min <= price_t <= price_max。 - 库存非负:
I_t >= 0。
5.2 使用PuLP求解
from pulp import LpProblem, LpVariable, LpMaximize, LpStatus, value # 定义参数 T = 7 # 7天 base_demand = future_sales # 假设这是我们预测的基准销量列表 base_price = 5.0 # 基准价格,假设为5元 k = 10.0 # 价格弹性系数,价格每上涨1元,销量减少10单位(需根据历史数据拟合) cost = [3.0] * T # 未来7天成本,假设固定为3元/单位 I0 = 100 # 初始库存 replenish_min, replenish_max = 50, 200 # 日补货量上下限 price_min, price_max = 3.5, 8.0 # 售价上下限(需高于成本) loss_rate = 0.02 # 日损耗率,假设为2% # 创建问题 prob = LpProblem("Vegetable_Pricing_Replenishment", LpMaximize) # 创建决策变量 price_vars = LpVariable.dicts("Price", range(T), lowBound=price_min, upBound=price_max) replenish_vars = LpVariable.dicts("Replenish", range(T), lowBound=replenish_min, upBound=replenish_max) sale_vars = LpVariable.dicts("Sale", range(T), lowBound=0) # 销量非负 inventory_vars = LpVariable.dicts("Inventory", range(T), lowBound=0) # 库存非负 # 注意:sale_vars 还需要满足库存约束,这将在下面添加 # 设置目标函数 profit_expr = 0 for t in range(T): revenue = price_vars[t] * sale_vars[t] purchase_cost = cost[t] * replenish_vars[t] loss_cost = loss_rate * inventory_vars[t] * cost[t] # 简化:损耗成本=损耗率*库存*成本价 profit_expr += (revenue - purchase_cost - loss_cost) prob += profit_expr # 添加约束 for t in range(T): # 约束1: 需求函数 (线性) prob += sale_vars[t] == base_demand[t] - k * (price_vars[t] - base_price), f"Demand_Function_{t}" # 约束2 & 3: 库存平衡与销量上限 if t == 0: prob += inventory_vars[t] == I0 + replenish_vars[t] - sale_vars[t] - loss_rate * I0, f"Inventory_Balance_{t}" else: prob += inventory_vars[t] == inventory_vars[t-1] + replenish_vars[t] - sale_vars[t] - loss_rate * inventory_vars[t-1], f"Inventory_Balance_{t}" # 销量不能超过可用库存(前一天库存+当天补货) if t == 0: prob += sale_vars[t] <= I0 + replenish_vars[t], f"Sale_Limit_{t}" else: prob += sale_vars[t] <= inventory_vars[t-1] + replenish_vars[t], f"Sale_Limit_{t}" # 求解问题 prob.solve() print(f"求解状态: {LpStatus[prob.status]}") # 输出结果 if prob.status == 1: # Optimal print("最优决策方案:") for t in range(T): print(f"Day {t+1}: Price={value(price_vars[t]):.2f}, Replenish={value(replenish_vars[t]):.1f}, " f"Sale={value(sale_vars[t]):.1f}, Inventory={value(inventory_vars[t]):.1f}") print(f"预计总利润: {value(prob.objective):.2f}") else: print("未找到最优解。")实操心得:线性规划(LP)求解器(如PuLP默认的CBC)对这类中小规模问题非常有效。但上述模型将需求函数简化为线性,这在实际中可能过于理想。更复杂的非线性关系(如价格对销量的指数影响)或整数约束(补货按箱计算),可能需要用到非线性规划(NLP)或混合整数规划(MIP)求解器(如
PuLP支持调用Gurobi,CPLEX等商业求解器,或开源的SCIP)。如果问题规模很大(成百上千商品),可能需要设计启发式算法(如遗传算法、模拟退火)来求近似最优解。
6. 品类决策与分配问题实现
问题三的复杂性在于品类间的关联和内部的分配。这里概述关键步骤。
6.1 估计交叉价格弹性
假设我们有3个品类(A, B, C)。我们可以为每个品类建立一个需求方程,其中销量受自身价格和其他品类价格影响。
Sale_A = α_A - β_AA * Price_A + β_AB * Price_B + β_AC * Price_C + ... (其他因素)Sale_B = α_B + β_BA * Price_A - β_BB * Price_B + β_BC * Price_C + ...Sale_C = ...
其中,β_ij (i≠j)就是交叉价格弹性。β_ij > 0表示品类i和j是替代品(j涨价,i销量增)。β_ij < 0表示是互补品(j涨价,i销量减)。
我们可以用历史数据,对每个品类进行多元线性回归来估计这些系数。特征包括:自身价格、其他品类价格、时间特征、历史销量滞后项等。
import statsmodels.api as sm # 假设 df_agg 是按天、按品类聚合后的数据,包含各品类的日销量和日均售价 # 例如,列有:date, cat_A_sale, cat_A_price, cat_B_sale, cat_B_price, cat_C_sale, cat_C_price, ... # 为品类A构建回归模型 X_a = df_agg[['cat_A_price', 'cat_B_price', 'cat_C_price', 'day_of_week', 'month', 'cat_A_sale_lag1', 'cat_A_sale_lag7']] X_a = sm.add_constant(X_a) # 添加截距项 y_a = df_agg['cat_A_sale'] model_a = sm.OLS(y_a, X_a).fit() print(model_a.summary()) # 从结果中读取 cat_B_price 和 cat_C_price 的系数,即为交叉弹性 β_AB 和 β_AC。6.2 品类级优化建模
在得到需求方程组后,品类级的优化模型与单品级类似,但决策变量是每个品类的价格和总补货量,目标函数是三个品类利润之和,约束中包含上述联立的需求方程组。这形成了一个更复杂的优化问题,通常仍可用线性/非线性规划求解,只要将需求方程作为约束代入。
6.3 单品分配策略
得到品类总补货量Replenish_Cat后,如何分配给下属的N个单品item_1, ..., item_N?
一个简单有效的策略是按历史销售比例分配:
- 计算过去M天(如30天)每个单品的平均日销量
avg_sale_i。 - 计算每个单品的销售占比
share_i = avg_sale_i / sum(avg_sale_i)。 - 单品补货量
replenish_i = Replenish_Cat * share_i。
更精细的策略可以考虑单品的利润率、损耗率、当前库存和保质期。例如,可以构建一个单品级的二次优化,在满足品类总补货量的约束下,分配方案使得单品总利润最大或总损耗最小。
定价分配也可以类似处理:先确定品类基准价,然后根据单品的成本、等级、历史价格进行上下浮动。
7. 完整代码框架与避坑指南
将以上所有步骤整合,形成一个完整的、模块化的代码框架至关重要。
7.1 项目目录结构建议
vegetable_pricing_replenishment/ ├── data/ │ ├── raw/ # 存放原始赛题数据 │ └── processed/ # 存放清洗合并后的数据 ├── src/ │ ├── data_preprocessing.py # 数据清洗、合并、特征工程 │ ├── forecasting.py # 销量/损耗预测模型 │ ├── elasticity_estimation.py # 价格弹性与交叉弹性估计 │ ├── optimization.py # 定价与补货优化模型 │ └── utils.py # 工具函数(评估指标、可视化等) ├── config.yaml # 配置文件(路径、参数) ├── main.py # 主程序,串联整个流程 └── requirements.txt # 项目依赖7.2 核心避坑指南与常见问题
数据泄露:这是新手最容易犯的错误。永远不要在全局数据上计算统计特征(如均值、标准差)后再划分训练测试集。正确的做法是:在训练集上计算统计量,然后用这些统计量去转换测试集。对于时序问题,更安全的做法是使用
sklearn的Pipeline结合TimeSeriesSplit。特征工程过拟合:不要盲目构造大量特征。使用特征重要性(LightGBM自带)或递归特征消除(RFE)来选择对预测最有用的特征。对于滞后特征,可以通过相关性分析确定最佳的滞后阶数。
预测与优化的循环依赖:如前所述,预测模型假设价格已知,优化模型又需要预测的销量。在实际系统中,这可能是一个迭代或动态调整的过程。在竞赛的静态环境下,我们采用“两阶段法”是合理的。但在方案阐述中,需要明确指出这个假设及其局限性。
模型评估不恰当:对于时序预测,不要用简单的随机划分验证。务必使用时序交叉验证(TimeSeriesSplit)或滚动窗口验证。评估指标也不要用
R²,而应该用MAE(平均绝对误差)、RMSE(均方根误差)或MAPE(平均绝对百分比误差,注意分母为零的情况)等业务相关指标。优化问题不可行或无解:检查约束条件是否相互矛盾。例如,价格下限设得过高,可能导致需求函数计算出的销量为负数,进而违反非负约束。初始参数设置要合理。可以尝试逐步放松约束,或检查求解器的日志输出。
代码效率:处理数百个单品几年数据时,循环操作可能极慢。尽量使用
Pandas的向量化操作和groupby。对于需要为每个单品单独训练模型的情况,可以考虑使用joblib进行并行计算。结果的可解释性与稳定性:数学建模竞赛不仅看结果,更看重建模过程的合理性。确保你的模型结果符合业务常识(例如,价格提高,预测销量应下降)。对关键参数(如价格弹性k)进行敏感性分析,说明其变化对最终利润的影响,能极大提升论文的说服力。
这套从数据到预测,再到优化决策的完整框架,不仅适用于这道赛题,也适用于许多类似的供应链管理、零售库存优化问题。真正的挑战在于根据具体数据和业务背景,灵活调整和细化每一个模块。记住,没有一劳永逸的模型,只有不断迭代和贴近业务逻辑的解决方案。