基于LightGBM与MIP的小批量生产调度预测优化实战
1. 项目概述与核心价值
看到“2022年全国大学生数学建模竞赛E题-小批量物料生产安排”这个标题,很多参加过数模竞赛或者对生产调度感兴趣的朋友应该会心一笑。这题目可以说是经典中的经典,它把一个看似抽象的“安排”问题,具体化到了一个非常真实的工业场景里:你不是在生产海量的标准品,而是在处理一系列种类多、批量小、工艺链可能还特别长的定制化订单。这恰恰是当前许多高端制造、医疗器械、航空航天零部件生产面临的常态。题目要求你做的,远不止是排个时间表那么简单,它本质上是在考验你如何用数学和算法的语言,去刻画并优化一个充满不确定性和复杂约束的随机动态系统。
我自己带学生打比赛以及在实际的工业软件项目中,处理过大量类似问题。这道E题的魅力在于,它完美地融合了运筹学、统计学和计算机科学。你需要预测未来物料到达的“时间”和“数量”(时序预测),需要在机器、模具、人力等多种资源约束下做出“安排”(调度优化),还需要考虑订单优先级、生产切换成本、库存成本等一系列经济指标。这几乎是一个微缩版的“高级计划与排程(APS)”系统核心问题。而Python,凭借其强大的科学计算库和清晰的语法,成为了解决此类问题的不二之选。本文将围绕如何构建一个完整的求解框架展开,重点拆解其中的时序预测模型部分,并分享从思路到代码实现的全流程实战经验。
2. 题目深度解析与核心问题拆解
2.1 问题场景与核心挑战
我们先抛开数学术语,用大白话还原一下题目场景。你是一家工厂的生产主管,每天会收到未来一段时间内各种物料的生产需求订单。这些订单有几个特点:1)小批量:每种物料要生产的数量不多,但种类繁多;2)随机性:物料什么时候到、一次到多少,不是固定的,但有历史规律可循;3)多工序:物料需要经过多台机器或工作站按顺序加工;4)资源约束:机器、工人、专用模具等资源是有限的,且不同物料切换生产时可能需要准备时间(setup time)。
你的核心任务就两个:“猜得准”和“排得好”。“猜得准”是指基于历史物料到达数据,尽可能准确地预测未来每天甚至每班次各种物料的到达量。这是所有生产安排的前提,预测错了,原料要么积压占库存,要么短缺导致生产线停工。“排得好”是指在预测信息的基础上,综合考虑各种约束,制定出一个高效的生产计划,目标是让订单完成得快(缩短生产周期)、机器利用得足(减少闲置)、切换成本低(减少换模换线时间)。
2.2 从问题到模型的数学抽象
面对这样一个复杂系统,直接上手编程是行不通的。我们必须先进行数学抽象,建立模型。
首先,定义核心要素:
- 物料(Item):
i = 1, 2, ..., I,表示有I种不同的物料需要生产。 - 时间(Time):
t = 1, 2, ..., T,我们将时间离散化,例如以“天”或“班次”为单位,规划未来T个时间段。 - 资源(Resource):
k = 1, 2, ..., K,包括机器、工人、模具等。每种资源在每个时间段内有固定的可用量(如8小时/班)。 - 工序(Operation):每种物料
i的生产需要经过一系列工序j。工序j必须在指定的资源k上加工,耗时p_ijk,并且可能需要在资源k上消耗一定的能力单位。
其次,识别关键变量与约束:
- 预测变量:
D_it,表示在时间t物料i的预测到达量。这是我们时序预测模型需要输出的核心结果。 - 决策变量:
X_ijt,表示在时间t,开始加工物料i的第j道工序的数量。这是一个核心的决策变量,整个生产计划就体现在这个变量的取值上。 - 约束条件网络:
- 物料流平衡:上游工序完成的数量,必须等于下游工序可开始的数量。这保证了生产过程的连续性。
- 资源能力约束:在任何时间段
t,所有在资源k上加工的工序,其总加工时间不能超过该资源在该时间段内的可用时间。这是硬约束,不能违反。 - 顺序约束:工序
j必须在其前序工序j-1完成之后才能开始。 - 非负与整数约束:生产数量通常是整数。
最后,明确优化目标:常见的优化目标是一个多目标组合,可能需要权衡:
- 最小化总完成时间(Makespan):让最后一件产品完工的时间尽可能早。
- 最小化总延迟(Tardiness):所有订单的实际完成时间与其交货期的延迟之和最小。
- 最小化总成本:包括库存持有成本、生产切换成本、资源闲置成本等。 在实际竞赛或应用中,往往需要根据题目要求或业务重点,选择一个主要目标或将多目标转化为单目标(如加权和)。
理解了这个抽象模型,我们就知道,整个解决方案是一座“桥梁”,时序预测模型是桥墩,负责提供稳定的输入(D_it);优化调度算法是桥面,负责在输入的基础上规划出最优路径(X_ijt)。下面,我们重点来浇筑“预测”这个桥墩。
3. 时序预测模型的选择、构建与实战
预测未来物料的到达,本质上是一个多变量时间序列预测问题。每种物料的历史到达数据构成一个时间序列,但这些序列之间可能存在相关性(例如,某些物料总是配套到达)。
3.1 模型选型思路与评估
面对历史数据,我们有哪些武器可以选择?
经典统计模型:
- ARIMA/SARIMA:适用于具有明显趋势和季节性的单变量序列。对于物料预测,如果某种物料的到达有很强的周期性(如每周一某类零件到货多),SARIMA会是一个不错的基线模型。但其对多变量关联关系的捕捉能力较弱。
- 指数平滑(ETS):简单快速,对于没有复杂模式的序列表现稳定。可以作为快速验证预测可行性的工具。
机器学习模型:
- 线性回归/Ridge/Lasso:将历史时间点作为特征,进行回归预测。可以方便地加入其他特征(如星期几、是否节假日),但捕捉非线性关系能力有限。
- 随机森林/XGBoost/LightGBM:这类树模型是时序预测竞赛中的常客。它们能自动处理非线性关系、特征交互,且对缺失值不敏感。我们可以构建“滞后特征”(如用前3天、前7天的数据作为特征)来让模型学习时间依赖。这是本次实战中我们重点采用的方法,因其在中小数据集上表现稳健且易于调参。
深度学习模型:
- LSTM/GRU:专门为序列数据设计的循环神经网络,能捕捉长距离依赖。当数据量足够大、序列模式非常复杂时,LSTM潜力巨大。但对于典型的数模竞赛数据量(可能只有几百条),有过拟合的风险,且训练时间较长。
- Transformer:在自然语言处理中取得巨大成功的模型,也开始应用于时序预测。其自注意力机制能更好地捕捉全局依赖,但同样需要大量数据和计算资源。
模型选择心法:在数模竞赛或项目初期,遵循“从简到繁”的原则。我通常会先建立一个LightGBM基准模型,因为它速度快、效果不错、可解释性相对较好。如果效果不理想,再分析是特征工程不足,还是需要引入更复杂的模型。千万不要一上来就追求最复杂的模型,模型复杂度越高,调试成本和过拟合风险也越高。
3.2 基于LightGBM的预测实战全流程
我们以LightGBM为例,详细拆解构建预测模型的每一步。假设我们有一份历史数据historical_data.csv,包含字段:date(日期),item_A,item_B,item_C(分别代表三种物料每日的到达量)。
import pandas as pd import numpy as np from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error, mean_squared_error import lightgbm as lgb import warnings warnings.filterwarnings('ignore') # 1. 数据加载与探索 df = pd.read_csv('historical_data.csv', parse_dates=['date']) df.set_index('date', inplace=True) print(df.head()) print(df.describe()) # 绘制时序图 (这里以item_A为例) import matplotlib.pyplot as plt plt.figure(figsize=(12,4)) plt.plot(df.index, df['item_A'], label='Item A Daily Arrival') plt.title('Historical Arrival of Item A') plt.xlabel('Date') plt.ylabel('Quantity') plt.legend() plt.grid(True) plt.show()第一步:特征工程——预测模型的“燃料”特征工程的质量直接决定模型性能的上限。对于时序预测,我们主要构建以下几类特征:
def create_features(df, target_col, lags=[1,2,3,7,14,30], window_sizes=[3,7,14]): """ 为时序预测创建特征 df: 输入DataFrame target_col: 要预测的目标列名 lags: 滞后阶数列表 window_sizes: 滑动窗口大小列表,用于计算统计量 """ df_feat = df.copy() # 1. 滞后特征 (Lag Features):模型了解过去 for lag in lags: df_feat[f'{target_col}_lag_{lag}'] = df_feat[target_col].shift(lag) # 2. 滑动窗口统计特征 (Rolling Features):捕捉近期趋势 for window in window_sizes: df_feat[f'{target_col}_roll_mean_{window}'] = df_feat[target_col].rolling(window=window, min_periods=1).mean() df_feat[f'{target_col}_roll_std_{window}'] = df_feat[target_col].rolling(window=window, min_periods=1).std() df_feat[f'{target_col}_roll_max_{window}'] = df_feat[target_col].rolling(window=window, min_periods=1).max() # 可以添加更多,如min, median等 # 3. 时间特征 (Temporal Features):捕捉周期性和季节性 df_feat['day_of_week'] = df_feat.index.dayofweek # 周一=0, 周日=6 df_feat['day_of_month'] = df_feat.index.day df_feat['month'] = df_feat.index.month df_feat['is_weekend'] = df_feat['day_of_week'].apply(lambda x: 1 if x>=5 else 0) # 可以添加季度、是否月初/月末、是否节假日等 # 4. 目标变量的差分特征 (Difference Features):捕捉变化率 df_feat[f'{target_col}_diff_1'] = df_feat[target_col].diff(1) # 5. 其他物料的交叉特征 (可选):如果物料间有关联 # 例如,假设item_B和item_A有关联 # df_feat['item_B_lag_1'] = df_feat['item_B'].shift(1) # 删除因创建滞后和窗口特征产生的NaN行 df_feat.dropna(inplace=True) return df_feat # 为每种物料创建特征数据集 target_items = ['item_A', 'item_B', 'item_C'] feature_dfs = {} for item in target_items: feature_dfs[item] = create_features(df, item) print(f"Features for {item}: {feature_dfs[item].shape}")第二步:模型训练与验证——避免“纸上谈兵”绝对不能把所有数据都用来训练,然后用训练数据来评价模型,那叫“自欺欺人”。我们必须使用时序交叉验证。
def train_lgb_model_for_item(df_feat, target_col, test_size_days=30): """ 训练并评估单个物料的LightGBM模型 """ # 准备特征X和目标y feature_cols = [col for col in df_feat.columns if col != target_col] X = df_feat[feature_cols] y = df_feat[target_col] # 时序分割:用最后`test_size_days`天作为测试集 split_idx = -test_size_days X_train, X_test = X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_test = y.iloc[:split_idx], y.iloc[split_idx:] print(f"Training size: {X_train.shape}, Test size: {X_test.shape}") # 定义LightGBM模型参数 params = { 'objective': 'regression', # 回归任务 'metric': 'mae', # 使用平均绝对误差作为评估指标,对异常值不如MSE敏感 'boosting_type': 'gbdt', 'num_leaves': 31, # 控制树复杂度,小数据集不宜过大 'learning_rate': 0.05, 'feature_fraction': 0.9, # 每次迭代随机选择90%的特征,防止过拟合 'bagging_fraction': 0.8, # 随机选择80%的数据,同上 'bagging_freq': 5, 'verbose': -1, 'seed': 42 } # 创建数据集 train_data = lgb.Dataset(X_train, label=y_train) test_data = lgb.Dataset(X_test, label=y_test, reference=train_data) # 训练模型,并早停防止过拟合 print(f"Training model for {target_col}...") model = lgb.train(params, train_data, valid_sets=[test_data], num_boost_round=1000, callbacks=[lgb.early_stopping(stopping_rounds=50)], verbose_eval=100) # 每100轮输出一次日志 # 在测试集上预测并评估 y_pred = model.predict(X_test, num_iteration=model.best_iteration) mae = mean_absolute_error(y_test, y_pred) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) print(f"\nResults for {target_col}:") print(f" Best Iteration: {model.best_iteration}") print(f" Test MAE: {mae:.2f}") print(f" Test RMSE: {rmse:.2f}") # 可视化预测结果 plt.figure(figsize=(10,4)) plt.plot(y_test.index, y_test.values, label='Actual', alpha=0.7) plt.plot(y_test.index, y_pred, label='Predicted', alpha=0.7, linestyle='--') plt.title(f'Actual vs Predicted for {target_col}') plt.xlabel('Date') plt.ylabel('Quantity') plt.legend() plt.grid(True) plt.show() return model, feature_cols, mae, rmse # 训练所有物料的模型 models = {} metrics = {} for item in target_items: model, feats, mae, rmse = train_lgb_model_for_item(feature_dfs[item], item, test_size_days=30) models[item] = {'model': model, 'features': feats} metrics[item] = {'MAE': mae, 'RMSE': rmse} # 打印总体评估 print("\n=== Overall Prediction Metrics ===") for item, metric in metrics.items(): print(f"{item}: MAE={metric['MAE']:.2f}, RMSE={metric['RMSE']:.2f}")第三步:未来多步预测——真正的挑战在竞赛或实际排产中,我们需要预测未来多天(例如未来14天)的物料到达。这不能简单地用模型预测t+1天,然后把预测值当作已知输入再去预测t+2天(这会导致误差快速累积)。我们需要采用递归预测或直接多输出预测策略。
这里展示递归预测的方法:
def forecast_future(model, last_known_data, feature_cols, steps=14): """ 使用训练好的模型进行多步递归预测 model: 训练好的LightGBM模型 last_known_data: 包含最新特征数据的DataFrame(最后一行是最近的历史数据) feature_cols: 模型训练时使用的特征列名列表 steps: 需要预测的未来步数 """ future_forecast = [] current_features = last_known_data.copy() # 避免修改原数据 for step in range(steps): # 1. 使用当前特征数据,预测下一步 # 确保输入数据的形状和特征顺序与训练时一致 X_input = current_features[feature_cols].iloc[[-1]] # 取最后一行 pred = model.predict(X_input, num_iteration=model.best_iteration)[0] future_forecast.append(pred) # 2. 为下一步预测更新特征(模拟时间前进) # 这是一个简化的更新,实际中需要根据create_features函数逻辑仔细更新所有特征 new_row = current_features.iloc[-1:].copy() new_row.index = [new_row.index[0] + pd.Timedelta(days=1)] # 日期+1天 # 更新目标值:用预测值填充 # 注意:这里需要知道目标列在原始df中的名字,我们假设为'item_A' target_col_name = 'item_A' # 实际应用中需要动态获取或传入 new_row[target_col_name] = pred # 更新滞后特征:将当前值向后移动 for col in feature_cols: if '_lag_1' in col: base_col = col.replace('_lag_1', '') new_row[col] = current_features[base_col].iloc[-1] elif '_lag_2' in col: base_col = col.replace('_lag_2', '') new_row[col] = current_features[base_col].iloc[-2] if len(current_features) > 1 else np.nan # ... 需要根据所有创建的滞后特征进行更新,这是一个复杂但必须精确处理的部分 # 更新滚动统计特征:需要基于一个扩展的窗口重新计算,这里简化处理 # 在实际应用中,可能需要维护一个足够长的序列来重新计算rolling mean/std # 更新时间特征 new_row['day_of_week'] = new_row.index.dayofweek[0] new_row['day_of_month'] = new_row.index.day[0] new_row['month'] = new_row.index.month[0] new_row['is_weekend'] = 1 if new_row['day_of_week'] >=5 else 0 # 3. 将新行追加到特征数据中,用于下一步预测 current_features = pd.concat([current_features, new_row], ignore_index=False) return future_forecast # 示例:预测item_A未来14天的到达量 item = 'item_A' model_info = models[item] last_data = feature_dfs[item].iloc[-30:] # 取最近30天数据作为已知基础 future_preds = forecast_future(model_info['model'], last_data, model_info['features'], steps=14) print(f"Forecasted arrival for {item} in next 14 days: {future_preds}")核心要点与避坑指南:
- 数据泄露:在创建滚动窗口特征(如7日均值)时,必须确保只使用历史信息。在代码中,
rolling(..., min_periods=1).mean()在计算第一个点时就会用到自身,这在训练时没问题,但在预测未来时,你必须手动计算,而不能直接调用rolling。- 递归预测的误差累积:递归预测中,每一步的预测误差都会带入下一步的特征中,导致长期预测可能偏离真实值。缓解方法包括:a) 使用更稳健的模型和特征;b) 预测“变化量”而非绝对值;c) 采用“直接多步预测”策略,为每个未来时间点训练单独的模型(但特征工程更复杂)。
- 特征更新逻辑:
forecast_future函数中的特征更新部分是最易出错、最需要根据实际特征列表精心编写的。一个错误的更新会导致后续预测完全失真。建议将特征创建和更新逻辑封装成函数,确保一致性。- 模型集成:不要只依赖一个模型。可以训练多个不同参数或不同算法的模型(如LightGBM, XGBoost, 线性模型),然后对它们的预测结果进行平均或加权,这通常能提升预测的稳定性和准确性。
4. 预测结果如何融入生产调度优化
拿到了未来T天的物料预测到达量D_it,我们的任务只完成了一半。接下来,需要将这些预测值作为输入,放入生产调度优化模型中。
4.1 构建调度优化模型的基本框架
我们可以使用混合整数规划(MIP)来建模。这里以最小化总完成时间为目标,给出一个高度简化的PuLP(Python线性规划库)模型框架,以展示预测与优化的衔接。
import pulp # 假设数据 items = ['item_A', 'item_B'] resources = ['Machine_1', 'Machine_2'] time_horizon = 10 # 未来10个时间段 # 预测的到达量 (shape: [items, time_horizon]) predicted_arrival = { 'item_A': [10, 12, 8, 15, 9, 11, 13, 7, 14, 10], 'item_B': [5, 7, 6, 4, 8, 5, 9, 6, 7, 5] } # 加工时间 (item -> resource -> time) processing_time = { 'item_A': {'Machine_1': 2, 'Machine_2': 1}, 'item_B': {'Machine_1': 1, 'Machine_2': 3} } # 资源能力 (每个时间段每台机器的可用时间,例如8小时) capacity = {res: 8 for res in resources for t in range(time_horizon)} # 创建问题 prob = pulp.LpProblem('Small_Batch_Production_Scheduling', pulp.LpMinimize) # 定义决策变量 # X[i][r][t]:在时间t,在资源r上开始加工物料i的数量(整数) X = {} for i in items: for r in resources: for t in range(time_horizon): X[(i, r, t)] = pulp.LpVariable(f'X_{i}_{r}_{t}', lowBound=0, cat='Integer') # 定义辅助变量:完工时间(简化,这里假设每项工作只有一个工序) C_max = pulp.LpVariable('C_max', lowBound=0) # 目标函数:最小化最大完工时间 prob += C_max # 约束条件 # 1. 资源能力约束:每个资源在每个时间段内加工总时间不超过其能力 for r in resources: for t in range(time_horizon): prob += pulp.lpSum([X[(i, r, t)] * processing_time[i][r] for i in items]) <= capacity[r] # 2. 需求满足约束:到每个时间点为止,累计生产量必须至少等于累计预测到达量(这里简化了工序流) for i in items: for t in range(time_horizon): # 累计生产量(假设所有生产都能立即完成,无在制品) total_produced = pulp.lpSum([X[(i, r, tau)] for r in resources for tau in range(t+1)]) # 累计需求量(预测到达量) total_demand = sum(predicted_arrival[i][:t+1]) prob += total_produced >= total_demand # 3. 最大完工时间约束(链接到C_max变量) for i in items: for r in resources: for t in range(time_horizon): # 如果在该时间点有生产,则完工时间至少晚于该时间点(简化处理) prob += C_max >= (t + 1) * (X[(i, r, t)] > 0) # 这是一个逻辑约束,实际需要用大M法线性化,此处仅为示意 # 求解问题 solver = pulp.PULP_CBC_CMD(msg=False) # 使用CBC求解器,不输出日志 prob.solve(solver) # 打印结果 print(f"Status: {pulp.LpStatus[prob.status]}") print(f"Objective (Minimized C_max): {pulp.value(prob.objective)}") if prob.status == pulp.LpOptimal: print("\nProduction Schedule (Quantity to start at each period):") for t in range(time_horizon): print(f"Period {t}:") for i in items: for r in resources: val = pulp.value(X[(i, r, t)]) if val > 0: print(f" Produce {val} units of {i} on {r}")注意:以上是一个极度简化的模型框架,仅用于说明预测结果(
predicted_arrival)如何作为参数输入到优化模型中。真实的E题模型要复杂得多,涉及多工序、顺序约束、准备时间、在制品库存等。你需要根据题目具体要求,建立完整的约束体系。
4.2 处理预测不确定性:鲁棒优化与滚动计划
预测永远不可能100%准确。直接将点预测值D_it当作确定值用于调度,风险很高。在实际应用中,我们需要考虑预测的不确定性。
- 情景分析:利用预测模型(如LightGBM)不仅可以输出点预测,还可以输出预测区间(通过分位数回归或使用模型预测误差的分布来估计)。我们可以构建几个典型情景(如乐观、悲观、最可能),然后分别求解调度方案,观察方案的稳定性。
- 鲁棒优化:在优化模型中,将需求
D_it视为一个不确定参数,它在一个给定的集合(如[D_it_lower, D_it_upper])内变化。优化目标是在最坏情况(worst-case)下也能保证性能不太差。这种方法得到的方案更保守,但抗干扰能力强。 - 滚动时域优化:这是最实用、最贴近工业实践的方法。我们不会一次性制定未来30天的详细计划。而是:
- 第1步:基于当前所有信息(包括最新预测),制定未来N天(例如5天)的详细生产计划,并锁定第1天的计划。
- 第2步:执行第1天的计划。
- 第3步:一天结束后,收集实际到达的物料数据,与预测进行对比,更新预测模型。
- 第4步:时间窗口向前滚动一天,基于新的信息和更新的预测,重新优化未来第2天到第N+1天的计划。
- 如此循环往复。这种方法能动态吸收不确定性,不断修正计划。
在数模竞赛中,如果题目数据允许,采用滚动时域框架并结合点预测+安全缓冲的策略,往往能写出亮点,体现出对实际生产管理动态性的深刻理解。
5. 竞赛实战技巧与代码整合建议
5.1 从解题到论文的完整工作流
- 第一步:问题重述与假设(论文第一章)。用你自己的话清晰定义问题,并列出合理的假设,例如:“假设物料到达过程是平稳的”、“假设机器故障忽略不计”、“假设切换时间与顺序无关”等。合理的假设能简化模型,但要在论文中说明其合理性。
- 第二步:数据分析与预处理(论文第二章)。展示你对提供的数据做了什么:缺失值处理、异常值检测、描述性统计、可视化(时序图、分布图、相关性热力图)。这部分能体现你的数据科学基本功。
- 第三步:预测模型建立(论文第三章)。对应本文第3部分。详细说明你为何选择某个模型(如LightGBM),如何进行特征工程,如何验证(时序交叉验证),并展示预测效果(MAE, RMSE, 可视化对比图)。将关键代码以清晰、注释良好的形式放入论文附录。
- 第四步:调度优化模型建立(论文第四章)。对应本文第4.1部分。用数学公式清晰定义集合、参数、决策变量、目标函数、约束条件。这是论文的理论核心。
- 第五步:模型求解与结果分析(论文第五章)。说明你用了什么求解器(如PuLP调用CBC,或OR-Tools,或Gurobi if allowed),求解规模(变量数、约束数),求解时间。展示你的调度方案(最好用甘特图可视化),并进行灵敏度分析(例如,如果加工时间增加10%,总完工时间如何变化?如果预测误差增大,方案稳定性如何?)。
- 第六步:模型评价与推广(论文第六章)。客观评价自己模型的优点(如快速、有效、鲁棒)和缺点(如假设过强、未考虑XX因素)。提出改进方向,并将模型推广到更一般的场景。
5.2 Python代码组织与可复现性
一个清晰的代码结构至关重要,不仅利于自己调试,也方便评委阅读。
your_project/ │ ├── data/ │ ├── raw/ # 原始赛题数据 │ └── processed/ # 清洗处理后的数据 │ ├── src/ │ ├── 01_data_preprocessing.py │ ├── 02_feature_engineering.py │ ├── 03_train_predictive_model.py │ ├── 04_build_optimization_model.py │ ├── 05_rolling_horizon_simulation.py │ └── utils.py # 通用函数 │ ├── models/ # 保存训练好的预测模型 ├── results/ # 预测结果、调度方案、图表 ├── config.yaml # 配置文件(参数、路径) ├── requirements.txt # 依赖包列表 └── main.py # 主运行脚本在main.py中,你可以这样组织:
# main.py import yaml from src import (data_preprocessing, feature_engineering, train_predictive_model, build_optimization_model, rolling_horizon_simulation) def main(): # 加载配置 with open('config.yaml', 'r') as f: config = yaml.safe_load(f) # 1. 数据预处理 print("Step 1: Data Preprocessing...") df_clean = data_preprocessing.load_and_clean(config['data_path']) # 2. 特征工程与预测模型训练 print("Step 2: Feature Engineering & Model Training...") feature_df, trained_models = feature_engineering.create_all_features(df_clean) # 假设我们保存模型或直接使用 forecast_results = train_predictive_model.rolling_forecast(trained_models, feature_df, steps=config['forecast_horizon']) # 3. 优化求解 print("Step 3: Optimization Scheduling...") # 将预测结果转换为优化模型所需的参数格式 schedule, obj_value = build_optimization_model.solve_mip_model(forecast_results, config['processing_times'], config['capacities']) # 4. (可选) 滚动时域仿真 print("Step 4: Rolling Horizon Simulation...") final_schedule, performance_metrics = rolling_horizon_simulation.run_simulation(config) # 5. 输出结果 print("\n=== Final Results ===") print(f"Objective Value: {obj_value}") # 将结果保存为CSV或绘制图表 # ... if __name__ == '__main__': main()5.3 常见陷阱与应对策略
陷阱一:过拟合预测模型。在有限的历史数据上追求极低的训练误差,结果模型学到了噪声而非规律,对未来预测一塌糊涂。
- 对策:严格使用时序交叉验证(TimeSeriesSplit)评估模型;使用正则化(如LightGBM的
feature_fraction,bagging_fraction);避免使用过于复杂的模型;关注测试集(或验证集)上的表现。
- 对策:严格使用时序交叉验证(TimeSeriesSplit)评估模型;使用正则化(如LightGBM的
陷阱二:忽略调度模型的求解可行性。建立的MIP模型变量和约束太多,导致在比赛时间内无法求解到满意解。
- 对策:先建立简化模型(如忽略部分次要约束)快速验证思路;使用高效的求解器(如OR-Tools的CP-SAT求解器对调度问题非常高效);考虑启发式或元启发式算法(如遗传算法、模拟退火)作为备选,它们可能找不到最优解,但能在短时间内找到高质量可行解。
陷阱三:预测与调度“两层皮”。预测是预测,调度是调度,两者没有闭环。没有考虑预测误差对调度的影响。
- 对策:在论文中必须讨论预测不确定性,并提出应对策略(如滚动计划、安全时间缓冲)。这是体现模型实用性和思维深度的关键。
陷阱四:代码“一锅粥”。所有代码写在一个Jupyter Notebook的几十个单元格里,变量命名混乱,没有注释。
- 对策:如上所述,采用模块化的代码结构。关键函数和复杂逻辑必须写注释。使用有意义的变量名。这不仅能让你在最后关头快速调试,也能给评审老师留下好印象。
这道E题是一个绝佳的练手项目,它串联了数据分析、机器学习、运筹优化和仿真等多个领域。真正吃透它,你收获的不仅仅是一个竞赛奖项,更是一套解决实际工业排产问题的系统性思维和实战能力。在实际操作中,最大的体会往往是:一个漂亮的数学模型,必须建立在对业务细节(如切换时间究竟怎么算、瓶颈工位在哪里)的深刻理解之上;而一个可行的算法方案,也必须在求解效率和解决方案质量之间做出精妙的权衡。
