4. 探索性数据分析(EDA):电价分布可视化、相关性热力图、季节性分解、趋势与周期分析

好,咱们进入第四章。说实话,很多同学做模型调优,一上来就调参数、换网络结构,结果效果死活上不去。为什么?因为你根本不了解你的数据。我个人的习惯是,拿到任何电价数据集,先花至少半天时间做EDA。这一步做扎实了,后面调参事半功倍。

电价数据有个特点——它既受物理规律约束(发电成本、供需平衡),又受市场情绪影响(交易策略、政策变动)。所以它的分布、相关性、季节性都很有嚼头。咱们一个一个来看。

4.1 电价分布可视化:看看你的数据长什么样

先问个问题:电价是正态分布吗?

我告诉你,大概率不是。我在欧洲电力市场项目中见过,电价分布往往是右偏的,而且有很长的尾巴。什么意思?就是大部分时间电价在低位徘徊,但偶尔会飙到天价——比如寒潮来袭、核电机组跳闸的时候。

咱们用Python画个直方图加核密度估计曲线,一眼就能看出来。

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# 假设df是包含电价数据的DataFrame,'price'是电价列
plt.figure(figsize=(10, 6))
sns.histplot(df['price'], bins=50, kde=True, color='steelblue')
plt.title('电价分布直方图 + KDE曲线', fontsize=14)
plt.xlabel('电价 (EUR/MWh)')
plt.ylabel('频次')
plt.axvline(df['price'].median(), color='red', linestyle='--', label=f'中位数: {df["price"].median():.2f}')
plt.axvline(df['price'].mean(), color='green', linestyle='--', label=f'均值: {df["price"].mean():.2f}')
plt.legend()
plt.show()

关键发现:

  • 如果均值远大于中位数,说明数据右偏,存在极端高电价
  • 如果出现双峰甚至多峰分布,说明市场可能存在两种或多种典型状态(比如白天/夜间、工作日/周末)
  • 我见过最夸张的一次,德国某天的电价冲到500+ EUR/MWh,而平时只有30-40。这种极端值如果不处理,模型会被带偏

避坑指南: 我曾经在做一个短期预测项目时,没做分布分析就直接训练LSTM。结果模型在测试集上表现极差,后来发现训练集里根本没有包含极端电价样本。所以,分布可视化能帮你判断是否需要做分层采样或数据增强。

4.2 相关性热力图:找到电价的“朋友圈”

电价不是孤立存在的。它跟天气、负荷、燃料价格、甚至节假日都有关系。相关性热力图就是帮你快速找出哪些变量跟电价“走得近”。

我个人习惯用Spearman秩相关系数,而不是Pearson。为什么?因为电价和很多特征的关系不是线性的。比如气温和电价——太冷或太热都会推高电价,中间温度反而低。这种U型关系,Pearson算出来可能接近0,但Spearman能捕捉到。

# 选取数值型特征列
features = ['price', 'load', 'temperature', 'wind_speed', 'solar_irradiance', 
            'gas_price', 'coal_price', 'hour', 'day_of_week', 'month']

# 计算Spearman相关系数矩阵
corr_matrix = df[features].corr(method='spearman')

# 绘制热力图
plt.figure(figsize=(12, 10))
sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='RdBu_r', 
            center=0, square=True, linewidths=0.5)
plt.title('电价与各特征的Spearman相关性热力图', fontsize=14)
plt.tight_layout()
plt.show()
特征 与电价的Spearman相关系数 解读
负荷 (load) 0.45 ~ 0.65 正相关,用电越多电价越高
天然气价格 0.30 ~ 0.50 气电是边际机组,直接影响电价
太阳能辐射 -0.20 ~ -0.40 中午光伏大发时,电价往往被压低
风速 -0.10 ~ -0.30 风电出力增加会拉低电价
小时 (hour) 0.10 ~ 0.30 白天电价通常高于夜间

注意: 相关性不等于因果。我见过有人把“冰淇淋销量”和“电价”做相关分析,发现相关系数0.6,就以为吃冰淇淋会导致电价上涨。其实是因为夏天两者都高。所以,做特征选择时,要结合领域知识判断。

4.3 季节性分解:拆开电价的“洋葱”

电价数据有明显的多重季节性。你想想看:

  • 日季节性: 早高峰、晚高峰、夜间低谷
  • 周季节性: 工作日高、周末低
  • 年季节性: 冬夏高、春秋低

