电力市场出清价格预测对于电力市场中主体博弈产生的结算价格的预测具有重要的理论和现实意义。电力部门推动能源转型促进可持续发展的主体。因此,对电力市场主体行为的分析以及最终的市场结算价格的预测能够促进多学科融合,推动电力部门以及电力市场的产业转型。此外,可靠的电力系统保障社会稳定与安全,提升应急响应能力,并增强国家的全球竞争力和未来应对挑战的能力。
1.背景
该项目主要针对“电力现货市场”(可以类比证券交易市场),泛指短时间内的电能量交易市场。 市场中有大量发电机组(供给者,会发电并卖出电力)按照交易规则,在指定的平台上采取集中竞价的方式确定电能的交易量和价格。这些电能会被输送给个体户、商业用户等,从而利用市场机制实现资源优化配置。 在最理想的情况下,市场完全竞争(市场参与者众多,以至于没有任何一方能够影响价格,不存在控制价格的可能性),没有任何博弈行为。每个发电机组都诚实报价,市场出清价格稳定可靠,达到最优效率。 但现实中,电力现货市场有以下特点:
- 寡头竞争(几家电力公司独大,对价格有显著影响)
- 不完全信息(不同机组信息不互通,存在打信息差牟利的可能)
- 非合作博弈(机组之间各谋其利,追求各自的利益最大化)
- 参与者有限理性(受限于经济知识和对市场的了解,机组不一定能做出最优决策) 从而不同机组之间有复杂的博弈行为,这就让市场出清价格难以估计。 因此,要求针对电力现货市场价格和市场博弈主体(549个发电机组)的信息,用ABM方法建模这些机组在报价上的博弈行为,使最终模拟的市场出清报价接近现实中的市场出清价格。
2. 电力市场出清价格
电力市场概述
2000以前,国内并不存在电力市场,而是叫计划电力经济。发电侧为卖方,核算发电成本和利润上报国家,审核通过后就是上网电价。用户侧为买方,被动执行国家制定的分时电价。计划电力经济的优势为:电价相对稳定,企业用电成本核算相对简单;但是问题也比较突出,特别是煤价疏导滞后,体制机制僵化,资源配置粗放,不能灵敏准确的反映发电成本、发现电力价格。 2002年,电力市场化改革文件《国务院关于印发电力体制改革方案的通知》指出:打破垄断,引入竞争,提高效率,降低成本,健全电价机制,优化资源配置,促进电力发展,推进全国联网,构建政府监管下的政企分开、公平竞争、开放有序、健康发展的电力市场体系。通知发布后,原国家电力公司拆分为5大发电集团(家能源投资集团、中国华能集团、中国华电集团、中国大唐集团、国家电力投资集团)与2大电网(国家电网、南网)。发电厂试行竞价上网,成立国家电监会,对市场行为进行监管,从一定程度上打破了垄断。但这一阶段改革成效不彻底,其主要原因在于销售侧电价没有放开,所谓“放开两头,管住中间”只是在发电这一头产生了一定的成效,发电成本的变化并没有得到及时有效的传导。 2015年中发9号文《关于进一步深化电力体制的若干意见》指出:让发电企业和用户(公共事业、居民和农业用户仍执行政府定价)进入市场,通过报量报价进行交易撮合和价格出清,形成了真正的电力市场,基本达到了发现价格、优化配置的目标。
电力现货市场的价格出清机制
市场价格出清是通过交易系统完成的,即买方和卖方均通过交易系统提交买(卖)数量和价格的申请,然后通过交易系统进行匹配,最后形成一个价格。价格一旦形成,将被所有成员接受。最后形成的价格被称为边际出清价格。市场出清电价是指在竞争定价的电力市场中,能够实现市场供-需平衡的度电价格。 电力市场的出清价格形成类似证券市场早上9:15-9:25的集合竞价,由于数据里只提供了总需求,我们可以认为需求曲线是一条直线。 出清价格的形成步骤如下:
- 所有发电机组申报自己卖出的电价和电量
- 市场根据机组报价,从低到高排序,依次从低价开始成交
- 当成交的容量和大于等于总需求时,达到市场出清(供需平衡),这时候最后一个达成交易的机组报价为市场出清价格 我们用一个实际案例来解释,假如市场总需求为3000MW,四个机组依次报价报量如下:
- 机组1:报价150元,容量500MW
- 机组2:报价200元,容量500MW
- 机组3:报价250元,容量2000MW
- 机组4:报价400元,容量2500MW 按价格从低到高,机组1先成交,随后是机组2、机组3,此时容量和等于总需求,不再有额外的电力需求,市场达到出清状态,市场出清价格为250元。 这里我们还可以发现一些现象:
- 机组4由于过高的价格竞标失败,导致没有卖出任何电力。现实中机组4不得不停掉一些发电机。但煤电机组频繁关停会影响设备寿命和性能,还可能延误后续高价时段的正常发电,最终只能亏本卖出。
- 机组1实际上能卖更多钱,如果他能预料到机组2和3报价为200和250,他就可以报价300元,保证最后一个出清的是他,而不是只能卖150元(但它无法报特别高的价,因为电力是公共物品,受政府管控,设置了价格上下限) 因此报价本身就是一个充满策略博弈的行为,就像在股市中总想做买入价格最低,卖出价格最高的那个人。
价格帽
根据上面内容,我们会想:“那如果用电特别紧缺”,机组不是能开特别高价来获利吗?
为了解决这个问题,并且保证市场稳定,电力现货市场中存在价格帽,即规定:
-
报价上限为:1300元/MWH
-
报价下限为:-80元/MWH
-
出清价格上限:1500元/MWH
-
出清价格下限:-100元/MWH
3. 电力市场数据解读
EDA
EDA通常指的是探索性数据分析(Exploratory Data Analysis)。这是一种用于寻找数据集中的模式和趋势,以及从数据中提炼出有用信息的统计方法。EDA强调的是数据的初步分析,利用图形和技术来识别可能的数据模式、异常值、偏差、误差等。
在数据分析流程中,EDA是关键的第一步,它帮助分析师理解数据的基本结构和特征,为后续的数据清洗、特征工程和建模提供基础。常见的EDA技术包括但不限于数据可视化、统计摘要(如均值、中位数、标准差等)以及计算相关性矩阵等。通过这些手段,可以帮助研究人员发现数据之间的潜在关系,为制定假设和进一步研究提供依据。
现在的数据挖掘类比赛中,模型和方法选择空间往往很小,同时存在不少自动机器学习框架(如AutoGulon、AutoSKLearn)会基于一定规则,自动构造特征,采用尽可能多的模型组合来获得好分数。因此最后的关键涨分点落在了对数据的理解上,并由此构造的强特征(对结果有关键影响的变量)。
电力市场数据的分析
查看数据可以发现,出清价格在一小时中是保持不变的,也就是实际上交易时间是1小时而不是15分钟,但总负荷会在一小时中变化。
不同小时的总负荷和电价
首先分析不同年份下,一天不同小时的总负荷和电价。我们可以发现,数据中用电存在两个高峰期和一个低谷期
-
早高峰:一般在6-9点
-
晚高峰:一般在16-21点
-
低谷期:10-15点
这似乎很反常,理论上10-15点都在进行大量的工业和商业活动,为什么反而电价和总负荷会更低呢,这里需要了解一个概念:“鸭子曲线”。
首先,城市用电呈现类正弦曲线,在早九点和晚九点分别达到高峰(peak),分别是早高峰和晚高峰偏后些,要不就是刚开始上班,要不就是刚回到家休息。
一般来说一个城市的电力供应主要有火电、水电和太阳能等,其中以火电作为基荷,以其电力供应较稳定去承担了大部分电力供应,对于多出来的部分则由向水电或者太阳能承担。
以加州为例,光伏发电的迅猛发展为电网提供了大量廉价的电力,但由于其白天发电,晚上就没光可用。也就出现了提到的鸭形曲线,也就是中午时间由电网供应电量较少,但是晚上会急剧增加达到峰值。
随着时间的推移,这种现象也越来越明显了。主要还是加州的光伏发电的迅猛发展,让鸭形曲线更突出。对于峰值和谷底就要特别处理了。
这会造成什么后果呢,那就是弃荷,一般采取的方法是在中午弃掉一部分的太阳光能。
这样就能保证基荷不会发生显著变化,同时满足了电网供电需求。
但是随着加州可再生能源比例的提高,给能源局也带了不少的挑战。加州独立运营商并不清楚光伏发电究竟有多少容量,因为部分并没有并入电网系统。电力调控也相应收到了影响。
对比加利福尼亚的数据和目前的数据,可以发现二者的形式几乎一致,这样我们又增加了两个内容信息:
-
考虑其他新能源(尤其是光伏)的影响对预测价格意义重大
-
随着时间推移和中国碳中和的发展,光伏必定会在更大程度上替代火电,因此可以猜测2024年的火电价格会进一步下降。
负电价与高电价形成原因分析
负电价
从不同小时下负电价的出现次数来看,低谷期的负电价较为明显,可能是受市场竞争导致电价中标失败,只能亏本售出。
通过按负电价出现频数排序,可以发现最典型的一次集中的负电价发生在2022年的五一假期中,连续出现了7次的负电价。
猜测是因为火电厂发电需要人为监控(例如开关机,设置出力,而风电和光伏放着自己发电就好),假期期间大多数员工放假,从而导致火力发电量下降,出现负电价。
我们还可以进一步探究假期对电价的影响,例如2022年的大年三十是1月31日,由于调休,1月28日-1月30日上班,1月31日-2月6日放假。果然在春节假期中也出现了大量的负电价。这说明节假日是判断负电价的重要特征。
4.电力市场出清价格预测主要代码解读
完整代码地址:
特征工程
针对上面的数据分析,为了进一步去考虑时间原因,因此将day和time进行合并,便于提取时间戳特征。
# 将day和time列合并成timestamp列,便于提取时间戳特征
electricity_price["timestamp"] = pd.to_datetime(
electricity_price["day"] + " " + electricity_price["time"].str.replace("24:00:00", "00:00"))
# 处理24:00:00的情况,即表示第二天的00:00:00
mask = electricity_price['timestamp'].dt.time == pd.Timestamp('00:00:00').time()
# 需要将这些行的日期部分加一天
electricity_price.loc[mask, 'timestamp'] += pd.Timedelta(days=1)
# 设置列的顺序,同时去除day和time列
electricity_price = electricity_price[["timestamp", "demand", "clearing price (CNY/MWh)","time"]]
将合并好的时间戳去按照时间特征进行提取,从而为方便考虑节假日特征。
# 提取时间特征
electricity_price["hour"] = electricity_price["timestamp"].dt.hour
electricity_price["day"] = electricity_price["timestamp"].dt.day
electricity_price["month"] = electricity_price["timestamp"].dt.month
electricity_price["year"] = electricity_price["timestamp"].dt.year
electricity_price["weekday"] = electricity_price["timestamp"].dt.weekday
electricity_price["quarter"] = electricity_price["timestamp"].dt.quarter
electricity_price["is_windy_season"] = electricity_price["timestamp"].dt.month.isin([1, 2, 3, 4, 5, 9, 10, 11, 12])
electricity_price["is_valley"] = electricity_price["timestamp"].dt.hour.isin([10, 11, 12, 13, 14, 15])
因为是光伏发电等新能源对预测价格意义重大,所以特地引入光伏数据。
# 引入光伏数据(假设数据)
pv_unit = pd.DataFrame({
"Capacity(MW)": np.random.uniform(50, 150, size=10),
"coal consumption (g coal/KWh)": np.zeros(10), # 光伏不消耗煤
"is_solar": [True] * 10 # 标记为光伏发电机组
})
# 将光伏数据添加到unit数据中
unit["is_solar"] = False
unit = pd.concat([unit, pv_unit], ignore_index=True)
电力损耗因子和电力损耗惩罚因子
数据power consumption rate主要指电厂单位时间内耗电量与发电量的百分比,例如单位时间耗电量为500度电,发电量为10000度电,利用率就是500/10000=5%。
电厂在发电过程中不可避免地会产生一定的效率损失,这些损失体现在多个方面,导致并非所有的输入能源都能被完全转化为有效的电能输出。主要的效率损失来源包括热损失、机械损失和电气损失等。热损失是最常见的形式之一,在热力发电厂中,部分热能在转换为机械能的过程中会通过散热等方式流失;机械损失则是由于设备运转时的摩擦等因素造成的;电气损失则发生在电力传输和变电过程中,如输电线路上的电阻损耗。此外,电厂还需要消耗部分自己生产的电力用于自身的运行需求,这部分称为自用电,它包括了运行辅助设备如泵、风扇、压缩机等所需的电力,以及维持控制系统、监测设备正常工作的电力消耗。自用电率是指自用电量占总发电量的比例,反映了电厂运行的内部电力需求。
因此参考了下图中的电力传输中的网损因子和网损惩罚因子进行了一个转换。
# 电力损耗惩罚因子
unit['coal consumption (g coal/KWh)'] = unit['coal consumption (g coal/KWh)'] / (1 - unit['power consumption rate (%)'] / 100)
unit['Capacity(MW)'] = unit['Capacity(MW)']*(1 - unit['power consumption rate (%)'] / 100)
ABM策略
将系统视为由多个相互作用的主体构成,每个主体拥有自己的属性和行为规则,通过模拟这些主体的相互交互来研究复杂系统的动态和行为。用于模拟拥有自主意识的智能体(独立组织或共同群体)的行动和相互作用的计算模型。
ABM 明确了模拟个体或对象的行为在时间和空间中的因果关系。其核心概念是“agent”(自主行动者),研究者在模型中设置不同的行动者,并赋予其特定的认知能力、资源、属性以及判断流程和行动模式。足够数量的自主行动者被放置在一个人工建构的世界中,按照设定的规则进行互动。随着时间的进程,通过行动者之间以及行动者和世界之间的不断互动,得到特定现象的演化历程。
ABM 主要探讨生成性因果关系,强调原因导致结果的过程,它是目前唯一可以使异质性的个体基于不同规则交流和互动,并分析其宏观结果的模型。ABM 综合了一些其他思想,如博弈论、复杂系统、涌现、计算社会学、多智能体系统和演化计算等。它具有多种优势,例如能够代表和模拟人的决策行为,侧重于对事物模式的产生过程进行探讨,而这些模式往往是从个体的行为决策中涌现的。
典型的ABM模型由以下几个部分组成:
-
代理(Agent):系统中的基本个体,每个代理都有自己的属性和行为规则。
-
环境(Environment):代理活动的空间或网络,可能影响代理的行为。
-
交互规则(Interaction Rules):代理之间、代理与环境之间的相互作用规则。
-
时间步(Time Steps):系统按离散的时间步推进,模拟出系统的动态变化过程。
建立 ABM 模型的一般过程可分为三步:
-
定义“代理人”:确定模型中所包含的代理人类型,以及它们可能具有的属性。
-
定义“代理人”的行动规则:描述每个代理人在每次迭代时依据自身状态和周边环境执行的操作。
-
定义“代理人”行动的时间跨度:通常通过定义代理人行动的变化频率来实现,这取决于模型对现实的表示。
ABM模型的影响因素
-
代理人行为规则:行为规则的合理性和准确性对模型结果有重要影响。如果规则不能准确反映代理人的实际行为模式,可能导致模型偏差。
-
数据质量和数量:输入模型的数据质量差或数量不足,可能无法充分描述系统的特征和变化,从而影响模型的准确性和可靠性。
-
模型复杂度:过于简单的模型可能无法捕捉到系统的关键特征和相互作用,而过于复杂的模型可能导致计算困难和过拟合问题。
-
初始条件设置:不同的初始条件可能使模型产生不同的结果,因此需要合理设置初始条件以反映实际情况或研究目的。
-
时间步长设置:时间步长的选择会影响模型的精度和计算效率。不合适的时间步长可能无法准确模拟系统的动态变化。
-
环境因素的考虑:环境对代理人的行为和相互作用有重要影响,需要充分考虑环境的各种因素及其变化。
-
代理人的属性和异质性:代理人的属性(如个体粒度、特征等)以及是否具有异质性会影响模型的表现和结果。
-
相互作用的描述:准确刻画代理人之间以及代理人与环境之间相互作用的方式是模型的关键之一。
-
校准和验证:缺乏充分的校准和验证过程可能难以确定模型的准确性和适用性,以及模型在不同条件下的稳定性。
上面设置的光伏发电组现在就起作用了,利用前面的鸭子曲线,决定光伏发电是否出价。具体的代码如下:
# 定义发电机组代理
class GeneratorAgent(Agent):
def __init__(self, unique_id, model, capacity, coal_consumption, is_solar=False):
super().__init__(unique_id, model)
self.capacity = capacity
self.coal_consumption = coal_consumption
self.is_solar = is_solar
self.price = self.coal_consumption
self.successful_bid = False
def adjust_price(self, market_clearing_price, demand, hour):
if self.is_solar:
# 假设光伏发电在10-15点之间最活跃
if hour >= 10 and hour <= 15:
self.price = 0 # 光伏发电在活跃时段出价为0
else:
self.price = np.inf # 非活跃时段不参与竞价
else:
# 其他机组根据市场清算价格调整出价
if not self.successful_bid:
self.price += (market_clearing_price - self.price) * 0.1
def step(self):
pass
# 定义电力市场模型
class ElectricityMarketModel(Model):
def __init__(self, agents_data, electricity_price):
self.schedule = RandomActivation(self)
self.electricity_price = electricity_price
for index, row in agents_data.iterrows():
agent = GeneratorAgent(index, self, row['Capacity(MW)'], row['coal consumption (g coal/KWh)'], row['is_solar'])
self.schedule.add(agent)
self.datacollector = DataCollector(
agent_reporters={"Capacity": "capacity", "Coal Consumption": "coal_consumption"}
)
def market_clear(self, demand, hour):
bids = [(agent.price, agent) for agent in self.schedule.agents]
bids.sort(key=lambda x: x[0])
total_capacity = 0
clearing_price = 0
for price, agent in bids:
total_capacity += agent.capacity
if total_capacity >= demand:
clearing_price = price
agent.successful_bid = True
break
agent.successful_bid = False
return clearing_price
def step(self, demand, hour):
market_clearing_price = self.market_clear(demand, hour)
for agent in self.schedule.agents:
agent.adjust_price(market_clearing_price, demand, hour)
self.datacollector.collect(self)
self.schedule.step()
# 初始化模型
sorted_unit = unit.sort_values("coal consumption (g coal/KWh)")
sorted_unit['cumulative_capacity'] = sorted_unit['Capacity(MW)'].cumsum()
# 创建市场模型并运行
market_model = ElectricityMarketModel(sorted_unit, electricity_price)
for i in range(96):
hour = electricity_price.iloc[i]["hour"]
demand = electricity_price.iloc[i]["demand"]
market_model.step(demand, hour)
# 提取模拟数据
simulation_data = market_model.datacollector.get_agent_vars_dataframe()
# 使用ABM模拟数据和电力需求进行价格预测
prices = []
# for demand in electricity_price["demand"]:
for i in range(len(electricity_price)):
demand = electricity_price["demand"].iloc[i]
price = simulation_data[simulation_data['Capacity'].cumsum() >= demand]["Coal Consumption"].iloc[0]
prices.append(price)
train_length=55392
train_data=electricity_price.copy(deep=True)
LGBMRegressor 模型
GBDT(Gradient Boosting Decision Tree)是机器学习中一种经久不衰的模型,其核心思想是通过迭代训练多个弱分类器(通常是决策树),并将它们组合起来以获得一个强预测模型。GBDT 因其优秀的训练效果和较强的泛化能力而广受青睐。它不仅在工业界得到了广泛应用,涵盖了多分类、点击率预测、搜索排序等多种任务,而且在数据挖掘竞赛中也表现卓越,据统计,Kaggle 上超过一半的冠军解决方案都基于 GBDT。
LightGBM(Light Gradient Boosting Machine)是实现 GBDT 算法的一种框架,以其高效的并行训练能力著称。相较于传统的 GBDT 实现,LightGBM 具有更快的训练速度、更低的内存消耗、更高的准确率,并且支持分布式处理,能够有效应对大规模数据集。这些特性使得 LightGBM 成为处理工业级海量数据的理想选择。
# 创建 LGBMRegressor 模型对象,设置参数
lgb_model = LGBMRegressor(num_leaves=2**5-1, n_estimators=300, verbose=-1)
# 使用训练集数据训练 LGBMRegressor 模型
# X_train:训练集特征数据
# y_train:训练集目标数据
lgb_model.fit(X_train, y_train)
# 使用训练好的 LGBMRegressor 模型预测测试集特征数据
# X_test:测试集特征数据
# 返回预测的目标值
lgb_pred = lgb_model.predict(X_test)
利用线性模型转换耗煤量为机组报价
# 创建线性回归模型对象
linear_model = LinearRegression()
# 使用训练集数据中的 "demand" 特征训练线性回归模型
# X_train[["demand"]]:训练集特征数据中仅包含 "demand" 列
# y_train:训练集目标数据
linear_model.fit(X_train[["demand"]], y_train)
# 使用训练好的线性回归模型预测测试集特征数据中的 "demand" 列
# X_test[["demand"]]:测试集特征数据中仅包含 "demand" 列
# 返回预测的目标值,并将结果展平为一维数组
linear_pred = linear_model.predict(X_test[["demand"]]).flatten()
y_pred = 0.7*linear_pred+0.3*lgb_pred
模拟随着时间火力发电价格下降
# 按照长度将y_pred分成三段进行修正,模拟鸭子曲线
n = len(y_pred)
segment1_end = n // 3
segment2_end = 2 * n // 3
# 对每一段应用不同的修正系数
y_pred[:segment1_end] *= 0.95
y_pred[segment1_end:segment2_end] *= 0.9
y_pred[segment2_end:] *= 0.85
y_pred = [f"{x:.4f}" for x in y_pred]
将每小时数据归一化
sample_submit["clearing price (CNY/MWh)"] = y_pred
# 定义一个函数来替换每组4个值为它们的最小值
def replace_with_min(group):
min_value = group.min()
return group.apply(lambda x: min_value)
# 对'clearing price (CNY/MWh)'列每4行进行分组,并替换为最小值,把一个小时内的四段归一成最小值
sample_submit['clearing price (CNY/MWh)'] = sample_submit['clearing price (CNY/MWh)'].groupby(sample_submit.index // 4).transform(replace_with_min)
考虑节假日负电价
## 确保 'day' 列为日期类型
sample_submit['day'] = pd.to_datetime(sample_submit['day'])
# 筛选出2024年2月9日之后的十天的数据
start_date = pd.to_datetime('2024-02-08')
end_date = start_date + timedelta(days=10)
filtered_df = sample_submit[(sample_submit['day'] > start_date) & (sample_submit['day'] <= end_date)]
# 进一步筛选出时间为10:00到16:00的数据
filtered_df = filtered_df[filtered_df['time'].between('10:15', '16:00')]
filtered_df['clearing price (CNY/MWh)'] = pd.to_numeric(filtered_df['clearing price (CNY/MWh)'], errors='coerce')
# 对于每一天,找到最大值并进行调整
for day in filtered_df['day'].unique():
day_data = filtered_df[filtered_df['day'] == day]
if not day_data.empty:
max_idx = day_data['clearing price (CNY/MWh)'].idxmax()
max_value = day_data.loc[max_idx, 'clearing price (CNY/MWh)']
# 将其他值设为-80
sample_submit.loc[day_data.index, 'clearing price (CNY/MWh)'] = -80
# 保留最大值
for i in range(0,4):
sample_submit.loc[max_idx+i, 'clearing price (CNY/MWh)'] = max_value*0.5
# # 筛选出2023年12月31日之后的两天的数据
# start_date = pd.to_datetime('2023-12-31')
# end_date = start_date + timedelta(days=2)
# filtered_df = sample_submit[(sample_submit['day'] > start_date) & (sample_submit['day'] <= end_date)]
# # 进一步筛选出时间为10:00到15:00的数据
# filtered_df = filtered_df[filtered_df['time'].between('10:15', '15:00')]
# filtered_df['clearing price (CNY/MWh)'] = pd.to_numeric(filtered_df['clearing price (CNY/MWh)'], errors='coerce')
# # 对于每一天,找到最大值并进行调整
# for day in filtered_df['day'].unique():
# day_data = filtered_df[filtered_df['day'] == day]
# if not day_data.empty:
# max_idx = day_data['clearing price (CNY/MWh)'].idxmax()
# max_value = day_data.loc[max_idx, 'clearing price (CNY/MWh)']
# # 将其他值设为-80
# sample_submit.loc[day_data.index, 'clearing price (CNY/MWh)'] = -80
# # 保留最大值
# for i in range(0,4):
# sample_submit.loc[max_idx+i, 'clearing price (CNY/MWh)'] = max_value*0.5
# # 筛选出2023年4月1日之后的两天的数据
# start_date = pd.to_datetime('2024-4-1')
# end_date = start_date + timedelta(days=3)
# filtered_df = sample_submit[(sample_submit['day'] > start_date) & (sample_submit['day'] <= end_date)]
# # 进一步筛选出时间为10:00到15:00的数据
# filtered_df = filtered_df[filtered_df['time'].between('10:15', '15:00')]
# filtered_df['clearing price (CNY/MWh)'] = pd.to_numeric(filtered_df['clearing price (CNY/MWh)'], errors='coerce')
# # 对于每一天,找到最大值并进行调整
# for day in filtered_df['day'].unique():
# day_data = filtered_df[filtered_df['day'] == day]
# if not day_data.empty:
# max_idx = day_data['clearing price (CNY/MWh)'].idxmax()
# max_value = day_data.loc[max_idx, 'clearing price (CNY/MWh)']
# # 将其他值设为-80
# sample_submit.loc[day_data.index, 'clearing price (CNY/MWh)'] = -80
# # 保留最大值
# for i in range(0,4):
# sample_submit.loc[max_idx+i, 'clearing price (CNY/MWh)'] = max_value
# # 筛选出2023年9月28日之后的两天的数据 端午节
# start_date = pd.to_datetime('2023-9-28')
# end_date = start_date + timedelta(days=2)
# filtered_df = sample_submit[(sample_submit['day'] > start_date) & (sample_submit['day'] <= end_date)]
# # 进一步筛选出时间为10:00到15:00的数据
# filtered_df = filtered_df[filtered_df['time'].between('10:15', '15:00')]
# filtered_df['clearing price (CNY/MWh)'] = pd.to_numeric(filtered_df['clearing price (CNY/MWh)'], errors='coerce')
# # 对于每一天,找到最大值并进行调整
# for day in filtered_df['day'].unique():
# day_data = filtered_df[filtered_df['day'] == day]
# if not day_data.empty:
# max_idx = day_data['clearing price (CNY/MWh)'].idxmax()
# max_value = day_data.loc[max_idx, 'clearing price (CNY/MWh)']
# # 将其他值设为-80
# sample_submit.loc[day_data.index, 'clearing price (CNY/MWh)'] = -80
# # 保留最大值
# for i in range(0,4):
# sample_submit.loc[max_idx+i, 'clearing price (CNY/MWh)'] = -80
5. 优化思路
采用更好的时序预测模型
TimesNet模型
TimesNet,它在短周期序列的预测上排名第一,其在捕捉时序中的多周期关系是其成功的核心。
TimesNet根据时间序列的多周期性,它的模块化架构能够捕捉来自不同周期的时间模式。对于每个周期,为了捕捉相应的周期内和周期间的变化,我们在TimesNet中设计了一个TimesBlock,它可以将一维时间序列转换为二维空间,并通过一个参数高效的inception模块同时对这两种变化进行建模。
核心思想
-
使用一种模块化的方法来进行时间变化建模。通过将一维时间序列转换为二维空间,这样可以同时呈现周期内和周期间的变。这样我们便可以挖掘多重周期性以及周期内和周期间复杂交互。
-
TimesNet使用TimesBlock通过一个参数高效的Inception模块来发现多个周期,并从转换后的二维张量中捕捉时间上的二维变化。
参考代码:
class TimesBlock(nn.Module):
def __init__(self, configs):
super(TimesBlock, self).__init__()
self.seq_len = configs.seq_len
self.pred_len = configs.pred_len
self.k = configs.top_k
# parameter-efficient design
self.conv = nn.Sequential(
Inception_Block_V1(configs.d_model, configs.d_ff,
num_kernels=configs.num_kernels),
nn.GELU(),
Inception_Block_V1(configs.d_ff, configs.d_model,
num_kernels=configs.num_kernels)
)
def forward(self, x):
B, T, N = x.size()
# 1. FFT转化
period_list, period_weight = FFT_for_Period(x, self.k)
# 2. 1维变2维并使用卷积处理之后转化为原先的长度
res = []
for i in range(self.k):
period = period_list[i]
# padding
if (self.seq_len + self.pred_len) % period != 0:
length = (
((self.seq_len + self.pred_len) // period) + 1) * period
padding = torch.zeros([x.shape[0], (length - (self.seq_len + self.pred_len)), x.shape[2]]).to(x.device)
out = torch.cat([x, padding], dim=1)
else:
length = (self.seq_len + self.pred_len)
out = x
# reshape
out = out.reshape(B, length // period, period,
N).permute(0, 3, 1, 2).contiguous()
# 2D conv: from 1d Variation to 2d Variation
out = self.conv(out)
# reshape back
out = out.permute(0, 2, 3, 1).reshape(B, -1, N)
res.append(out[:, :(self.seq_len + self.pred_len), :])
# 3. softmax 和 res相乘之后做residual链接
res = torch.stack(res, dim=-1)
# adaptive aggregation
period_weight = F.softmax(period_weight, dim=1)
period_weight = period_weight.unsqueeze(
1).unsqueeze(1).repeat(1, T, N, 1)
res = torch.sum(res * period_weight, -1)
res = res + x
return res
时序多步预测
大多数预测问题都被定义为单步预测,根据最近发生的事件预测系列的下一个值。时间序列多步预测需要预测未来多个值, 提前预测许多步骤具有重要的实际优势,多步预测减少了长期的不确定性。 但模型试图预测更远的未来时,模型的误差也会逐渐增加。
递归预测
多步预测最简单的方法是递归形式,训练单个模型进行单步预测,然后将模型与其先前的预测结果作为输入得到后续的输出。
from sklearn.linear_model import LinearRegression
# using a linear regression for simplicity. any regression will do.
recursive = LinearRegression()
# training it to predict the next value of the series (t+1)
recursive.fit(X_tr, Y_tr['t+1'])
# setting up the prediction data structure
predictions = pd.DataFrame(np.zeros(Y_ts.shape), columns=Y_ts.columns)
# making predictions for t+1
yh = recursive.predict(X_ts)
predictions['t+1'] = yh
# iterating the model with its own predictions for multi-step forecasting
X_ts_aux = X_ts.copy()
for i in range(2, Y_tr.shape[1] + 1):
X_ts_aux.iloc[:, :-1] = X_ts_aux.iloc[:, 1:].values
X_ts_aux['t-0'] = yh
yh = recursive.predict(X_ts_aux)
predictions[f't+{i}'] = yh
递归方法只需要一个模型即可完成整个预测范围,且无需事先确定预测范围。
但此种方法用自己的预测作为输入,这导致误差逐渐累计,对长期预测的预测性能较差。
多目标回归
多目标回归为每一个预测结果构建一个模型,如下是一个使用案例:
from sklearn.multioutput import MultiOutputRegressor
direct = MultiOutputRegressor(LinearRegression())
direct.fit(X_tr, Y_tr)
direct.predict(X_ts)
scikit-learn的MultiOutputRegressor
为每个目标变量复制了一个学习算法。在这种情况下,预测方法是LinearRegression
。
此种方法避免了递归方式中错误传播,但多目标预测需要更多的计算资源。此外多目标预测假设每个点是独立的,这是违背了时序数据的特点。
递归多目标回归
递归多目标回归结合了多目标和递归的思想。为每个点建立一个模型。但是在每一步的输入数据都会随着前一个模型的预测而增加。
from sklearn.multioutput import RegressorChain
dirrec = RegressorChain(LinearRegression())
dirrec.fit(X_tr, Y_tr)
dirrec.predict(X_ts)
特征工程
考虑更多的特征工程会带来更好的提升,我提几个可以优化的地方
1.参考上面的时序预测优化,可以考虑不同模型使用不同特征。
2.针对已有特征,计算其高级特征,通过特征两两之间的2阶交互项可能会提供更好的预测效果。
3.考虑随着时间增加更多光伏机组
4. 外部天气
5. 煤价趋势
6. PS
首先感谢dataWhale提供这次难得学习机会,还有感谢上海科学智能研究院提供的这次比赛,很遗憾因为个人原因没有提交最后的结果导致错失了决赛,但是也从这次比赛中学到了很多,在此对两个团队人员表示真挚的感谢。这篇内容是对这次学习的一个总结,利用ABM策略加时序预测来进行了一个优化,ABM策略中加入了光伏发电机组方便模拟外部环境对于电力市场影响,时间步设置为了一天(24*4=96),时序预测了主要用到LGB和线性回归两个模型,通过调整比例发现线性回归模型预测内容可能更接近实际效果,故而设置了3:7的比例。