多场景随机生成与缩减:风光负荷不确定性建模的Matlab实现
2026/9/10 18:36:45 网站建设 项目流程

1. 这个项目到底在解决什么问题

1.1 电力系统研究里的"不确定性"从哪来

做电力系统规划、微电网调度或者配电网分析的工程师和研究生,大概率都碰到过这个场景:风电出力时大时小,光伏白天晴雨不定,负荷曲线跟着季节和作息波动,偏偏这些不确定性又直接影响调度策略的可行性、备用容量的设置、储能配置的合理性。你要是只用一条"典型日曲线"去做优化,结果往往偏乐观,实际运行一兑现就出问题。

那怎么把不确定性考虑进去?业界主流思路之一就是"多场景法":把风光和负荷可能出现的各种情况,用大量随机生成的场景表示出来,再通过缩减算法挑出少量有代表性的典型场景,用这几十个场景去逼近原始上千个场景的统计特性。这样一来,随机优化问题就转化成了确定性场景集合上的优化问题,计算复杂度可控,模型也足够贴近真实情况。

这也是"风光及负荷多场景随机生成与缩减Matlab代码"这类项目如此普遍的根源。它本质上是一套"不确定性建模的预处理工具箱",不管你后续做两阶段鲁棒优化、随机规划,还是做基于场景的调度策略评估,这套代码都能直接复用。

1.2 多场景方法的核心思想:把连续问题变成离散问题

我用一个简单的类比来解释多场景法的价值。想象你要评估一个城市未来一年的天气对出行的影响,理论上天气有无穷多种组合。但实际决策时你不需要这一年每一天的天气,你只需要知道"晴天、雨天、台风天、高温天"各有几成概率,每种天气下大家的出行方式如何变化。把无穷种天气聚类成几种典型天气,就是场景缩减的基本思想。

放到电力系统里,风电、光伏、负荷的出力都是连续随机变量,理论上它们的联合分布是无穷维的。多场景方法分两步走:

第一,根据风、光、负荷的历史统计规律,用蒙特卡洛采样生成大量随机场景(比如1000个),每个场景包含一组"某时刻风电出力+光伏出力+负荷大小"的时序数据;

第二,用场景缩减算法(常见的有同步回代缩减、K-means聚类等)把1000个场景精简成10~20个典型场景,同时给每个典型场景算出一个概率权重。缩减后的场景集合在概率分布意义上尽量接近原始场景集合,但规模大幅缩小,可以直接嵌入优化模型。

这套流程的价值在于:它把"不确定性"从抽象的统计分布,变成了优化模型里实实在在可以写进约束的一组组系数和权重。这正是随机规划、机会约束规划、鲁棒优化等高级建模方法的地基。

2. 场景生成与缩减的数学原理

2.1 风光负荷的概率分布模型怎么选

在做场景生成之前,第一步是确定每个随机变量的概率分布。这块没有统一标准,但工程实践里有一些默认的"老规矩"。

风电出力,通常先用两参数Weibull分布描述风速,再做风机功率曲线转换。风速概率密度函数为:

f(v) = (k/c) * (v/c)^(k-1) * exp(-(v/c)^k)

其中k是形状参数,c是尺度参数。但直接用出力数据做分布拟合时,很多人也会直接用Beta分布拟合风电出力(因为出力范围是0到额定功率之间,Beta分布定义在[0,1]区间,天然匹配)。

光伏出力,主流做法是用Beta分布拟合光照强度或出力水平,同样是因为光伏出力归一化后在0到1之间,Beta分布的两个形状参数alpha和beta可以灵活拟合不同偏态。也有不少论文直接用正态分布近似,精度略低但操作简单。

负荷呢,一般假设服从正态分布或对数正态分布,均值和方差从历史数据中统计得到。负荷的特性是相关性比较强——同一区域内的不同负荷节点往往同涨同跌,所以做多节点负荷场景时还要考虑相关性。

![注意] 分布假设并不是越复杂越好。我见过不少论文把分布建模写得很花哨,但实际用下来,Beta分布拟合光伏、Weibull或Beta拟合风电、正态拟合负荷,加上相关性修正,对绝大多数规划调度问题已经够用。花哨模型带来的收益微乎其微,反而让参数估计和代码调试变得复杂。

