简介:面向能源系统分析与可再生能源预测方向的研究人员及工程师,该资源围绕风光出力与负荷场景的生成与预测,将拉丁超立方抽样与样本削减技术相结合,用于解决多维气象参数下场景样本代表性不足及计算负担过重问题。压缩包内共1个文件,为MATLAB脚本,文件大小仅1KB,便于快速查看核心实现逻辑。脚本以注释清晰的代码组织完整流程:从数据清洗、缺失值填补与标准化开始,利用拉丁超立方抽样构造多样化气象场景,再通过样本削减保留高代表性样本,最后基于机器学习模型输出风光出力与负荷预测结果并量化误差。该方法兼顾多因素复杂性与计算效率,可直接替换数据运行,也可调整抽样规模与削减比例以适配不同精度需求。已有2593人学习下载,适用于需要掌握场景分析方法的初学者,以及希望提升风电、光伏并网稳定性的研究或工程人员。
1. 拉丁超立方采样与场景削减:风光出力不确定性建模的入门第一课
做新能源并网规划或电力系统优化调度的人,迟早都会撞上同一个问题:风电、光伏、负荷的不确定性怎么进模型?直接把全年8760小时时序数据塞进优化模型,计算量会大到无法收敛,更别提还要跑随机规划的多场景迭代。这个标题给了一条从业者公认的可行路径——先用拉丁超立方采样生成一组能覆盖参数空间的初始场景,再用样本削减算法把成百上千个场景压缩到十几个或几十个,最后用削减后的场景集做出力与负荷的预测分析。它解决的是计算复杂度与精度之间的核心矛盾,适合正在做配电网规划、微电网容量配置、储能调度策略的同学直接复现。拉丁超立方采样相对蒙特卡洛最大的优势,是同样采样规模下样本覆盖更均匀,收敛速度更快;而场景削减的意义在于让模型只保留那些概率权重高、空间分布有代表性的场景,把计算代价降下来。
2. 风光出力与负荷的概率建模:三项分布假设与相关性怎么进采样器
2.1 风速Weibull、光照Beta、负荷正态:参数与取值逻辑
场景生成的第一步不是写代码,而是把三种不确定性要素的概率分布函数定下来。风速通常用两参数Weibull分布来描述,这是风资源评估领域应用最广的假设,形状参数k一般在1.8到2.3之间,尺度参数c跟平均风速正相关。风电功率P与风速v之间不是线性关系,切入风速、额定风速、切出风速三段区间要分别处理,但场景采样的入口仍然是风速分布,功率折算放在采样之后。
光伏出力主要受光照强度影响,光照强度一般用Beta分布建模,因为Beta分布定义在[0,1]区间上,形状参数alpha和beta可以根据历史光照数据的均值和方差反推。负荷不确定性相对温和,工程上通常用正态分布描述,均值取预测负荷值,标准差按历史预测误差的比例设置,比如取均值的3%到5%。三种分布确定之后,场景生成的任务就是从这些分布中抽取样本,每一条样本代表一种可能出现的风光出力与负荷组合。
这里有一个容易忽略的点:三种变量不是独立采样的。风电出力受气象系统影响,光伏出力受云层影响,两者在时间尺度上存在负相关性;负荷与气温、用电行为相关,与风光出力也有关联。如果各自独立采样,生成的场景会丢失这些相关性结构,后续削减和预测分析都会失真。解决路径是先采样独立随机数,再通过相关性处理把它们绑在一起,下一节展开说。
2.2 Nataf变换与Cholesky分解:把相关系数矩阵嵌入采样流程
处理相关性的标准做法是Nataf变换配合Cholesky分解。基本思路是:先在标准正态空间里生成一组具有目标相关系数矩阵的样本,然后利用排序法把相关性搬到原始分布空间的样本上。这样做的前提是能够给出变量间的Pearson相关系数矩阵R,这个矩阵可以从历史出力数据估算,也可以根据专家经验设定。
具体流程分三步。第一步,对相关系数矩阵R做Cholesky分解,得到下三角矩阵L,满足R等于L乘L的转置。第二步,生成独立标准正态样本矩阵Z,乘L转置得到带相关性的正态样本Z_corr。第三步,对每个变量,把Z_corr该列的排序顺序映射到LHS分层采样得到的样本上,完成相关性移植。这里要注意,Nataf变换在理论上要求把原始分布先转换到标准正态空间,严格做法是先做等概率变换再分解,但工程上多数实现直接对原始数据的秩相关系数做Cholesky,精度够用,代码简洁很多。
我一般会输出一个相关系数热图来验证采样结果是否复现了目标矩阵,这是整个流程里最短的验证闭环。如果发现相关性偏了,优先检查历史数据的相关系数矩阵是否正定,不正定时要做特征值修正,这是后面避坑章节会细说的高频翻车点。
3. 用Python实现拉丁超立方采样:分层、逆变换与排序三件套
3.1 LHS采样器的最小实现:分层采样与逆变换
拉丁超立方采样的核心思想是分层。把[0,1]区间分成N个等宽层,每个层只抽取一个点,这样无论真实分布如何,样本都能均匀覆盖整个参数空间。与之对比,蒙特卡洛采样是纯粹随机撒点,样本少时会漏掉尾部区间。LHS的最小实现代码如下:
import numpy as np def lhs_uniform(n_samples, n_vars, seed=42): """生成[0,1]区间上的拉丁超立方样本 参数: n_samples: 分层数,即最终样本数 n_vars: 变量个数,对应风电、光伏、负荷等 seed: 随机种子,保证结果可复现 返回: samples: (n_samples, n_vars) 的均匀分布样本 """ rng = np.random.default_rng(seed) samples = np.empty((n_samples, n_vars)) for j in range(n_vars): # 把 [0,1] 分成 n_samples 层 strata = np.linspace(0, 1, n_samples + 1) # 每个分层内均匀随机取一个点 u = rng.uniform(strata[:-1], strata[1:]) # 打乱顺序,使各变量的分层位置互不相关 rng.shuffle(u) samples[:, j] = u return samples这段代码的输出是[0,1]区间上的均匀样本,还不是Weibull、Beta或正态样本。要做概率分布转换,需要调用逆累积分布函数。SciPy的statistical functions可以直接完成这个映射:
from scipy.stats import weibull_min, beta, norm def lhs_from_dist(u_samples, dists): """将LHS均匀样本转换为指定分布样本 参数: u_samples: lhs_uniform 的输出 dists: 每个变量对应的scipy分布对象列表 返回: samples: 满足目标边缘分布的样本矩阵 """ n_vars = u_samples.shape[1] samples = np.empty_like(u_samples) for j in range(n_vars): # 逆变换采样:均匀分布 -> 目标分布 samples[:, j] = dists[j].ppf(u_samples[:, j]) return samples # 示例:风速Weibull(k=2.0, c=8.5)、光照Beta(2.1, 2.4)、负荷正态(mu=1000, sigma=40) dists = [ weibull_min(c=8.5, scale=8.5), # 形状参数k=2.0, 尺度参数c=8.5 beta(2.1, 2.4), norm(1000, 40) ] u = lhs_uniform(500, 3, seed=7) samples = lhs_from_dist(u, dists)逻辑说明分两层。第一层,LHS分层保证了采样点在累积概率轴上的均匀覆盖,这对Weibull这类右偏分布尤其重要,尾部极端风速能被采到;第二层,逆变换采样是连接均匀分布与任意目标分布的通用桥梁,ppf就是分布函数的反函数。参数方面,n_samples取500是常用起步值,兼顾初始场景集的丰富度和后续削减的计算量;n_vars即为变量数,不要与时间序列维度搞混。
3.2 相关性排序:排序不当会破坏分布形状
过逆变换得到的独立样本,各变量之间的相关性接近零,直接用会造成风光出力完全不相关的假象。修正方式是Cholesky排序法,代码实现如下:
from scipy.linalg import cholesky def corr_sort(samples, corr_matrix, seed=0): """用Cholesky分解引入变量间相关性 参数: samples: 已满足边缘分布的LHS样本 corr_matrix: 目标相关系数矩阵,形状 (n_vars, n_vars) 返回: samples_corr: 带相关性的样本 """ rng = np.random.default_rng(seed) n = samples.shape[0] n_vars = samples.shape[1] # 步骤1: 生成独立标准正态样本 z = rng.normal(size=(n, n_vars)) # 步骤2: Cholesky分解, L @ L.T = corr_matrix L = cholesky(corr_matrix, lower=True) z_corr = z @ L.T # 步骤3: 按相关正态样本的秩对LHS样本重排 sorted_samples = np.empty_like(samples) for j in range(n_vars): order = np.argsort(z_corr[:, j]) sorted_samples[:, j] = samples[order, j] return sorted_samples注意这里的坑:直接对目标分布的样本做线性变换,比如用samples乘以某个矩阵,会破坏已经设计好的边缘分布,导致风速出现负值或光照越界。排序法只改变样本的排列次序,不改变每个变量自身的取值集合,因此边缘分布保持不变,相关性却能逼近目标矩阵。排序后建议用numpy的corrcoef检查一下实际相关系数与目标矩阵的偏差,偏差大于0.05时优先怀疑相关系数矩阵不是正定矩阵,考虑做特征值修正或改用秩相关系数。
3.3 采样规模N怎么定:100、500、1000各有什么影响
采样规模没有统一答案,取决于下游任务。跑场景削减测试,100到200条初始场景就够用;做随机规划里的期望值评估,500条比较稳妥;要做概率性分析或评估极端场景对系统的影响,1000条更保险。初始场景越多,削减后的场景集越稳定,但LHS采样和削减算法的计算时间也在涨。
判断N够不够的一个分析技巧是看统计矩是否收敛。把样本数量从100逐步加到1000,每次计算样本均值和协方差矩阵的偏差,当增加采样量后统计量变化小于某个阈值(比如1%),说明样本量已经够用。不必一开始就选大N,先用小规模把流程跑通,再逐步加大。
4. 样本削减算法与实现:同步回代消除与聚类剪枝怎么选
4.1 同步回代消除:算法逻辑、坎托距离与概率转移规则
初始场景集动辄几百上千条,直接进优化模型会让求解时间指数级增长。样本削减的思路是删掉那些与邻近场景高度相似、概率权重占比低的场景,把删掉场景的概率转移到保留场景上,从而保持整体概率分布特性。同步回代消除是经典方法,核心度量是坎托距离,即两个场景向量之间的欧氏距离,用于衡量场景间的相似程度。
削减过程是迭代式的:每次从当前场景集合中找出与其他场景坎托距离最小的一对,在其中删掉一个,把被删场景的概率加到保留场景上,直到场景数降到目标值。这个过程的logical是优先删掉最“冗余”的场景,而概率转移保证了不会因为删除而损失概率质量。实现如下:
def scenario_reduction(scenarios, probs, k_target): """同步回代消除法削减场景 参数: scenarios: 初始场景数组, (n, d) 每行是一个场景 probs: 初始场景概率, 长度n的一维数组 k_target: 削减后保留的场景数量 返回: (red_scenes, red_probs): 削减后的场景与概率 """ n = len(probs) # 预计算所有场景之间的欧氏距离 dist = np.zeros((n, n)) for i in range(n): for j in range(n): dist[i, j] = np.linalg.norm(scenarios[i] - scenarios[j]) # 标记场景是否已被删除 active = np.ones(n, dtype=bool) while active.sum() > k_target: # 找出所有活动场景中距离最小的一对 best_pair = None min_val = np.inf indices = np.where(active)[0] for a in indices: for b in indices: if a < b: # 加权距离: 概率大的场景更不容易被删 weighted = probs[a] * dist[a, b] if weighted < min_val: min_val = weighted best_pair = (a, b) i, j = best_pair # 删除场景i, 概率转移到场景j probs[j] += probs[i] probs[i] = 0.0 active[i] = False keep = np.where(active)[0] return scenarios[keep], probs[keep]核心参数是k_target,即保留场景数量。它不是自由变量,而是精度与计算量的平衡点。工程经验是:分析网格规划类问题,k_target取20到50;运行调度类问题,可以取10到20;如果想保留更多极端场景,再结合下一节的肘部法则判断。这段代码里用得上的一个细节是加权距离中probs[a]做权重,概率越大的场景越不容易被删除,只是容易被删的场景通常包括低概率的边缘场景。
4.2 K-means聚类剪枝:更快的替代方案与适用边界
同步回代消除的问题是计算复杂度高,每次迭代都要扫描整个距离矩阵,场景数上千时耗时严重。 K-means聚类剪枝是一种更快的替代方案:把初始场景当作数据点聚类,保留每个聚类中心作为代表场景,聚类内所有场景的概率之和赋给该代表场景。sklearn的KMeans即可完成这个任务:
from sklearn.cluster import KMeans def kmeans_reduction(scenarios, probs, k_target, seed=42): """基于K-means聚类的场景削减 参数: scenarios: 初始场景 (n, d) probs: 各场景概率 (n,) k_target: 聚类中心个数 返回: (centroids, new_probs): 削减后的场景与概率 """ km = KMeans(n_clusters=k_target, n_init=10, random_state=seed) labels = km.fit_predict(scenarios) centroids = km.cluster_centers_ new_probs = np.zeros(k_target) for i in range(k_target): # 同一聚类的场景概率求和,赋给对应聚类中心 mask = (labels == i) new_probs[i] = probs[mask].sum() return centroids, new_probsK-means的优点是速度快,对上千级别的初始场景集也能在几秒内完成;缺点是聚类中心是几何中心,不是实际的历史场景切片,可能在物理上不是一个可实现的场景组合,比如风速和光照的组合在极端情况下会偏离现实约束。 SBR保留的是真实场景,K-means保留的是平均状态,对优化结果偏保守。
4.3 目标场景数K的选择:肘部法则与统计矩校验
K值拍脑袋是很多同学会踩的坑。我一般用肘部法则:对不同的K值执行一次削减,计算削减前后场景的统计矩偏差,比如均值和协方差的相对误差。当K从5增加到20时误差急速下降,K到30以后下降变缓,那么30附近就是肘部。代码里把K从10到60循环一遍,画出误差曲线即可。
另一种更直接的校验方式是蒙特卡洛对比:用削减后的场景集计算某个目标函数,比如系统年发电量期望,再与用全部初始场景计算的结果比较。偏差小于2%则说明K值满足精度要求。K太小会丢掉极端场景,K太大则优化模型求解时间飙升,肘部法则在两者之间找到一个相对合理的平衡点。
5. 场景削减避坑手册:相关性失真、尾部丢失与5个踩坑记录
5.1 坑1:LHS排序后风光相关性偏移,场景集出现“抱团消失”
现象:用Cholesky排序法引入相关性后,砍掉一半场景,风-光相关系数从-0.35直接跳到了-0.15,削减后的场景集在散点图上明显聚成一团,丢失了两个变量之间的负相关结构。
原因:排序法引入的相关性比较脆弱,削减时按欧氏距离删除场景,会把距离上邻近、但相关性模式雷同的场景成片删掉,导致相关结构变形。另外初始相关系数矩阵若来自历史数据且非正定,Cholesky分解得到L矩阵有偏差,相关性在源头就歪了。
解决:先对相关系数矩阵做特征值修正,把所有负特征值置一个极小正数后重建矩阵;削减之后立刻重算相关系数与目标矩阵的偏差,超过0.05就增加初始采样量,或改用k_target更大的削减配置。我现在的习惯是相关性验证放在削减前后各跑一次,两个数对比起来看。
5.2 坑2:砍到多少场景是拍脑袋定的,协方差崩了
现象:A同学把K直接设成10,削减后均值误差只有1.8%,看起来不错,但协方差矩阵的Frobenius范数相对误差达到了25%,优化结果在多个运行方式下出现出力缺额。
原因:均值是低阶统计量,对场景数量不敏感;协方差是高阶统计量,K太小时样本太少无法维持原始波动范围,方差和协方差自然失真。只看均值校验会给人虚假的安全感。
解决:把统计矩校验从均值和协方差扩展到偏度和峰度,至少检查方差阵的迹,即总方差是否接近。K的选取用肘部法则而不是经验值,而且要针对自己的场景集跑一遍。如果协方差误差始终压不下来,优先给初始场景集增加样本量,而不是继续调K。
5.3 坑3:削减前忘记归一化,风电和负荷量纲差异导致距离失真
现象:负荷数据是兆瓦级,数值上千;风电出力也是兆瓦级,但波动范围只有几十到几百。 K-means聚类出来的中心几乎只反映了负荷的变化,风电场景多样性被距离公式中的量纲效应压制了。
原因:欧氏距离对量纲敏感,数值大的变量在距离计算中占据绝对主导地位。 SBR算法同样如此,坎托距离里负荷分量淹没了风电和光伏的变化信息。
解决:削减之前,把每个变量按照历史分位数或最大最小值做标准化。标准化会改变场景的物理单位,因此要在削减完成之后反标准化回原始数量级。我用的标准化方式是取历史数据的95%分位数做缩放基准,这样能避免最大值异常波动带来的缩放失真。
5.4 坑4:只用削减后场景的期望值做预测分析,把概率信息丢了
现象:有人把削减后的20个场景再平均成一条“典型出力曲线”,然后拿这条曲线去做确定性分析,优化结果跟实际运行偏差很大,且无法解释为什么预测区间覆盖不住真实负荷。
原因:场景削减的目标是保留概率分布结构,不是帮你浓缩成一条曲线。把场景集压成均值,等于把概率信息全部丢弃,回到确定性建模的老路上。削减后的场景集应该保留多条曲线和各自的概率权重,进入优化模型时按场景加权计算目标函数。
解决:预测分析阶段用削减后的场景矩阵和概率向量,目标函数写概率加权的形式,至少计算5%、50%、95%分位数作为预测区间。概率信息是场景方法相对确定性方法的立身之本,这个信息不能用均值丢掉。
5.5 坑5:随机种子不固定,两次削减结果差异大到无法解释
现象:同一份历史数据,跑两次场景削减,得到的保留场景编号完全不同,优化结果也相差很大。某前辈排查了大半天,才发现是代码里没固定随机种子,LHS采样和K-means初始化每次都不一致。
原因:LHS虽然是分层采样,但每层内部的随机取值依赖随机数生成器;K-means聚类的初始中心也是随机选择的。随机种子不固定,最终场景集自然不稳定。场景削减是随机算法,可复现性靠seed保证。
解决:LHS、K-means、SBR三段代码全部显式传seed参数,实验记录里写明seed值。正式分析用的seed固定为某个值,需要评估结果稳定性时再改seed跑多次。
6. 用削减场景做预测分析:概率区间、统计校验与两个验证习惯
削减只是中间步骤,真正交付的是预测分析结果。用得最多的形式是给出一组带概率权重的场景,然后输出预测区间。以负荷预测为例,削减后的20条负荷曲线各自有概率,按概率加权排序后取5%和95%分位数,就能得到负荷预测的置信区间。这个区间比单点预测更有工程价值,调度员可以直接拿它判断备用容量够不够。用一个简短代码完成分位数计算:
def forecast_interval(red_scenes, red_probs, quantiles=(0.05, 0.5, 0.95)): """按概率权重计算预测分位数 参数: red_scenes: 削减后的场景, (k_target, 时序长度) red_probs: 场景概率, (k_target,) quantiles: 需要输出的分位数 返回: q_curves: 每个分位数对应一条时序曲线 """ k, t_len = red_scenes.shape q_curves = np.zeros((len(quantiles), t_len)) for t in range(t_len): # 按场景在该时刻的取值排序,概率做累积排序插值 order = np.argsort(red_scenes[:, t]) sorted_vals = red_scenes[order, t] sorted_probs = np.cumsum(red_probs[order]) for qi, q in enumerate(quantiles): # 找到概率累积超过q的索引 idx = np.searchsorted(sorted_probs, q) q_curves[qi, t] = sorted_vals[min(idx, k - 1)] return q_curves验证逻辑与代码实现一样重要。我个人的固定习惯是两条。第一,每次削减之后、进入任何下游优化之前,先跑一遍统计矩校验,算出削减前后均值偏差、协方差偏差和覆盖概率偏差,三项都达标才继续。第二,预测分析一定输出分位数曲线,至少三分位,而不是只给均值。这两条习惯帮我挡掉了不少后续模型跑偏的麻烦。
场景削减做多了之后,更深一层的体会是:削减质量不是越高越好,而是要与下游模型的计算精度相匹配。调度模型用20个场景结果已经不差,规划模型可能要50个场景才能覆盖极端出力组合。这个平衡点只能用实验数据说话,没有统一答案。希望这篇把拉丁超立方采样和样本削减的细节、常见踩坑都讲透,能帮你在自己的项目里少走几步弯路。
本文还有配套的精品资源,点击获取