第三章:数据获取与探索——打好电价预测的第一口井

各位同学,欢迎来到第三章。前面我们聊了电价预测为什么难做,也搭建了基本的技术框架。今天咱们要干一件最接地气、但也最容易翻车的事——把数据搞到手,并且搞清楚它到底长什么样

我个人习惯把数据获取比作「打井」。你想想看,井打歪了,后面再牛的算法也白搭。我在项目里见过太多人,模型调得飞起,最后发现是数据源出了问题——嗯,那种感觉,就像你跑完马拉松发现跑错了赛道。

3.1 数据来源:两大主流市场

电价预测领域,有两个数据源是绕不开的。一个是美国的PJM,一个是欧洲的Nord Pool。我建议初学者先从PJM入手,因为它的数据接口最友好,文档也最全。

3.1.1 PJM(宾夕法尼亚-新泽西-马里兰联合电力市场)

PJM覆盖美国13个州,是全世界最大的竞争性电力市场之一。它的数据公开、免费、实时更新,简直是做研究的宝库。

数据类别 说明 获取方式
实时电价(LMP) 每5分钟一个点,分节点、区域、枢纽 PJM API / 数据矿
日前电价(DALMP) 提前一天发布的预测电价 PJM Data Miner 2
负荷数据 实际负荷与预测负荷 PJM OASIS
发电组合 煤、气、风、光等出力情况 PJM Generation
我的小技巧: 别一上来就拉全量数据。先取最近30天的数据做测试,确认字段没问题了,再拉历史数据。我曾经一次性拉了3年的数据,结果发现有个字段名变了——那叫一个酸爽。

3.1.2 Nord Pool(北欧电力市场)

Nord Pool覆盖北欧四国(挪威、瑞典、芬兰、丹麦),它的特点是水电占比高,电价受天气影响极大。如果你要做中长期预测,Nord Pool的数据更有挑战性。

Nord Pool的数据获取稍微麻烦一点,需要注册账号。但它的系统电价(System Price)和区域电价(Area Price)是很好的研究素材。

3.2 数据字段解读:别被字段名骗了

拿到数据后,第一件事不是跑模型,而是搞清楚每个字段到底代表什么。我见过有人把「预测负荷」当「实际负荷」用,结果模型精度惨不忍睹。

以PJM的实时电价数据为例,常见的字段有:

  • datetime:时间戳,注意时区!PJM用的是东部时间(EST/EDT),夏令时要注意
  • node_id:节点编号,不同节点电价差异很大
  • lmp:节点边际电价(Locational Marginal Price),单位是$/MWh
  • congestion:阻塞成本,反映输电线路拥堵情况
  • loss:网损成本
  • system_energy:系统能量价格
避坑指南: 我曾经在项目里发现,PJM的「lmp」字段在不同数据接口里单位不一样!有的返回$/MWh,有的返回cents/kWh。差了100倍!所以拿到数据后,先算个均值,看看是否在合理范围内(PJM的LMP通常在20-100 $/MWh之间)。

3.3 数据质量检查:脏数据是模型的天敌

数据拿到手了,字段也看懂了。接下来就是最枯燥但最重要的一步——数据质量检查。我总结了一个「三步检查法」:

第一步:缺失值检查

电力数据最常见的缺失原因有两个:通信中断和系统维护。直接删掉缺失行?别急,先看看缺失模式。

import pandas as pd
import numpy as np

# 加载数据
df = pd.read_csv('pjm_lmp_2024.csv', parse_dates=['datetime'])

# 检查缺失率
missing_rate = df.isnull().mean()
print('各字段缺失率:')
print(missing_rate)

# 看看缺失的时间分布
df['date'] = df['datetime'].dt.date
missing_by_date = df.groupby('date')['lmp'].apply(lambda x: x.isnull().mean())
print('\n每日缺失率(前10天):')
print(missing_by_date.head(10))

如果缺失率低于5%,而且分布随机,我一般用前向填充(ffill)处理。但如果某一天缺失率超过20%,那就要警惕了——可能是那天数据源出了问题。

第二步:异常值检测

电价数据里经常出现「尖刺」——比如正常50 $/MWh,突然蹦出个5000 $/MWh。这可能是真实的市场事件(比如极端天气),也可能是数据错误。

# 用IQR方法检测异常值
Q1 = df['lmp'].quantile(0.25)
Q3 = df['lmp'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 3 * IQR
upper_bound = Q3 + 3 * IQR

outliers = df[(df['lmp'] < lower_bound) | (df['lmp'] > upper_bound)]
print(f'检测到 {len(outliers)} 个异常值')
print(outliers[['datetime', 'lmp']].head())
关键判断: 异常值不一定是错误。如果异常值出现在极端天气日(比如寒潮、热浪),那可能是真实的市场反应。我的做法是:先标记,然后结合天气数据做交叉验证。

第三步:时间连续性检查

电力数据必须是等间隔的。PJM的实时数据是5分钟一个点,如果中间有跳点,模型会出问题。

# 检查时间间隔是否均匀
df = df.sort_values('datetime')
time_diff = df['datetime'].diff().dropna()

# 统计间隔分布
print('时间间隔统计(分钟):')
print(time_diff.dt.total_seconds().div(60).describe())

# 找出间隔异常的点
abnormal_gaps = time_diff[time_diff.dt.total_seconds() > 360]  # 超过6分钟
if len(abnormal_gaps) > 0:
    print(f'\n发现 {len(abnormal_gaps)} 个异常间隔')
    print(abnormal_gaps.head())

嗯,这里要注意:如果发现间隔不均匀,别急着插值。先搞清楚原因——是数据源本身就不连续,还是我拉数据的时候漏了。我曾经因为API限流,导致数据中间缺了2个小时,后来重跑了一遍才补上。

3.4 知识体系总览

说了这么多,咱们用一张图把本章的核心逻辑串起来:

数据获取与探索核心流程 数据来源 数据来源 PJM(美国) Nord Pool(北欧) 数据字段解读 datetime | node_id | lmp | congestion | loss | system_energy 数据质量检查(三步法) ① 缺失值检查 ② 异常值检测 ③ 时间连续性 输出:干净、连续、可用的电价时间序列

这张图把咱们今天讲的内容串起来了。从数据源出发,经过字段解读,再到质量检查,最终得到干净可用的数据。每一步都马虎不得。

最后说一句: 数据获取和探索这个环节,占整个项目时间的30%以上是正常的。别着急,慢慢来。我见过太多人急着跑模型,结果数据有问题,后面全白干。记住:垃圾进,垃圾出

公众号:蓝海数据掘金营,微信deep3321