2.2 蒙特卡洛采样与相关性处理

确定了边缘分布之后,场景生成的核心就是采样。最简单的做法是直接对每个随机变量独立采样,然后组合成场景。但这样会产生一个问题:风、光、负荷之间往往存在相关性,比如同一区域的风电和光伏可能负相关(白天风小、晚上风大),不同节点的负荷强正相关。忽略这些相关性,生成出来的场景虽然单个变量分布正确,但联合分布完全失真,后续优化结果也就失去意义。

处理相关性的标准做法是Cholesky分解配合Nataf变换。大致步骤是:

  1. 根据历史数据计算各随机变量之间的相关系数矩阵R;
  2. 对R做Cholesky分解,得到下三角矩阵L,满足R = L * L';
  3. 生成一组独立的标准正态随机向量Z;
  4. 做线性变换得到相关标准正态向量Y = L * Z;
  5. 用等概率变换,把相关标准正态分布映射回各自的目标分布。

这个流程在Matlab里实现很顺手,核心只是几个矩阵运算。等概率变换的原理是:标准正态分布的累计分布函数值服从[0,1]均匀分布,再通过目标分布的逆累计分布函数,就能把相关性保留下来。

需要注意:Cholesky分解要求相关系数矩阵是正定的。实际操作中,如果矩阵里存在完全线性相关的变量,或者采样数量太少导致经验相关矩阵不正定,分解会直接报错。我的建议是,在生成前对相关矩阵做一次特征值修正,把所有负特征值往零方向做极小偏移。

2.3 场景缩减:同步回代法的核心逻辑

缩减这一步是整个流程的灵魂。做缩减的方法很多,工程里最常见的是两种:基于概率距离的同步回代缩减K-means聚类缩减

同步回代法的思路非常直观:原始有N个场景,每个场景有自身的概率权重(等概率时就是1/N),现在要删掉一部分场景,把被删场景的概率累加到保留场景上,使得删减前后场景集合的"概率分布距离"最小。

具体步骤是迭代进行的,每一轮删除一个场景:

  1. 对当前场景集合中的每一对场景(i, j),计算它们之间的距离d(i,j),常用的是 Kantorovich 距离,本质上就是在某范数(如2范数)下求两个场景时序数据的距离;
  2. 找到距离最近的那一对,删掉其中一个;
  3. 把被删场景的概率累加到与之距离最近的那个保留场景上;
  4. 重复上述过程,直到场景数达到预设的目标值。

这个算法实现起来不复杂,但直接暴力循环在初始场景数很大时会很慢。1000个场景缩减到10个,理论上最多迭代990轮,每轮都要遍历当前所有场景对,复杂度近似O(N^3),Matlab纯循环跑起来可能要几十秒甚至几分钟。不过考虑到这是离线预处理,一次性计算,时间通常可以接受。

K-means聚类的思路则是把N个场景看成N个样本点,每个样本点是T维向量(T是时间段数),然后直接聚成K类,每个类的质心就是典型场景,类内样本占比就是场景概率。K-means实现简单、速度快,但缺点也很明显:K值要事先指定;聚类结果依赖初始中心的选择,每次运行可能不同;而且K-means用欧氏距离聚类,本质上假设每个时段独立同权,对峰谷差异明显的数据会偏向"平均化"。

我在后面的代码实现里,两种方法都会给出来,默认用同步回代法,因为它在论文里认可度更高、结果更稳定。

3. Matlab代码实现与实操流程

3.1 整体架构与关键函数设计

整个代码我建议拆成四个模块,每个模块一个脚本或函数,方便独立调试和复用:

  1. 数据输入模块:读入风光负荷的历史数据,或者直接设置分布参数(对于没有实测数据的研究场景,用分布参数合成数据是常见的做法);
  2. 场景生成模块:实现蒙特卡洛采样、相关性处理,输出初始场景矩阵;
  3. 场景缩减模块:实现同步回代缩减或K-means缩减,输出典型场景和对应概率;
  4. 评估与可视化模块:对比缩减前后场景的均值、方差、累积分布,画出场景集和典型场景曲线。