怎么把这些成分拆开?经典方法是STL分解(Seasonal-Trend decomposition using LOESS)。我推荐用statsmodels库的seasonal_decompose,但要注意——它只支持单一周期。对于电价这种多周期数据,我一般会先做日周期分解,再对残差做周周期分解。

from statsmodels.tsa.seasonal import seasonal_decompose

# 假设数据是小时粒度,周期为24小时(日周期)
decomposition = seasonal_decompose(df['price'].values, model='additive', period=24)

# 提取成分
trend = decomposition.trend
seasonal = decomposition.seasonal
residual = decomposition.resid

# 可视化
plt.figure(figsize=(14, 10))
plt.subplot(4, 1, 1)
plt.plot(df.index, df['price'], label='原始电价', color='gray')
plt.legend()
plt.subplot(4, 1, 2)
plt.plot(df.index, trend, label='趋势成分', color='red')
plt.legend()
plt.subplot(4, 1, 3)
plt.plot(df.index[:48], seasonal[:48], label='日季节性成分(前48小时)', color='blue')
plt.legend()
plt.subplot(4, 1, 4)
plt.plot(df.index, residual, label='残差成分', color='green', alpha=0.6)
plt.legend()
plt.tight_layout()
plt.show()

实战经验: 我在做北欧电力市场项目时,发现残差里还有明显的周模式。于是我对残差再做一次周期=168(7天×24小时)的分解。这样层层剥离,最后剩下的残差基本就是随机噪声了。模型要预测的,其实就是这个“干净”的残差。

4.4 趋势与周期分析:看清大方向

趋势分析告诉你电价在长期是涨还是跌。周期分析告诉你什么时候涨、什么时候跌。

趋势分析: 我一般用两种方法:

  1. 移动平均: 用30天或90天窗口平滑数据,看长期走向
  2. HP滤波器: 把趋势和周期分开,参数lambda选1600(季度数据)或14400(月度数据)
from statsmodels.tsa.filters.hp_filter import hpfilter

# HP滤波器,lambda=1600适用于季度数据,这里用14400(月度)
cycle, trend = hpfilter(df['price'], lamb=14400)

plt.figure(figsize=(12, 6))
plt.plot(df.index, df['price'], label='原始电价', alpha=0.5, color='gray')
plt.plot(df.index, trend, label='HP趋势', linewidth=2, color='darkred')
plt.plot(df.index, cycle, label='HP周期', linewidth=1, color='navy', alpha=0.7)
plt.legend()
plt.title('电价趋势与周期分析(HP滤波器)', fontsize=14)
plt.show()

周期分析: 用自相关函数(ACF)和偏自相关函数(PACF)来识别周期长度。

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

fig, axes = plt.subplots(2, 1, figsize=(12, 8))
plot_acf(df['price'].dropna(), lags=168, ax=axes[0])  # 看7天的自相关
plot_pacf(df['price'].dropna(), lags=168, ax=axes[1])
plt.tight_layout()
plt.show()

ACF图上,如果每隔24小时出现一个峰值,说明日周期显著;每隔168小时出现峰值,说明周周期显著。PACF图则告诉你,要预测当前电价,需要回溯多少个小时的历史数据。

小技巧: 我习惯把ACF/PACF的峰值位置打印出来,做成表格。这样在后续选择LSTM的lookback窗口大小时,就有据可依了。比如ACF在lag=24、48、168处都有显著峰值,那lookback至少取168。

4.5 本章知识体系

下面这张图总结了EDA阶段的核心逻辑。说白了,就是“先看分布、再看关系、再拆成分、最后定周期”。每一步的输出都会直接影响后续的特征工程和模型设计。

电价EDA知识体系 分布可视化 相关性热力图 季节性分解 趋势与周期分析 直方图 + KDE曲线 偏度、峰度、异常值检测 Spearman相关系数矩阵 特征与电价的关系强度 STL分解:趋势+季节+残差 日周期(24h) / 周周期(168h) 移动平均 / HP滤波器 ACF/PACF自相关分析 输出:特征工程 & 模型设计依据 EDA不是一次性工作,而是迭代过程 每次发现新特征,都要回到EDA重新验证

嗯,EDA这部分内容不少,但每一步都值得你亲手跑一遍代码。我当年刚入行时,总觉得EDA是“体力活”,想跳过直接建模。结果被现实狠狠教育了一波——数据质量差、特征冗余、周期没对齐,模型怎么调都过拟合。从那以后,我再也不敢轻视EDA了。

记住一句话:好的EDA,是成功模型的一半。

专注资料整理