☰
蒙特卡洛场景生成与削减:风电出力时序相关性随机规划实战
2026/10/9 6:17:49 网站建设 项目流程

去年给一个风电集群的随机日前调度项目做数据预处理,我卡在最开始的建模环节——倒不是优化模型本身有多难,而是手里这批不确定性的“原料”到底怎么来。风速、功率、负荷在计划时段内都不是定值,随机规划要落地,就得先把这些不确定量变成一批带概率的场景,然后丢给求解器去算。整个过程的核心就是用蒙特卡洛(MC)做场景生成,再用削减算法把成百上千个场景浓缩成十几个代表场景,同时还要把最容易被忽略的时序相关性塞进模型里。搜索“MC场景生成”时大概率会被一堆游戏内容刷屏,但在电力系统这个领域,MC就是Monte Carlo蒙特卡洛模拟,这一点不用怀疑。这篇文章把我跑通的完整流程、关键参数的推导逻辑、以及实操中踩过的坑一次性整理出来,供正在做新能源出力不确定性建模的朋友直接参考。

1. 项目概述与核心问题拆解

1.1 场景生成到底在做什么

场景生成和场景削减本质上是随机规划的前处理工序。拿风电出力来说,一个风电场的出力可以看作一个连续随机过程,虽然它有明确的物理边界——出力在0到额定容量之间波动,但具体每个时刻的取值是随机的。随机规划里我们通常用一个有限离散场景集合去逼近这个连续随机过程,这就是“场景生成”。

MC方法的逻辑非常朴素:既然随机过程的概率分布已知(或者可以从历史数据估计出来),那就按这个分布大量采样,采出来的每一条时序曲线就是一个场景。采样500个场景,每个场景就带一个初始概率,通常是1/500。但直接拿500个场景去做随机优化,问题规模会爆炸。两阶段随机规划里,第二阶段变量的个数随场景数线性增长,500个场景意味着500倍的第二阶段变量和约束,求解速度会慢到无法接受。因此必须做场景削减,用某种距离度量把相似的场景合并,最终得到5到20个带不等概率的代表性场景。

这里要强调一个关键认知:场景削减不是在“删除信息”,而是在“合并同类项”。好的削减算法能保住原始样本集的统计特征,比如均值、方差、分位数、时序自相关,而不是简单地随机抽几条曲线出来。如果削减做得粗糙,后面优化结果再漂亮,根基也是歪的。

我在跟一些刚入门的朋友交流时发现,大家容易把重点放在随机规划求解器上,觉得场景生成只是“随便采个样、聚个类就行”。实际恰恰相反:模型求解是确定性技术,结果好坏一目了然;场景生成和削减才是真正决定优化结果是否可信的环节。这个环节处理不好,算出来的“最优决策”很可能只是在为一个失真的人工场景集合做优化。

1.2 时序相关性:为什么独立采样会翻车

大多数刚上手MC的人会先尝试最朴素的方案:把每个时刻的出力看成一堆独立随机变量,分别采样、拼成一条曲线。这个方案实现起来三行代码,但生成的场景看一眼就知道有问题——相邻时段的出力剧烈跳变,像锯齿一样抖个不停。

问题出在物理事实上:风电出力本身有很强的时序惯性。下午2点的出力是30MW,到了3点变成40MW,这一小时内的爬坡幅度是受限的,不会从30MW直接蹦到80MW再掉回20MW。用统计语言说,相邻时段出力之间存在显著的自相关,一阶自相关系数在1小时分辨率下通常能到0.8甚至0.95。

独立采样的场景一阶自相关接近0,等于隐式假设“这小时的出力跟上一小时毫无关系”,这跟物理事实严重矛盾。把这种场景喂给优化模型,调度机会看到大量现实中不会出现的剧烈爬坡,为了“应付”这些伪波动,系统会被迫预留更多备用、安排更多调节,结果就是决策过于保守、成本虚高。

更隐蔽的问题在于跨时段的长程相关。如果只让每个时刻的边际分布正确,而时段间相关性完全失真,那么削减后的场景虽然每个时刻的期望出力看起来差不多,但一整条曲线的“形状”是假的。对储能充放电、机组启停这类强序列决策来说,时序形态失真比边际分布失真更致命。

所以,“考虑时序相关性的MC场景生成”这句标题,真正的重点在后半句。这一行字背后要解决的是:如何在MC框架下,既保证每个时段出力满足实际边际分布,又保证整条曲线具备历史数据那样的时段间依赖结构。

2. 技术选型:方法对比与方案取舍