模块化的好处是:后面你换一套数据,只需要改数据输入模块;想换缩减算法,只替换场景缩减模块,其他部分完全不用动。

3.2 核心代码实现(含参数计算过程)

我给出一个可以直接运行的完整示例,场景设定为:两个风电场、一个光伏电站、一个负荷节点,一共4个随机变量,24小时时段,初始生成1000个场景,缩减到10个。

%% 参数设置 num_scenarios = 1000; % 初始场景数 num_typ_scenarios = 10; % 缩减后的典型场景数 num_vars = 4; % 随机变量数:风电2个 + 光伏1个 + 负荷1个 num_periods = 24; % 时间维度:24小时 %% 分布参数设置(以Beta分布为例) % 风电1:Beta分布参数 a1, b1(每个时段一组,这里为简洁只给全天均值示例) % 实际使用时,应按24个时段分别拟合参数 beta_params_wind1 = [2.0, 3.5]; % 形状参数 beta_params_wind2 = [1.8, 4.0]; beta_params_pv = [3.0, 5.0]; % 光伏出力,夜间时段应近似为0 % 负荷:正态分布 mu_load = 100; % 均值,单位MW sigma_load = 10; % 标准差,单位MW %% 相关系数矩阵(4x4) R = [1.0, 0.3, -0.2, 0.1; 0.3, 1.0, -0.1, 0.2; -0.2, -0.1, 1.0, 0.0; 0.1, 0.2, 0.0, 1.0]; %% 场景生成 % 步骤1:Cholesky分解 % 先做一次特征值修正,确保正定 [V, D] = eig(R); d = diag(D); d(d < 1e-6) = 1e-6; R_fixed = V * diag(d) * V'; L = chol(R_fixed, 'lower'); % 步骤2:生成独立标准正态随机数 Z = randn(num_vars, num_periods, num_scenarios); % 步骤3:施加相关性(每个时段独立处理) Y = zeros(size(Z)); for t = 1:num_periods Y(:, t, :) = L * squeeze(Z(:, t, :)); end % 步骤4:等概率变换到目标分布 % 标准正态CDF -> 均匀分布 U = normcdf(Y); % 均匀分布 -> Beta分布(风电和光伏) scenarios_wind1 = betainv(U(1, :, :), beta_params_wind1(1), beta_params_wind1(2)); scenarios_wind2 = betainv(U(2, :, :), beta_params_wind2(1), beta_params_wind2(2)); scenarios_pv = betainv(U(3, :, :), beta_params_pv(1), beta_params_pv(2)); % 均匀分布 -> 正态分布(负荷) scenarios_load = norminv(U(4, :, :), mu_load, sigma_load); % 组装成场景矩阵:每行一个场景,每列对应 (变量, 时段) % 存储为 struct 更清晰 for k = 1:num_scenarios scen(k).wind1 = squeeze(scenarios_wind1(1, :, k))'; scen(k).wind2 = squeeze(scenarios_wind2(1, :, k))'; scen(k).pv = squeeze(scenarios_pv(1, :, k))'; scen(k).load = squeeze(scenarios_load(1, :, k))'; scen(k).prob = 1 / num_scenarios; end

上面这段代码的关键点在Cholesky分解和等概率变换。我在调试时最容易出错的地方是维度搞混,这里统一逻辑:每个场景是一个24×4的矩阵,即"时段×变量",但Matlab里用三维数组存储时,(变量, 时段, 场景)这种排布更利于矩阵运算。后面读取某个场景时要小心别把维度弄反。

3.3 同步回代缩减的Matlab实现

