数学建模竞赛实战:基于Python的蔬菜定价与补货决策系统构建

数学建模竞赛实战:基于Python的蔬菜定价与补货决策系统构建
1. 从赛题到实战一次完整的数学建模竞赛复盘去年带队参加国赛我们组选的正是C题“蔬菜类商品的自动定价与补货决策”。这道题乍一看是供应链管理问题但内核其实是一场对数据处理、模型构建和编程实现能力的综合考验。很多队伍拿到题目后容易陷入两个极端要么一头扎进复杂的理论模型里出不来要么在数据处理阶段就耗费了过多时间导致最后模型“纸上谈兵”代码跑不通。我们当时也踩了不少坑最终通过一套清晰的解题框架和可落地的代码实现了从数据清洗、特征工程、模型选择到决策输出的完整闭环。今天我就把这套经过实战检验的思路和核心代码逻辑拆解出来重点不是给你一个“黑箱”代码包而是让你理解每一步“为什么这么做”以及在实际编程中如何避开那些教科书上不会写的“暗礁”。这道题的核心目标很明确基于历史销售数据为超市的蔬菜类商品制定未来的自动定价策略和补货计划。这本质上是一个典型的“数据驱动决策”问题它要求我们同时处理两个相互关联的子问题需求预测和库存优化。定价会影响需求需求决定了补货量而补货成本又反过来制约定价空间。因此孤立地看任何一个问题都是不行的必须建立一个联动的系统。我们的整体思路是先通过历史数据预测未来一段时间内比如未来一周每种蔬菜的“基准需求”然后构建一个以总利润最大化为目标的优化模型在这个模型中价格和补货量是决策变量需求预测结果是核心输入再考虑库存成本、损耗率、采购成本等约束条件最终求解出最优的定价与补货方案。下面我就分步骤带你走完这个全过程。2. 数据预处理比建模更关键的“地基工程”拿到组委会提供的数据通常是Excel或CSV格式第一步绝不是直接导入模型。真实数据往往充满“噪音”直接使用会导致模型失效。我们的预处理流程分为四步数据读取与探查、缺失值与异常值处理、数据转换与衍生、数据集构建。2.1 数据读取与初步探查我们使用Python的Pandas库这是处理表格数据的利器。第一步是全面了解数据结构和内容。import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 假设数据文件为 sales_data.csv 和 product_info.csv sales_df pd.read_csv(sales_data.csv, encodingutf-8) product_df pd.read_csv(product_info.csv, encodingutf-8) # 查看数据概览 print(销售数据形状:, sales_df.shape) print(sales_df.info()) print(sales_df.head()) print(\n商品信息形状:, product_df.shape) print(product_df.info()) # 查看基本统计量重点关注数值型字段 print(sales_df.describe())这里需要特别关注几个点时间字段的格式是否已转为datetime类型、商品ID的唯一性、销售数量和金额是否存在负值或零值可能是退货或数据错误、价格字段的分布是否合理。通过df.describe()可以快速发现一些统计上的异常比如销售数量的最大值如果远大于均值的三倍标准差就可能是一个离群点。2.2 缺失值与异常值处理稳健模型的起点蔬菜销售数据中缺失值可能出现在多个地方。我们的处理原则是根据业务逻辑进行填充或删除而不是简单粗暴地使用均值。# 检查缺失值 print(销售数据缺失情况:\n, sales_df.isnull().sum()) print(商品信息缺失情况:\n, product_df.isnull().sum()) # 处理销售数据中的缺失值 # 假设‘sale_quantity’销售量有缺失如果缺失很少且随机可以考虑用同品类商品同期的均值填充 # 但更稳妥的方法是如果某条记录的关键字段如商品ID、日期缺失直接删除该行。 sales_df_clean sales_df.dropna(subset[product_id, sale_date, sale_quantity]) # 对于价格等字段的缺失可以用该商品前一周的平均价格填充 sales_df_clean[price] sales_df_clean.groupby(product_id)[price].transform( lambda x: x.fillna(x.rolling(7, min_periods1).mean()) ) # 处理异常值使用统计学方法结合业务判断 # 例如定义销售量异常值为超出“均值 ± 3倍标准差”的范围 def cap_outliers(series): Q1 series.quantile(0.25) Q3 series.quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR # 将异常值缩放到边界上而不是直接删除以保留数据规模 return series.clip(lower_bound, upper_bound) sales_df_clean[sale_quantity] sales_df_clean.groupby(product_id)[sale_quantity].transform(cap_outliers)注意对于促销期的销量暴增这不算“异常值”而是重要的业务信号。因此在应用统计方法前最好先根据“是否促销”等字段将数据分组分别处理。我们当时就因为没有区分平日和促销日导致初期模型严重低估了促销期的需求弹性这是第一个大坑。2.3 特征工程从原始数据中“炼金”这是提升模型性能的关键。我们需要从现有的时间、商品、销售数据中构造出对预测需求有用的特征。# 将日期转为datetime格式并提取时间特征 sales_df_clean[sale_date] pd.to_datetime(sales_df_clean[sale_date]) sales_df_clean[year] sales_df_clean[sale_date].dt.year sales_df_clean[month] sales_df_clean[sale_date].dt.month sales_df_clean[day] sales_df_clean[sale_date].dt.day sales_df_clean[dayofweek] sales_df_clean[sale_date].dt.dayofweek # 周一0周日6 sales_df_clean[is_weekend] sales_df_clean[dayofweek].isin([5, 6]).astype(int) # 创建滞后特征过去1天、3天、7天的销量对于预测明天销量非常有用 sales_df_clean sales_df_clean.sort_values([product_id, sale_date]) for lag in [1, 3, 7]: sales_df_clean[flag_{lag}] sales_df_clean.groupby(product_id)[sale_quantity].shift(lag) # 创建滚动统计特征过去7天的平均销量、标准差 sales_df_clean[rolling_mean_7] sales_df_clean.groupby(product_id)[sale_quantity].transform( lambda x: x.rolling(7, min_periods1).mean() ) sales_df_clean[rolling_std_7] sales_df_clean.groupby(product_id)[sale_quantity].transform( lambda x: x.rolling(7, min_periods1).std() ) # 合并商品信息如品类、储存条件、进货成本等 sales_df_clean pd.merge(sales_df_clean, product_df, onproduct_id, howleft) # 计算一些衍生特征如价格弹性需后续模型拟合这里可先占位 # 或者简单的“价格变化率” sales_df_clean[price_change] sales_df_clean.groupby(product_id)[price].pct_change()特征工程后数据集就准备好了。我们需要为每个商品单独或分组同品类构建时间序列数据集用于下一步的需求预测。3. 需求预测模型寻找销量背后的规律需求预测是整个决策系统的输入它的准确性直接决定后续优化结果的好坏。我们对比了多种方法最终为不同特性的商品选择了不同的模型。3.1 模型选型没有银弹只有合适传统时间序列模型如ARIMA、Prophet适用于销量稳定、有明显季节性和趋势的商品。例如土豆、洋葱等日常消耗品。优点是模型可解释性强参数调优有章可循。缺点是对突发促销、外部事件如天气的捕捉能力弱。机器学习模型如LightGBM、XGBoost适用于销量波动大、受多因素影响的商品。例如绿叶蔬菜、时令水果。它能很好地利用我们上面构造的滞后特征、滚动特征、价格特征以及外部特征如天气、节假日。优点是拟合能力强能处理复杂关系。缺点是需要足够的数据量且可解释性相对较差。深度学习模型如LSTM理论上能捕捉更长期、更复杂的序列依赖。但在国赛有限的数据量和时间下训练和调参成本过高容易过拟合我们最终没有采用。我们的策略是先使用LightGBM作为基线模型因为它对特征工程友好、训练速度快、且能自动处理缺失值。对于LightGBM预测效果不佳误差大的少数商品再尝试用Prophet进行补充建模。3.2 基于LightGBM的需求预测实战这里以单个商品为例展示完整的建模流程。import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error, mean_squared_error # 假设我们已为商品‘A001’准备好了数据集 df_item df_item sales_df_clean[sales_df_clean[product_id] A001].copy() df_item df_item.dropna() # 删除因创建滞后特征产生的初始空行 # 定义特征和目标变量 # 注意不能使用未来的信息例如预测t日的销量只能使用t-1日及之前的数据作为特征。 features [lag_1, lag_3, lag_7, rolling_mean_7, rolling_std_7, price, price_change, dayofweek, is_weekend, month] target sale_quantity X df_item[features] y df_item[target] # 时间序列交叉验证不能用随机划分必须按时间顺序 tscv TimeSeriesSplit(n_splits5) mae_scores [] for train_index, test_index in tscv.split(X): X_train, X_test X.iloc[train_index], X.iloc[test_index] y_train, y_test y.iloc[train_index], y.iloc[test_index] # 创建并训练LightGBM模型 model lgb.LGBMRegressor( n_estimators200, learning_rate0.05, max_depth5, random_state42, verbosity-1 # 不输出训练信息 ) model.fit(X_train, y_train, eval_set[(X_test, y_test)], eval_metricmae, callbacks[lgb.early_stopping(stopping_rounds20)]) y_pred model.predict(X_test) mae mean_absolute_error(y_test, y_pred) mae_scores.append(mae) print(fFold MAE: {mae:.2f}) print(f平均MAE: {np.mean(mae_scores):.2f}) # 用全部数据训练最终模型用于预测未来 final_model lgb.LGBMRegressor(**model.get_params()) final_model.fit(X, y) # 预测未来7天这里需要构建未来7天的特征数据框 X_future # 这是一个难点因为未来特征如‘lag_1’依赖于预测值本身。 # 常用方法是使用迭代预测用预测出的t日销量作为t1日的‘lag_1’特征依次类推。 def iterative_forecast(model, last_known_data, features, steps7): last_known_data: 包含最近几天真实数据的DataFrame一行 features: 特征名列表 steps: 预测步长 forecast [] current_features last_known_data[features].copy().values.reshape(1, -1) for i in range(steps): pred model.predict(current_features)[0] forecast.append(pred) # 更新特征为下一次预测做准备简化版实际需更新所有滞后和滚动特征 # 这里假设特征顺序固定且‘lag_1’在第一个位置 new_features current_features[0].copy() # 将本次预测值作为下一次的‘lag_1’ new_features[0] pred # 假设features[0]是‘lag_1’ # 其他滞后特征需要相应顺延滚动特征需要重新计算此处简化 # 这是一个复杂的逻辑需要根据特征构造规则仔细实现 current_features new_features.reshape(1, -1) return forecast # 获取最后一条已知数据 last_row df_item.iloc[-1:] future_demand iterative_forecast(final_model, last_row, features, steps7) print(f商品 A001 未来7天预测销量: {future_demand})踩坑实录在迭代预测时我们最初只更新了lag_1忽略了rolling_mean_7等滚动特征也需要用预测值来更新。这导致预测误差随着预测步长增加而快速放大。后来我们重写了特征更新函数确保所有依赖于历史值的特征在每一步预测后都得到正确更新预测稳定性才大幅提升。3.3 预测结果的后处理与评估预测出的需求值不能直接用于优化模型还需要进行后处理。非负约束销量不能为负数使用np.maximum(forecast, 0)。整数化销量通常是整数可以四舍五入。业务约束考虑最小销售单位如按斤卖。不确定性量化对于优化模型知道预测的置信区间比一个点估计更有价值。我们可以通过计算交叉验证中预测误差的分布来估计未来需求可能的波动范围这将在后续优化模型中作为鲁棒性约束。4. 定价与补货联合优化模型有了未来一段时间如一周的需求预测d_tt表示第t天我们就可以构建优化模型了。核心思想是建立一个数学规划模型决策变量是未来每天的价格 p_t和补货量 q_t目标函数是总利润最大化。4.1 模型定义目标与约束我们假设需求受价格影响采用一个简单的线性需求函数d_t(p_t) base_demand_t - elasticity * p_t。其中base_demand_t是上面预测出的、在某个参考价格下的需求elasticity是价格弹性系数可以通过历史数据回归估计。目标函数最大化总利润总利润 总收入 - 总成本 总收入 Σ (p_t * min(实际销量_t, 库存_t)) 总成本 采购成本 库存持有成本 缺货/损耗成本更精确地我们建立如下模型设决策变量p_t: 第t天的销售单价q_t: 第t天开始的补货量假设凌晨补货当天可售I_t: 第t天结束时的库存量模型参数d_t: 第t天的预测基准需求来自上一节e: 价格弹性系数负值c: 商品单位进货成本h: 单位库存日持有成本s: 单位缺货惩罚成本或机会损失w: 单位商品损耗率如蔬菜每日自然损耗I_max: 最大库存容量p_min,p_max: 价格允许波动范围目标函数Maximize Σ [ p_t * min( d_t e*(p_t - p_ref), I_{t-1}q_t ) - cq_t - hI_t - s*max(0, demand_t - (I_{t-1}q_t)) ]约束条件库存平衡方程I_t (1-w) * (I_{t-1} q_t - sales_t)其中sales_t min( demand_t, I_{t-1}q_t )库存容量约束0 I_t I_max补货量非负q_t 0价格范围约束p_min p_t p_max需求函数demand_t max(0, d_t e * (p_t - p_ref))确保需求非负这是一个包含非线性项min/max的优化问题直接求解比较困难。常用的线性化方法或启发式算法在国赛时间内实现起来有挑战。我们采用了一种分步迭代优化的实用策略。4.2 实用求解策略分步迭代与模拟在有限竞赛时间内追求绝对最优解不现实我们的目标是得到一个明显优于规则策略的、可行的、可解释的解决方案。第一步价格决策基于需求弹性对于每个商品如果我们已经估计出价格弹性e那么理论上存在一个使单日利润最大化的价格p_t^*。可以通过求解d(p) * (p-c)的极大值来得到假设需求函数为线性。这是一个一元二次方程求极值的问题解析解为p_t^* (base_demand_t / (-e) c) / 2。然后将这个理论最优价格约束在[p_min, p_max]范围内。def calculate_optimal_price(base_demand, elasticity, cost, p_min, p_max): 计算单日利润最大化价格线性需求函数假设下 elasticity 应为负值 if elasticity 0: # 弹性非负不符合常识使用保守策略 return (p_min p_max) / 2 theoretical_opt_price (base_demand / (-elasticity) cost) / 2 # 施加价格上下限约束 constrained_price max(p_min, min(theoretical_opt_price, p_max)) return constrained_price第二步基于价格决策的需求修正将第一步计算出的价格p_t代入需求函数得到考虑价格影响后的“期望需求”expected_demand_t。第三步补货决策报童模型思想补货问题可以简化为一个经典的“报童模型”变种每天早晨我们需要决定补货量q_t使得在满足当天“期望需求”的同时平衡库存持有成本和潜在的缺货/损耗成本。 一个简化的启发式公式是q_t max(0, expected_demand_t - I_{t-1} safety_stock)。 其中safety_stock是安全库存用来应对需求不确定性。安全库存可以根据历史需求波动标准差和服务水平要求来计算例如safety_stock z * std_devz是服务水平对应的Z值如95%服务水平对应Z≈1.65。def calculate_order_quantity(expected_demand, current_inventory, safety_stock, max_capacity): 计算补货量 # 计算为了达到目标库存水平所需的量 target_inventory expected_demand safety_stock order_qty target_inventory - current_inventory # 确保非负且不超过最大库存容量限制 order_qty max(0, order_qty) if current_inventory order_qty max_capacity: order_qty max_capacity - current_inventory return order_qty第四步模拟仿真与迭代优化将前三步得到的{p_t, q_t}策略代入一个库存模拟器中按照库存平衡方程模拟未来一周或更长的运营情况计算出实际的总利润、平均库存水平、缺货率等指标。def simulate_inventory(demand_forecast, price_series, order_series, init_inventory, cost, hold_cost, shortage_cost, waste_rate): 模拟给定价格和补货策略下的库存动态与利润 demand_forecast: 预测的需求列表 price_series: 价格策略列表 order_series: 补货策略列表 total_profit 0 inventory init_inventory for t in range(len(demand_forecast)): # 当天开始时的可用库存 available inventory order_series[t] # 实际销量受需求和库存双重限制 actual_sales min(demand_forecast[t], available) # 收入 revenue actual_sales * price_series[t] # 采购成本 procurement_cost order_series[t] * cost # 计算损耗后的期末库存 leftover available - actual_sales inventory leftover * (1 - waste_rate) # 损耗 # 库存持有成本按期末库存算 holding_cost inventory * hold_cost # 缺货成本 shortage max(0, demand_forecast[t] - available) shortage_cost_t shortage * shortage_cost daily_profit revenue - procurement_cost - holding_cost - shortage_cost_t total_profit daily_profit return total_profit如果模拟结果不理想如利润低或缺货率高我们可以调整策略例如调整价格弹性系数的估计值。调整安全库存系数z。在价格决策中引入库存压力因子库存高时适当降价促销库存低时提价保利润。通过几轮这样的“策略生成 → 模拟评估 → 参数调整”的迭代我们可以快速找到一个表现不错的可行解。这种方法虽然不一定是最优解但逻辑清晰、易于实现、可解释性强非常适合数学建模竞赛。5. 代码整合与系统实现前面分模块介绍了核心思路现在需要将它们整合成一个可以运行的决策系统。系统的工作流程是输入历史数据 - 训练预测模型 - 预测未来需求 - 运行优化器生成策略 - 输出定价与补货计划表。5.1 核心类设计我们设计一个VegetablePricingReplenishmentSystem类来封装所有功能。class VegetablePricingReplenishmentSystem: def __init__(self, sales_data_path, product_data_path): self.sales_df None self.product_df None self.models {} # 存储每个商品的预测模型 self.forecasts {} # 存储每个商品的预测结果 self.strategies {} # 存储每个商品的最终策略 self.load_and_preprocess_data(sales_data_path, product_data_path) def load_and_preprocess_data(self, sales_path, product_path): 加载并预处理数据 # ... (集成第2节的数据预处理代码) print(数据加载与预处理完成。) def train_demand_forecast_models(self, product_listNone): 为指定商品列表训练需求预测模型 if product_list is None: product_list self.sales_df[product_id].unique() for pid in product_list: print(f训练商品 {pid} 的需求预测模型...) df_item self.sales_df[self.sales_df[product_id] pid].copy() # ... (集成第3.2节的模型训练代码) # 将训练好的模型存入 self.models[pid] # 将未来7天的预测结果存入 self.forecasts[pid] print(所有模型训练完成。) def estimate_price_elasticity(self, product_id): 估计商品的价格弹性系数简化版使用历史数据线性回归 df_item self.sales_df[self.sales_df[product_id] product_id].copy() # 确保数据按时间排序 df_item df_item.sort_values(sale_date) # 使用价格和销量的对数进行回归可能更稳定 # 这里使用简单线性回归作为示例 from sklearn.linear_model import LinearRegression X df_item[[price]].values y df_item[sale_quantity].values # 添加截距项 X np.concatenate([np.ones((len(X), 1)), X], axis1) model LinearRegression().fit(X, y) # 弹性系数大致是回归系数价格对销量的影响 elasticity model.coef_[1] if len(model.coef_) 1 else 0 # 通常弹性为负这里取个负号使其在需求函数中为负值 return -abs(elasticity) * 10 # 乘以一个缩放因子使其量级合理 def generate_strategy(self, product_id, forecast_days7): 为核心商品生成未来7天的定价与补货策略 base_demands self.forecasts[product_id] # 预测的基准需求列表 cost self.product_df[self.product_df[product_id]product_id][cost].values[0] p_min, p_max 0.5, 5.0 # 示例价格上下限 init_inventory 100 # 示例初始库存 max_capacity 500 # 示例最大库存 elasticity self.estimate_price_elasticity(product_id) prices [] orders [] inventory init_inventory for t in range(forecast_days): # 1. 定价决策 opt_price calculate_optimal_price(base_demands[t], elasticity, cost, p_min, p_max) prices.append(opt_price) # 2. 需求修正 expected_demand max(0, base_demands[t] elasticity * (opt_price - (p_minp_max)/2)) # 3. 补货决策简化安全库存计算 safety_stock 0.2 * expected_demand # 假设安全库存为期望需求的20% order_qty calculate_order_quantity(expected_demand, inventory, safety_stock, max_capacity) orders.append(order_qty) # 4. 更新库存简单模拟忽略损耗 sales min(expected_demand, inventory order_qty) inventory inventory order_qty - sales self.strategies[product_id] { dates: pd.date_range(startpd.Timestamp.today(), periodsforecast_days, freqD), prices: prices, order_quantities: orders } return self.strategies[product_id] def output_strategy_table(self, output_pathstrategy_output.csv): 输出所有商品的策略到CSV文件 all_strategies [] for pid, strat in self.strategies.items(): df_temp pd.DataFrame(strat) df_temp[product_id] pid all_strategies.append(df_temp) result_df pd.concat(all_strategies, ignore_indexTrue) result_df result_df[[product_id, dates, prices, order_quantities]] result_df.to_csv(output_path, indexFalse, encodingutf-8-sig) print(f策略已输出至 {output_path}) return result_df5.2 主程序执行流程if __name__ __main__: # 初始化系统 system VegetablePricingReplenishmentSystem(historical_sales.csv, product_info.csv) # 训练预测模型可以只选部分重要商品以节省时间 important_products [A001, B002, C003] # 示例 system.train_demand_forecast_models(product_listimportant_products) # 为每个商品生成策略 for pid in important_products: system.generate_strategy(pid, forecast_days7) # 输出最终决策表 final_strategy_df system.output_strategy_table(final_decision_next_week.csv) # 打印预览 print(final_strategy_df.head(10))这个系统框架提供了完整的流水线。在实际比赛中你需要根据题目给出的具体数据格式和问题要求调整数据预处理细节、需求预测模型可能需要对不同品类用不同模型、优化模型的目标函数和约束条件特别是损耗率、库存成本等具体参数。核心在于理解每个模块的输入输出和它们之间的逻辑关系而不是死记硬背代码。6. 竞赛实战心得与避坑指南结合我们参赛和后续复盘的经验有几个关键点需要特别注意这些往往决定了论文的深度和成绩。第一模型假设的明确性与合理性。论文中必须清晰说明你的需求函数形式为什么用线性、优化模型的目标和约束为什么包含这些成本项。例如蔬菜损耗率可能不是固定的而是和库存时间正相关你能建模吗如果题目数据支持尝试更复杂的函数如指数损耗并说明理由这是重要的加分项。第二参数估计的严谨性。价格弹性系数e和安全库存系数z不能拍脑袋决定。在论文中你需要展示如何从历史数据中估计这些参数。例如用统计回归方法估计弹性用历史需求波动分布确定服务水平对应的z值。即使方法简单过程透明也能体现严谨性。第三系统鲁棒性分析。评委喜欢看到你对模型“脆弱性”的思考。可以做敏感性分析如果预测误差增大10%总利润会下降多少如果进货成本突然上涨你的策略如何调整通过模拟不同场景展示你的策略在不确定性下的表现。第四可视化与结果解读。不要只扔出一堆数字和表格。用图表说话用折线图展示某商品未来一周的价格与补货决策。用柱状图对比不同策略下的利润、库存水平。用热力图展示不同品类商品的最优价格区间。 清晰的图表能极大提升论文的可读性和专业性。第五代码的整洁与可复现性。虽然论文主体不展示全部代码但附录或支撑材料中提供的代码应结构清晰、注释完整。使用函数和类来组织代码避免冗长的脚本。确保提供的代码能够用提供的数据跑出论文中的关键结果。最后也是最重要的一点时间管理。国赛时间紧不要试图实现一个完美无缺的复杂系统。采用“快速原型-迭代改进”的策略。先搭建一个最简单的可运行流程比如用移动平均预测用简单公式决策确保整个逻辑闭环能跑通。然后再逐步替换其中模块为更精细的模型如将移动平均换成LightGBM。这样即使最后时间不够你也有一个完整的、可展示的解决方案远比一个只有复杂设想但未实现的半成品要强。

最新新闻

日新闻

周新闻

月新闻