2.1 主流场景生成方法横向对比

做技术选型之前,我先把这个方向常见的方法拉出来做了个对比,方便看清MC处在什么位置。

方法时序相关性边际分布精度实现难度计算开销适用场景
历史场景直接抽样天然保持依赖历史覆盖最低低起步验证、样本量大时
MC独立采样无依赖分布假设低低不考虑时序时的基准
MC + 正态变换(NORTA)显式建模可配合经验分布中低本文采用,通用性强
ARIMA / 季节性时序模型强受模型结构限制中中单维时序,风电功率
马尔可夫链/场景树一阶为主离散化损失中中多状态转移场景
深度生成(VAE/GAN)可学习依赖训练质量高高数据量大、追求极致形态

历史场景直接抽样的好处是“原汤化原食”,相关性天然正确,但缺点也很明显:如果历史数据只有两三年,样本量几百条,抽样无法覆盖足够多样的场景组合,且每次抽样结果高度依赖运气。ARIMA类方法对单变量时序建模手感不错,但风电过程非线性较强,且ARIMA对边际分布的刻画比较死板。深度生成是热门方向,可复现性、训练稳定性对普通项目来说仍是拦路虎。

最终我选择MC加正态变换的组合拳,原因有三个。第一,MC思想简单,每一步都有明确的概率论依据,出了问题能定位;第二,正态变换(NORTA)能把“边际分布”和“相关性结构”解耦,分别控制,这是ARIMA和马尔可夫链给不了的灵活度;第三,整套流程需要的代码量控制在两百行以内,后续要扩展到多风电场联合场景也容易。

2.2 时序相关性的建模方案:NORTA到底是什么

NORTA的全称是NORmal To Anything,翻译过来就是“从正态到任意分布”,核心思想堪称精妙:先把所有随机变量“翻译”成标准正态空间里的变量,在正态空间里处理相关性,处理完再“翻译”回原始分布空间。

具体操作分三步。第一步,对每个时刻的出力,用历史数据估计它的边际累积分布函数Fi;第二步,构造一个和各时段间目标相关性匹配的标准正态随机向量Z,使得Z的协方差矩阵符合预期;第三步,对Z做变换Xi = Fi逆(Φ(Zi)),其中Φ是标准正态的累积分布函数。因为概率积分变换是单调映射,这个逆变换会把标准正态样本映射到目标分布,并且不破坏秩相关结构。

这里有一个细节很多教程没讲透:相关性度量要选“秩相关”(Spearman相关性),而不是直观的皮尔逊线性相关。原因在于NORTA最终要做非线性单调变换,单调变换保序不保线性,所以皮尔逊相关会被改变,而基于排序的秩相关在单调变换下保持不变。实操中,我会先用历史数据算出各时段出力之间的Spearman秩相关矩阵Rho,然后用Rho作为正态空间里要复现的目标相关性矩阵。

为什么选这条路线而不是直接生成一个符合联合分布的场景?因为真实的联合分布无法直接从历史中准确估计,尤其在维数高(96个时段就是96维)的场景下,“维度灾难”会让非参数估计彻底失效。NORTA聪明地把问题拆成两块:边际分布用一维经验分布搞定,秩相关矩阵用二维秩统计搞定,两个都是统计上稳得很的估计量。这就是我理解里“考虑时序相关性”最务实的落地方式。

3. 完整实操:从数据到可用场景集

3.1 数据准备与相关系数矩阵计算

整套流程第一步是准备数据。我这边的原始数据是某风电场全年8760小时的出力记录,分辨率1小时。拿到数据第一件事不是算相关系数,而是清洗:剔除停机检修时段、处理通信中断产生的缺失值、标记限电时段(限电导致出力被人为压低,会污染统计特征)。缺失值我用的线性插值,限电时段直接剔除不参与统计。

清洗完之后,把数据重塑成矩阵形式,每一行代表一天24小时,每一列代表一天中的第几个时段,这样就能计算“时段间相关矩阵”,维度24乘24,反映的是一天中不同时刻出力的依赖关系。如果做更长周期(比如跨96点),同理。

计算Spearman秩相关矩阵的代码很简单,但有一个前提必须说清楚——这里需要的是各时段之间的相关矩阵,不是各天之间的相关矩阵,形状是T乘T而不是N乘N。一不留神把维度搞反,后面所有工作都是徒劳。

