简介:这份资源是2023年电工杯B题人工智能赛题的完整Matlab实现方案,面向计算机、电子信息工程、数学等专业的大学生,可用于课程设计、期末大作业与毕业设计,帮助读者快速复现赛题建模与求解流程。压缩包共28个文件,约7.3MB,以m脚本、xlsx数据表、mat结果文件为主,另含csv与pdf思路说明,覆盖数据转换、评价得分、综合得分等环节,并附赠可直接运行的案例数据。代码采用参数化编程,参数修改方便,思路清晰、注释详细,兼容Matlab2014a、2019b与2024b。读者可据此获得三问的完整代码与中间结果,对照思路文档理解建模逻辑,借助数据表与mat文件复现计算过程,并在此基础上调整参数、迁移方法,完成自己的赛题方案与论文写作。目前已有45人学习下载。
1. 电工杯B题人工智能.zip:一份竞赛题包到底该怎么拆
电工杯B题人工智能.zip 这个标题,第一次看到的人多半会愣一下:它既不像论文题目,也不像开源项目名,更像某个竞赛群里传出来的压缩包。事实也确实如此——全国大学生电工数学建模竞赛(电工杯)的 B 题,近几年连续押在人工智能与电力系统交叉的方向上,赛题下发时通常就是一个压缩包,里面塞着题目 PDF、若干 CSV 或 Excel 数据、可能还有一份说明文档。你拿到它,真正要做的不是「跑一个模型」,而是在 72 小时内完成从读题、数据清洗、特征构造、建模到论文写作的全链路。这篇文章面向三类人:正在备赛想提前摸清套路的学生、带队的指导老师、以及想拿这道题当教学案例的从业者。我会把这类题包拆成可复现的工程流程,告诉你数据长什么样、模型怎么选、参数怎么调、论文里哪些图必须画,以及我踩过的那些坑。人工智能这个词现在几乎成了日常帮手,但在竞赛场景里,它意味着你必须用可解释、可复现的方法把电力数据里的规律讲清楚,而不是调个库就交差。
2. 先看清题包结构:电工杯B题的数据到底长什么样
2.1 压缩包里的四类文件与它们的用途
电工杯 B 题的题包结构历年变化不大,常见做法是四类文件。第一类是赛题说明 PDF,通常 3 到 8 页,包含背景描述、任务要求、数据字段解释和论文格式规范。第二类是训练数据,格式以 CSV 为主,偶尔是 Excel,字段一般包括时间戳、负荷值、温度、湿度、风速、电价、设备状态等。第三类是测试数据或待预测数据,结构与训练集一致但缺少目标列。第四类是附件说明,可能是一个 README 或者字段对照表。
我一般拿到包后第一件事不是打开数据,而是把 PDF 完整读两遍。第一遍通读,标记出所有带数字的要求,比如「预测未来 24 小时负荷」「误差控制在 5% 以内」「需给出特征重要性排序」。第二遍逐条拆解,把每个要求翻译成技术动作:预测 24 小时是多步时序预测,误差 5% 是 MAPE 指标,特征重要性排序意味着模型要可解释。这一步做扎实,后面少走一半弯路。
提示:题包里如果有多个版本的说明文档,以文件名带「最终」或日期最新的为准,竞赛期间题目偶尔会发勘误。
2.2 用 pandas 做一次五分钟数据体检
不管题目要求什么,先跑一遍数据体检。下面这段代码是我每次拿到新数据必跑的模板,能在一分钟内告诉你数据的基本形态、缺失情况和异常分布。
import pandas as pd import numpy as np # 读取数据,注意编码问题,电工杯数据常见 gbk 或 utf-8-sig df = pd.read_csv('train.csv', encoding='gbk', parse_dates=['timestamp']) # 基础信息 print("形状:", df.shape) print("字段类型:\n", df.dtypes) print("缺失值:\n", df.isnull().sum()) print("描述统计:\n", df.describe()) # 时间连续性检查 df = df.sort_values('timestamp') time_diff = df['timestamp'].diff().value_counts() print("时间间隔分布:\n", time_diff.head()) # 目标列异常值粗筛,以负荷为例 if 'load' in df.columns: q1, q3 = df['load'].quantile([0.25, 0.75]) iqr = q3 - q1 outliers = df[(df['load'] < q1 - 3*iqr) | (df['load'] > q3 + 3*iqr)] print("极端离群点数量:", len(outliers))这段代码的逻辑是:先确认数据规模和字段类型,再看缺失值集中在哪些列,描述统计帮你判断量纲是否统一。时间间隔分布是关键——电力负荷数据理论上应该是等间隔的,如果出现大量非标准间隔,说明数据有缺时刻或重复时刻,需要重采样。离群点用 3 倍 IQR 粗筛,注意这里只是标记,不要急着删,电力数据里的尖峰可能是真实的高温负荷或故障信号。
参数方面,encoding要根据实际文件调整,中文环境下的 CSV 常见 gbk;parse_dates指定时间列后可以直接用 pandas 的时间索引功能。如果数据量超过百万行,describe()会变慢,可以改成抽样后再统计。
2.3 字段含义与电力场景的对应关系
电工杯 B 题的数据字段通常围绕电力系统展开,理解每个字段的物理含义比盲目建模重要得多。常见字段和它们的场景对应如下:
| 字段名 | 物理含义 | 建模时的角色 |
|---|---|---|
| timestamp | 采样时刻 | 时间索引,构造滞后和周期特征 |
| load | 有功负荷 | 预测目标或关键输入 |
| temperature | 环境温度 | 外生变量,影响负荷 |
| humidity | 相对湿度 | 外生变量,影响较小 |
| wind_speed | 风速 | 新能源场景关键输入 |
| price | 电价 | 需求响应场景的目标或输入 |
| status | 设备状态 | 分类特征,需编码 |
温度对负荷的影响通常有滞后效应,不是当前温度决定当前负荷,而是前几小时甚至前一天的累积温度。我一般会构造温度的一阶差分和 24 小时滑动平均作为额外特征。风速在风电预测题里是核心输入,但要注意风速和功率之间是非线性关系,直接线性回归效果很差,常见做法是分段或做三次方变换。
3. 特征工程:把原始 CSV 变成模型能吃的矩阵
3.1 时间特征:周期编码比直接塞时间戳管用
电力负荷有极强的周期性:日周期、周周期、年周期。直接把 timestamp 转成整数喂给模型是新手最容易犯的错,模型会认为时间 23 点和 0 点相差 23,实际上它们只差 1 小时。正确做法是做周期编码。
def add_time_features(df, time_col='timestamp'): df = df.copy() df['hour'] = df[time_col].dt.hour df['dayofweek'] = df[time_col].dt.dayofweek df['month'] = df[time_col].dt.month df['is_weekend'] = (df['dayofweek'] >= 5).astype(int) # 周期编码:sin/cos 让 23 点和 0 点在数值上接近 df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24) df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24) df['dow_sin'] = np.sin(2 * np.pi * df['dayofweek'] / 7) df['dow_cos'] = np.cos(2 * np.pi * df['dayofweek'] / 7) return df df = add_time_features(df)周期编码的原理是把周期映射到单位圆上,23 点和 0 点在圆上相邻,数值距离也小。hour_sin和hour_cos必须成对使用,单独一个会丢失信息。is_weekend是布尔特征,电力负荷在工作日和周末的模式差异明显,这个特征几乎总是有效。
3.2 滞后特征与滑动窗口:多步预测的关键
如果题目要求预测未来 24 小时,你不能只用当前时刻的特征,必须构造滞后特征。常见做法是用过去 24 小时、48 小时、168 小时(一周)的负荷作为输入。
def add_lag_features(df, target='load', lags=[1, 2, 3, 24, 48, 168]): df = df.copy() for lag in lags: df[f'lag_{lag}'] = df[target].shift(lag) # 滑动统计 for window in [6, 12, 24]: df[f'roll_mean_{window}'] = df[target].shift(1).rolling(window).mean() df[f'roll_std_{window}'] = df[target].shift(1).rolling(window).std() return df df = add_lag_features(df) df = df.dropna() # 滞后会产生 NaN,训练前必须处理这里有个细节:rolling之前先shift(1),否则当前时刻的负荷会泄露到滑动统计里,造成数据穿越。数据穿越是时序预测里最隐蔽的坑,模型在验证集上表现很好,一到测试集就崩,多半是这个原因。滞后阶数的选择要看题目要求:预测 1 小时,lag 1 到 24 够用;预测 24 小时,lag 24 到 168 更关键。
3.3 外生变量的处理:温度和风速怎么进模型
温度和风速这类外生变量,处理方式和负荷不同。温度通常做滑动平均,因为建筑热惯性导致负荷对温度的响应有延迟。风速在风电场景要做功率曲线变换。
# 温度:24 小时滑动平均 + 一阶差分 df['temp_ma24'] = df['temperature'].rolling(24, min_periods=1).mean() df['temp_diff'] = df['temperature'].diff() # 风速:简化的功率曲线变换,切入风速 3,额定 12,切出 25 def wind_to_power(v): if v < 3 or v > 25: return 0 elif v >= 12: return 1.0 else: return (v**3 - 3**3) / (12**3 - 3**3) df['wind_power'] = df['wind_speed'].apply(wind_to_power)功率曲线变换的参数(3、12、25)是常见风机的典型值,如果题目给了具体机型参数,以题目为准。这个变换把非线性的风速-功率关系拉直,后续线性模型也能用。如果不做变换,树模型还能扛,线性模型基本废掉。
4. 模型选型与训练:从基线到集成的递进路线
4.1 先跑一个基线:线性回归和移动平均
竞赛里最忌讳一上来就上深度学习。我一般先用两个基线:移动平均和线性回归。移动平均用过去 24 小时均值作为预测,代码一行;线性回归用 sklearn 的 LinearRegression,特征用前面构造的完整矩阵。这两个基线的意义是给你一个误差下界,后面所有复杂模型都必须显著优于它们,否则说明特征工程有问题,不是模型不够强。
from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_percentage_error # 按时间切分,前 80% 训练,后 20% 验证 split = int(len(df) * 0.8) train, val = df.iloc[:split], df.iloc[split:] feature_cols = [c for c in df.columns if c not in ['timestamp', 'load']] X_train, y_train = train[feature_cols], train['load'] X_val, y_val = val[feature_cols], val['load'] lr = LinearRegression() lr.fit(X_train, y_train) pred = lr.predict(X_val) print("线性回归 MAPE:", mean_absolute_percentage_error(y_val, pred))注意切分必须按时间顺序,不能随机切分。随机切分会让未来数据泄露到训练集,验证指标虚高。MAPE 是电工杯常用的误差指标,但它在真实值接近零时会爆炸,如果负荷数据有接近零的时刻,改用 MAE 或 RMSE 更稳。
4.2 树模型:XGBoost 和 LightGBM 的参数怎么调
树模型是电力负荷预测的主力,XGBoost 和 LightGBM 都常用。我一般优先 LightGBM,训练快,对缺失值友好。关键参数有五个:n_estimators、learning_rate、max_depth、num_leaves、subsample。
import lightgbm as lgb params = { 'objective': 'regression', 'metric': 'mape', 'learning_rate': 0.05, 'num_leaves': 63, 'max_depth': 7, 'subsample': 0.8, 'colsample_bytree': 0.8, 'n_estimators': 1000, 'early_stopping_rounds': 50, 'verbose': -1 } dtrain = lgb.Dataset(X_train, y_train) dval = lgb.Dataset(X_val, y_val, reference=dtrain) model = lgb.train(params, dtrain, valid_sets=[dval])参数逻辑:learning_rate0.05 配合n_estimators1000 是稳妥组合,学习率再低需要更多树,竞赛时间紧不划算。num_leaves63 和max_depth7 控制模型复杂度,电力数据噪声大,树太深容易过拟合。early_stopping_rounds50 让模型在验证集不再提升时自动停,省去手调轮数。subsample和colsample_bytree0.8 是防过拟合的常规操作。
如果验证集 MAPE 比线性回归还差,先检查特征里有没有穿越,再检查时间切分是否正确,最后才怀疑参数。
4.3 时序专用模型:LSTM 和 Prophet 的适用边界
电工杯 B 题有时会明确要求用深度学习,或者数据量足够大时 LSTM 确实有优势。LSTM 的输入需要构造成三维张量:样本数、时间步长、特征数。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def make_sequences(data, target, window=24): X, y = [], [] for i in range(window, len(data)): X.append(data[i-window:i]) y.append(target[i]) return np.array(X), np.array(y) # data 是标准化后的特征矩阵,target 是负荷 X_seq, y_seq = make_sequences(data_scaled, target_scaled, window=24) split = int(len(X_seq) * 0.8) model = Sequential([ LSTM(64, return_sequences=True, input_shape=(24, X_seq.shape[2])), Dropout(0.2), LSTM(32), Dense(1) ]) model.compile(optimizer='adam', loss='mse') model.fit(X_seq[:split], y_seq[:split], validation_split=0.1, epochs=50, batch_size=32)LSTM 的坑在于:数据必须标准化,否则梯度爆炸;window选择 24 还是 168 要看周期;训练轮数不是越多越好,验证损失回升就停。Prophet 适合有明显趋势和季节性的数据,但它对多变量外生特征的支持较弱,电工杯这种多因素场景,Prophet 通常只作为对比基线,不作为主力。
5. 避坑与排查:竞赛里最容易翻车的五个地方
5.1 数据穿越:验证集漂亮测试集崩盘
现象:验证集 MAPE 只有 3%,提交后测试集误差 15% 以上。原因:特征构造时用了未来信息,比如滑动统计没 shift,或者标准化时用了全量数据的均值方差。解决:所有涉及时间聚合的操作,先 shift 再 rolling;标准化参数只在训练集上 fit,再 transform 验证集和测试集。
5.2 时间戳解析错误:数据顺序全乱
现象:模型训练正常但预测结果完全无规律。原因:CSV 读取时时间列被当成字符串,排序按字典序,导致 10 点排在 2 点前面。解决:pd.read_csv时加parse_dates,读入后立即sort_values并检查diff()是否等间隔。
5.3 缺失值填充方式不当:引入虚假规律
现象:填充后模型在缺失段附近误差异常大。原因:用全局均值填充时序数据,破坏了时间连续性。解决:时序数据优先用前向填充或线性插值,缺失超过 6 小时的段考虑整段剔除或标记为独立特征。
5.4 评估指标选错:MAPE 在零值附近爆炸
现象:MAPE 出现几百甚至无穷大。原因:真实负荷有接近零的时刻,分母极小。解决:改用 MAE、RMSE 或 SMAPE,或者在计算 MAPE 前过滤掉真实值小于某阈值的样本,并在论文里说明。
5.5 论文图表与代码结果不一致
现象:论文里的图和代码跑出来的图对不上。原因:多次实验后忘了固定随机种子,或者用了不同版本的数据。解决:所有涉及随机的环节设random_state,数据版本用文件名带日期区分,论文定稿前用最终代码重跑一遍所有图表。
6. 从交卷到进阶:把竞赛代码变成可复用的预测框架
竞赛结束后,这套代码不该扔。我一般会做三件事把它变成可复用的东西。第一,把特征工程和模型训练封装成函数,输入原始 CSV,输出预测结果和评估报告,下次遇到类似赛题改改字段名就能跑。第二,把验证策略从单次切分改成滚动预测,更接近真实部署场景。
def rolling_forecast(df, feature_cols, target='load', train_window=720, test_window=24): """滚动预测:每次用过去 train_window 小时训练,预测未来 test_window 小时""" results = [] for start in range(train_window, len(df) - test_window, test_window): train = df.iloc[start-train_window:start] test = df.iloc[start:start+test_window] model = lgb.LGBMRegressor(n_estimators=500, learning_rate=0.05, num_leaves=63) model.fit(train[feature_cols], train[target]) pred = model.predict(test[feature_cols]) results.append(pd.DataFrame({ 'timestamp': test['timestamp'].values, 'true': test[target].values, 'pred': pred })) return pd.concat(results)滚动预测的逻辑是模拟真实部署:每次只用历史数据训练,预测下一段,然后窗口前移。train_window720 小时约一个月,test_window24 小时对应题目要求。这个策略下得到的误差比单次切分更可信,论文里放这张滚动误差曲线,说服力比单一 MAPE 数字强得多。
第三件事是写一份字段映射表。电工杯每年的字段名会变,但物理含义大同小异。我维护了一个映射字典,把「load」「power」「demand」都映射到统一的目标列名,把「temp」「temperature」「air_temp」映射到温度列。下次拿到新题包,改字典比改代码快。
最后说个血泪经验:竞赛期间不要频繁换模型。我见过太多队伍在最后 12 小时把 LightGBM 换成 LSTM,结果调参来不及,论文里的结果还不如基线。定好路线就深耕,把特征工程做扎实,把验证做严谨,比追新模型有用得多。这套流程我用了几年,从读题到交卷的节奏基本稳定,希望帮到你。
本文还有配套的精品资源,点击获取