%% 同步回代缩减 % 输入:scen结构体数组,目标场景数 K % 输出:缩减后的场景结构体数组,每个场景带概率 function scen_reduced = sbr_reduction(scen, K) N = length(scen); probs = ones(N, 1) / N; % 记录场景编号对应的数据矩阵,便于距离计算 data = zeros(N, 24 * 4); % 每个场景拉平成一行 for i = 1:N data(i, :) = [scen(i).wind1, scen(i).wind2, scen(i).pv, scen(i).load]; end active = true(N, 1); % 标记场景是否存活 num_active = N; while num_active > K % 计算当前存活场景两两之间的距离 idx_active = find(active); m = length(idx_active); % 距离矩阵 dist_mat = zeros(m, m); for i = 1:m for j = i+1:m d = norm(data(idx_active(i), :) - data(idx_active(j), :)); dist_mat(i, j) = d; dist_mat(j, i) = d; end end % 对每个场景i,找到距离它最近的场景j,以及对应的距离和概率乘权值 min_dist = inf(m, 1); min_dist_idx = zeros(m, 1); for i = 1:m row = dist_mat(i, :); row(i) = inf; [val, j] = min(row); min_dist(i) = val; min_dist_idx(i) = j; end % 加权距离:概率 * 距离 weighted = probs(idx_active) .* min_dist; % 删除加权距离最小的那个场景 [~, del_idx] = min(weighted); del_global = idx_active(del_idx); % 找到距离该场景最近且存活的邻居 neighbor_idx = idx_active(min_dist_idx(del_idx)); % 概率转移:被删场景的概率累加到邻居 probs(neighbor_idx) = probs(neighbor_idx) + probs(del_global); % 标记删除 active(del_global) = false; num_active = num_active - 1; end % 输出 idx_keep = find(active); scen_reduced = scen(idx_keep); for i = 1:length(idx_keep) scen_reduced(i).prob = probs(idx_keep(i)); end end

这段代码是同步回代最直白的实现版本,每一步都对应前面讲的原理。实际运行时,如果初始场景数特别大(比如5000个),这个双重循环会很吃力。我的经验是:初始场景在1000以内时问题不大;超过2000,建议先把数据降维一下再算距离(例如用主成分分析把24×4维降到10~20维),能显著提速。

缩减完成之后,正规做法是做一个评估,看看缩减前后的均值曲线、标准差曲线、累积分布曲线是否吻合。我习惯画两张图:

%% 可视化对比 figure; % 原始场景的均值曲线 mean_orig = mean(data_orig, 1); plot(1:96, mean_orig, 'LineWidth', 1.5); hold on; % 缩减后场景的加权均值 mean_red = sum(data_red .* prob_red, 1); plot(1:96, mean_red, 'LineWidth', 1.5); legend('缩减前均值', '缩减后均值');

这里的96列是24时段×4个变量,画图时用垂直虚线分隔每个变量会更直观。缩减结果若均值偏差在3%以内,工程上完全可以接受。

4. 调试经验与参数调优建议

4.1 常见问题速查表

我在把这套代码用在不同的项目场景时,踩过不少坑,整理成表格分享出来,按出现频率排序:

问题现象根本原因解决方案
Cholesky分解报错"Matrix must be positive definite"相关系数矩阵非正定,变量间相关性设置不合理对相关矩阵做特征值修正,或检查是否出现完全线性相关的变量组合
缩减后典型场景全部挤在一起,多样性差初始场景数太少,或初始场景本身分布过于集中增大初始场景数(建议至少500),检查分布参数是否合理
场景均值和原始数据均值偏差过大等概率变换时,Beta逆函数对边界值0/1处理不当使用betainv时注意返回值可能为0或1,加入微小裁剪区间,比如限制在[0.001, 0.999]
同步回代运行速度极慢距离矩阵反复计算,复杂度高减少初始场景数;对数据降维;或改用K-means方法
负荷出现负值正态分布采样时尾部产生负值,电力系统负荷不可能为负对负荷采样做截断处理:生成后把负值置为0或取绝对值;更推荐用对数正态分布
光伏夜间时段出力不为零Beta分布采样没有区分昼夜,夜间时段仍然采到非零出力事先将夜间时段的出力直接置零,只在白天时段做随机采样
缩减概率之和不为1场景概率累加过程出现数值误差缩减结束后统一归一化:probs = probs / sum(probs)

4.2 参数设置的实践经验与坑