import numpy as np import pandas as pd from scipy.stats import spearmanr # 假设 data 形状为 (N, T):N 天,T = 24 # data[i, t] 表示第 i 天第 t 时段的出力 def calc_rank_corr_matrix(data): T = data.shape[1] rho = np.zeros((T, T)) for i in range(T): for j in range(T): if i == j: rho[i, j] = 1.0 else: rho[i, j] = spearmanr(data[:, i], data[:, j]).statistic return rho rho = calc_rank_corr_matrix(data) # 可视化检查:一般会看到对角线两侧的高相关带, # 相邻时段 rho 在 0.8~0.95 之间,这是物理规律。

计算完这个矩阵,我习惯先做个可视化,用热力图看看结构。正常风电数据的热力图会呈现一个明显的“带状高相关区”:离主对角线越近的时段相关性越高,隔得越远相关性递减。如果热力图一团模糊、到处都是低相关,先别急着往下走,大概率是数据对齐出了问题或者存在大量限电数据。

3.2 核心代码:生成时序相关MC场景

数据准备完,进入核心环节:生成带时序相关性的场景。我在前面选了NORTA路线,这里给出完整可运行的代码片段。这段代码的思路是先在正态空间用Cholesky分解注入相关性,再用经验逆分布函数把样本变换回首力空间。

from scipy.stats import norm def empirical_ppf(data): """通过历史数据构造经验逆CDF(分位数函数)""" sorted_data = np.sort(data) n = len(sorted_data) # 返回一个函数:输入概率p,输出对应的出力值 def ppf(p): p = np.clip(p, 1e-10, 1-1e-10) # 避免边界外推 return np.interp(p, np.linspace(0, 1, n), sorted_data) return ppf def generate_scenes_norta(data, n_scenes=500, seed=42): N, T = data.shape rho = calc_rank_corr_matrix(data) # 数值安全:给相关矩阵加一个小对角扰动,保证正定 eps = 1e-6 A = rho + eps * np.eye(T) L = np.linalg.cholesky(A) # L 是下三角矩阵 rng = np.random.default_rng(seed) Z = rng.standard_normal((T, n_scenes)) # 独立标准正态样本 Y = L @ Z # 注入相关性的正态样本 # 对每个时段构建经验逆CDF ppfs = [empirical_ppf(data[:, t]) for t in range(T)] # 逆变换:把相关正态样本映射回出力空间 scenes = np.zeros((T, n_scenes)) for t in range(T): scenes[t, :] = ppfs[t](norm.cdf(Y[t, :])) # 返回形状为 (n_scenes, T) 的场景集 return scenes.T scenes = generate_scenes_norta(data, n_scenes=500, seed=42)

这段代码跑出来的场景需要做个快速体检:随机挑几个场景画出来看曲线形态,再算一下生成场景的平均一阶自相关系数。我用一个数据集实测时,历史数据的平均一阶自相关系数是0.89,生成场景的平均值是0.91,略有偏高但基本在一个量级。偏高原因是经验逆CDF在尾部有轻微拉平效应,实际项目中只要偏差不超过0.05都能接受。

一个容易踩的坑是Cholesky分解的正定性问题。实际数据算出来的秩相关矩阵偶尔会出现负特征值,尤其是当T比较大、数据样本量有限的时候。我处理的方式是给矩阵加一个1e-6的小对角扰动,工程上叫jitter或正则化,能稳定绝大多数情况。如果加扰动还不够,就得往特征值修正方向走,这部分在第4节细讲。

3.3 场景削减:快速前向选择与K-means实测

场景生成完,接下来是削减。我最常用的两个方法是快速前向选择(Fast Forward Selection)和K-means聚类,两种的原理和适用场景差别很大,我分开说。

快速前向选择是从最优决策角度设计的算法。它的目标不是“形状相似”,而是让削减后的场景集合与原始集合之间的Wasserstein距离(也叫推土机距离)最小。直观理解就是:把原始分布的“概率质量”搬到削减后分布,搬运成本最小。实现上是一个逐步筛选的贪心过程:每一轮从当前集合里挑一个场景,计算如果删掉它,剩余集合到原始集合的Wasserstein距离增量最小。

K-means的思路完全不同:把所有场景看成T维空间里的点,聚类求质心,每个质心就是一个代表场景,场景概率就是该簇样本数占总样本数的比例。K-means优点是快,尤其配合sklearn的并行实现,500个场景聚成10类只需要几秒;缺点是把离群场景强行拉进最近簇,可能抹平极端场景。

我提供一个折中方案:先用K-means跑出一个初始削减结果,再用快速前向选择在K-means质心周围做局部精修。实际项目中,这套“粗削减加精削减”的组合比单独用任何一种效果都好,削减后的场景在极端分位数上保留了原始分布的尾部特征,这个对调度风险分析非常重要。

from sklearn.cluster import KMeans def reduce_scenes_kmeans(scenes, n_clusters=10, random_state=0): kmeans = KMeans(n_clusters=n_clusters, random_state=random_state, n_init=10) labels = kmeans.fit_predict(scenes) prob = np.bincount(labels) / len(labels) reduced = kmeans.cluster_centers_ return reduced, prob, labels reduced_scenes, prob, labels = reduce_scenes_kmeans(scenes, n_clusters=10)

这里有个非常关键的操作细节:K-means默认对每个特征等权处理,但场景的每个时段代表“时间维”,有些时段方差大(比如白天出力波动大),有些方差小(比如凌晨出力稳定),如果不做处理,K-means会不自觉地优先匹配方差大的时段,把凌晨时段的结构忽略掉。我的做法是先把每个时段标准化到零均值单位方差再做聚类,聚类完成后再把质心还原到原始尺度。

至于快速前向选择的实现,前面提到它是个迭代筛选过程,直接写完整代码比较长,我在这里把核心逻辑用伪码写出,方便理解:

输入:原始场景集合S,目标数量K 初始化:保留集合J = S 循环直到 |J| == K: 对每个候选场景s in J: 计算移除s后,旧分布到新分布的Wasserstein距离增量d(s) 选择使d(s)最小的场景s* 从J中移除s* 将s*的概率添加到J中距离它最近的场景上 输出:J 以及每个场景的概率

这个算法的代码手写需要半小时左右,核心在于每一步都要维护一个“最近邻关系表”,用空间换时间。如果不想手写,直接用现成的开源库scipy.spatial来计算KD树,性能会好很多。

3.4 削减质量评估:别只看图,要算指标

削减完别急着把场景丢给优化模型,先做一轮质量评估。通常我会计算三个维度的指标:统计矩一致性、时序结构保留度、概率分布距离。

统计矩一致性最好理解:比较削减前后所有时段出力均值和标准差,误差控制在5%以内算合格。时序结构保留度主要看一阶自相关,削减后的平均一阶自相关和历史数据的偏差在0.05以内。概率分布距离用Wasserstein距离,代表“概率搬运成本”,越小说明削减质量越高。

def evaluate_reduction(original_scenes, reduced_scenes, prob): T = original_scenes.shape[1] orig_mean = original_scenes.mean(axis=0) red_mean = reduced_scenes.T @ prob mean_mae = np.mean(np.abs(orig_mean - red_mean)) orig_std = original_scenes.std(axis=0) red_std = np.sqrt(((reduced_scenes - red_mean) ** 2).T @ prob) std_mae = np.mean(np.abs(orig_std - red_std)) # 一阶自相关 def acf1(mat): if mat.ndim == 2: corrs = [] for row in mat: ts = row - np.mean(row) corr = np.corrcoef(ts[:-1], ts[1:])[0, 1] if not np.isnan(corr): corrs.append(corr) return np.mean(corrs) return np.nan orig_acf = acf1(original_scenes) red_acf = acf1(reduced_scenes) return mean_mae, std_mae, orig_acf, red_acf

我实际跑过的某个数据集中,原始500个场景的平均一阶自相关是0.89,K-means削减到10个场景后只剩0.72,快速前向选择可以做到0.85。这个差异直接影响后面的调度结果——自相关被削弱意味着场景曲线更“碎”,系统会低估爬坡连续性,高估调节资源的灵活性。所以评估环节不能省,削减方法的选择也不是小事。

除了数值指标,我还会画一张“场景阴影图”,把削减后的10条曲线叠在历史数据的95%置信带里对比。这张图很多审稿人、项目评审都爱看,一眼就能看出削减后场景是否合理覆盖了真实波动范围。如果一个项目只放数值不放图,评审的第一反应就是“场景可能没生成好”。

4. 常见问题与排查技巧实录

4.1 协方差矩阵非正定:比想象中更常见

用Cholesky分解时遇到“矩阵不是正定矩阵”的报错,是这个流程里翻车率最高的一步。我起初以为是数据量不够,后来发现即使有三年8760点数据,96维场景的相关矩阵照样可能非正定,原因是时段间相关性太强,矩阵出现近似线性相关。

一个典型的例子:凌晨凌晨1点到4点的出力高度同步,相关系数在0.95以上,这会导致相关矩阵的特征值趋近于0,数值上表现为非正定。单纯加一个1e-6的对角扰动,有时候推不过去,这时有两个更稳的方案。