分布参数怎么估计是整个流程里最容易被低估的环节。很多人直接从论文里抄一套参数就用,但不同地区、不同季节的风光出力特性差别很大。我建议如果手头有历史数据,哪怕只有一年的小时级数据,也值得按"分时段拟合"来做——每个时段单独拟合Beta分布或Weibull分布的参数,这样能保留日内波动特性。比如光伏在上午10点和下午2点的Beta分布参数差异很大,直接用全天统一参数会抹平这种差别。

相关系数矩阵怎么定。如果数据充分,直接拿历史出力数据计算Pearson相关系数即可。如果数据不足,凭经验设置也行,但要注意:相关系数的绝对值不宜超过0.8,否则Cholesky分解后生成的场景会表现出过强的跟随性,看起来不自然。比如风电出力相关系数设到0.9,两个风电场场景几乎同步波动,调度模型会严重低估系统灵活性需求。

缩减到多少场景合适,这也是高频问题。我的经验是:初步研究用5~10个场景够用;论文级的结果建议分别做10、20、30个场景的对比实验,看均值和分位数的变化趋势,选取边际收益开始递减的那个数目。缩减场景数太少,典型场景丢失极端情况,优化结果偏乐观;太多则场景规模带来的计算负担反而得不偿失。

关于K-means与同步回代的选择,我的建议是:追求结果稳定、准备写论文,用同步回代法;做快速预实验、只是看看大致场景形态,用K-means。K-means在Matlab里可以直接调kmeans函数,代码量少一个数量级,缺点是要多跑几次选最优聚类结果(因为初始中心随机)。另外,K-means聚类前最好对数据做标准化,否则负荷量级远大于风电光伏时,聚类结果基本只由负荷变量主导,风电光伏的差异被忽略。

%% K-Means 缩减的极简实现 % data_orig: N行,每行一个场景拉平后的向量 % K: 聚类数 [idx, C] = kmeans(data_orig, K, 'Replicates', 10); % 重复10次取最优 prob_red = histcounts(idx, 1:K+1)' / length(idx); % C就是典型场景矩阵,每行对应一个典型场景

4.3 相关性不匹配时的排查思路

还有一个值得单独说的场景:生成的数据单变量分布是对的,但变量之间相关性显著偏离了你设定的相关系数矩阵。这个问题我在早期调试时经常遇到,后来排查发现是采样量不足导致的统计噪声——1000个场景下发样本相关矩阵就能大致收敛;如果只生成100个,估计值会波动得比较厉害。另一个隐蔽原因是等概率变换环节的离散化截断,比如Beta分布采样后把输出裁剪到[0.001, 0.999],会轻微改变线性相关性,但通常影响不大。

如果对相关性精度要求极高,可以采用更严格的方法:采样完成后计算实际相关矩阵,然后做迭代修正(调整初始Cholesky分解用的相关矩阵,使输出相关矩阵逼近目标值)。不过说句实在话,对绝大多数电力系统规划调度问题,初始相关的轻微偏差(±0.05以内)对后端优化结果几乎没影响,没必要上迭代修正这种复杂方案。

5. 代码的扩展方向与真实项目中的注意事项

5.1 从单时段独立采样到时序相关性

前面展示的代码是每个时段独立采样,没有考虑时间维度上的自相关性——也就是上一小时风电出力高,下一小时大概率也不会太低。这种"逐时段独立采样"的做法会在场景里产生锯齿状的出力曲线,看起来很不真实,也直接影响调度优化里爬坡约束的合理性。

改进思路有两条路线。一条是用马尔可夫链模型,把风速或出力的状态离散成若干区间,通过历史数据统计状态转移概率矩阵,再按转移概率逐时段生成场景。另一条是用多元正态分布直接生成整个时间序列,时间相关性体现在协方差矩阵里——但协方差矩阵规模是96×96(24小时×4变量),需要大量历史数据才能可靠估计。我在实际项目中常用的是第一种:先按马尔可夫链生成风速序列,再经过功率曲线转换得到风电出力序列。这样做物理意义清晰,而且能自然保证时序连续性。

5.2 场景缩减之后怎么用

缩减完成得到典型场景和概率后,下一步通常是嵌入优化模型。不管你是用Yalmip还是直接用Matlab的linprog/intlinprog,典型场景的用法都是一样的:把每个场景的出力曲线作为确定性参数代入约束条件,目标函数用场景概率加权求和。举个例子,随机经济调度问题的最简形式是:

目标:min sum_k prob_k * (火电成本 + 弃风惩罚 + 失负荷惩罚) 约束:每个场景k下都要满足功率平衡、机组出力上下限、爬坡约束等

这里特别注意:功率平衡约束必须在每个场景下都成立,而不是只在期望场景下成立。很多初学者在这里犯错——只用了期望值做调度,得到的最优解在大多数场景下根本不可行。场景缩减的价值就在于此:它对每个典型场景都单独建模,约束条件更贴近真实运行情况。

5.3 真实项目中一定要避开的三个坑

第一个坑是数据口径不统一。风电、光伏、负荷的历史数据可能来自不同数据库,时间分辨率不一致(有的15分钟、有的1小时),量纲不一致(有的MW、有的kW),采集时间有偏差。做场景生成前必须统一插值、统一量纲、对齐时间戳,否则后面的相关性分析全是错的。这一步工作琐碎耗时,但做了十年数据处理的工程师都会告诉你:数据清洗占整个项目60%的时间,但决定80%的结果质量

第二个坑是场景缩减目标数设得太少。有人为了省计算时间一上来就缩到3个场景,结果极端场景(大风无光、无风高温)被合并掉,调度方案在极端情况下面临切负荷风险。我的建议是至少要保证缩减后场景覆盖原始数据的"最小值情景"和"最大值情景"——也就是说,缩减前先把样本中风电出力最小的一天、光伏出力最大的那天找出来单独保留,再跑缩减算法,防止极端情况被平均掉。

第三个坑是忘记做缩减效果的定量评估。很多人在论文里只放一张"缩减前后场景对比图",看起来很直观但说服力不够。规范的评估指标至少有三个:缩减前后各时段均值的最大偏差、标准差的最大偏差、累积分布函数的Kolmogorov-Smirnov统计量。把这些数值列出来,审稿人和答辩老师都会觉得你的工作做得很扎实。

%% 缩减效果评估:KS检验 % 对每个时段、每个变量做两样本KS检验 % 原假设:缩减前后场景集合来自同一分布 h_ks = zeros(24, 4); p_ks = zeros(24, 4); for t = 1:24 for v = 1:4 [h_ks(t, v), p_ks(t, v)] = kstest2(data_orig(:, (v-1)*24+t), ... expand_scen_reduced(:, (v-1)*24+t)); end end % h=1表示拒绝原假设,说明该时段缩减前后分布差异显著

6. 我个人的实操体会

这套多场景随机生成与缩减代码,我在不同项目里迭代过四五个版本。最初版只是简单地对每个变量独立采样、用K-means缩减,那时候做完的结果被合作方一句"场景怎么都在平均曲线附近"就给打回来了。后来重构成现在这套"相关性处理+等概率变换+同步回代缩减"的架构,效果才稳定下来。中间最大的体会是:场景生成不是目的,分布保真才是目的——不要追求生成过程的数学华丽,要反复确认缩减后的场景集合在统计特性上经得起对比。

另外一个小技巧:调试的时候不要把初始场景数直接设1000,先用100个把整个流程跑通,检查生成的曲线形态是否符合物理直觉——风电曲线有无跳变、光伏是否只在白天有出力、负荷是否出现负值。确认代码逻辑无误后,再把场景数调大做正式实验。这样能省下大量等待循环的时间。

最后再分享一个我在展示结果时的做法:把10个典型场景画在同一张图上,不标颜色顺序,而是按概率从大到小排列,用线条粗细区分概率大小。这样一眼就能看出系统最可能落在什么运行状态,比摆一摞曲线图有说服力得多。这套代码本身改造成本很低,不管是加储能、加多节点负荷还是改成季度场景分析,你都能直接在现有框架上扩展。

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

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

立即咨询