方案一是用特征值修正:对矩阵做特征分解,把所有负特征值改成很小的正数,再重构矩阵。这样处理后的矩阵保证正定,且对原矩阵的改动最小。方案二是改用平滑相关结构:直接用指数衰减函数拟合相关矩阵,比如r(t,s)=ρ的|t-s|次方,用少数几个参数代替完整矩阵,天然正定。

def fix_positive_definite(matrix, eps=1e-6): eigvals, eigvecs = np.linalg.eigh(matrix) eigvals = np.clip(eigvals, eps, None) return eigvecs @ np.diag(eigvals) @ eigvecs.T

实操建议:在计算相关矩阵前,先对每个时段的出力序列做一次平稳性检查。如果某个时段出力几乎为常数(比如光伏夜间出力恒为0),这个时段的方差接近0,会让相关矩阵的数值稳定性大幅下降。这种情况建议把“必然为0”的时段单独处理,不参与相关性建模。

4.2 削减后时序相关性被破坏:隐蔽但致命

砍完场景后自相关大幅下降,是第二个高频问题,而且比矩阵非正定更隐蔽,因为程序不会报错,只有做质量评估时才发现。前面提到K-means把0.89削到0.72,这就是一个典型的破坏案例。

破坏的根本原因在于K-means的欧氏距离本质上“逐点比较”:两个场景在24个时刻的值都接近,就被认为相似,即使它们的时序形态差异巨大。比如场景A是“先高后低”,场景B是“先低后高”,只要总体离得近,K-means就可能放到一个簇里,平均出来的质心变成“中间平移”,自相关自然被抹掉了。

解决思路有两种。第一种是在距离度量里加时序特征权重:把每个场景提取出一阶自相关系数、最大爬坡速率、方差等特征拼接在原始时序后面,再做加权聚类。第二种是用动态时间规整(DTW)距离替代欧氏距离,DTW允许时间轴轻微错位匹配,对风电这种存在“相位偏移”的曲线更友好。

经验值参考:用K-means时,把一阶自相关特征以0.3到0.5的权重拼进特征向量,能把削减前后自相关偏差控制在0.02以内。这个权重是我试出来的经验值,不保证所有数据都最优,但作为起点非常稳。

4.3 场景数量和削减方法怎么选

“生成500个还是2000个?削减到5个还是20个?”这是被问得最多的问题。我给出的建议是看下游优化模型能承受多大规模,同时看评估指标是否达标。

如果只是做日前调度,第二阶段变量规模大,场景10个以内是常态,5个偏少,15个偏多。如果做规划层面的随机规划(比如求扩容方案),场景可以到20到30个,因为规划问题对求解速度的容忍度高,对尾部风险的刻画要求也高。初始生成数一般500起步,2000封顶,超过2000对削减算法的计算压力陡增,但边际收益明显递减。

多试几次随机种子,比较场景削减结果的稳定性。一个合格的结果应该是:更换随机种子后,削减出的场景虽然不完全一样,但评估指标(均值、方差、分位数偏差)波动很小。如果某个种子下结果“看着特别漂亮”,换一个种子就崩了,说明削减过程有问题,可能是聚类陷入了局部最优,也可能是距离度量对某些场景形态过敏。

4.4 常见问题速查表

现象根本原因排查手段推荐处理
Cholesky报非正定相关矩阵近奇异/非正定np.linalg.eigvalsh查特征值对角jitter、特征值截断、改用平稳参数化相关
生成场景曲线锯齿明显独立采样忽略时序相关性算ACF对比换NORTA流程,用秩相关矩阵注入
K-means削减后ACF大跌欧氏距离不感知时序结构比较削减前后ACF特征加权、用DTW距离、改用快速前向选择
削减后均值偏差大极值场景被簇中心“平均”掉检查各时段均值Mae先标准化再做K-means,或改用快速前向选择
生成场景全挤在历史均值附近正态逆变换的尾部采样不足查看阴影图分位数覆盖增加场景数、采用拟随机数(Sobol序列)
不同随机种子结果差异大采样方差主导多次重复评估指标固定并测试多个种子,选择指标中位数场景集

最后分享一个我一直在用的“兜底技巧”:场景生成和削减做完,不要急着删除中间结果。500个原始场景、削减用的距离矩阵、每一步评估的中间打印,这些都保留成文件,将来任何一个环节被质疑,都能快速回溯。这行工作最怕的就是“黑盒输出一张漂亮的结果图”,却没留下任何可验证的过程数据。我自己因为这个习惯,省了至少三次推翻重来的时间,强烈推荐照做。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询