先交代一个我自己的使用场景:做含高比例新能源的电力系统规划或调度时,最耗心力的往往不是优化模型本身,而是输入侧的“风、光出力场景”怎么准备。标题里的“风光”,在新能源领域通常默认指风电和光伏出力,不是风景照片;Copula和Kmeans两套方法放在一起,基本可以判断要处理的是随机场景集合。这类数据有很强的季节属性——春夏季风速偏低但日照变好,秋冬季风电可能更充沛但光伏出力下降,直接拿全年数据一锅炖,生成的典型场景很可能是“四季平均怪”,既不能指导规划,也不能用于调度校核。
解决路线并不复杂:先用Copula函数把风、光出力之间的相关结构提取出来,据此生成大量合理的随机场景,再用Kmeans把这些场景聚类削减成少数几个有代表性的典型场景,同时保留概率信息。整套流程我用Matlab跑通后,实测下来比单纯随机抽样或直接取历史典型日要稳得多。这篇文章会从为什么需要四季独立建模开始,讲清楚Copula和Kmeans各自在干什么,再给出可复现的Matlab代码骨架,最后把我踩过的几个坑一并列出来。适合正在做风光出力场景模拟、多场景优化调度或新能源容量规划的同学参考。
1. 先把问题掰开:四季场景里到底藏着哪些相关结构
1.1 风电和光伏不是两个独立的“随机数”
很多初学者做新能源出力场景的第一步,就是把历史风电出力和光伏出力分别拟合一个分布,然后各自独立抽样。这样做出来的场景集合,最大的问题是忽略了风电和光伏之间的相关性。
举个例子:我在处理某地夏季数据时,风电场午后风速通常有所抬升,而光伏出力恰好也在午后达到峰值,两者会出现一定正相关;在冬季,强冷空气过境往往带来大风和阴天,光伏出力反而偏低,这时候风、光又呈现负相关。如果独立抽样,就会生成大量“实际几乎不可能出现”的组合,比如大风且万里无云、光伏接近满发,或者静风且全天暴晒。把这些不合理的组合喂给优化模型,得到的调度策略会偏乐观或偏保守,后续结果自然不可信。
要处理“随机变量之间不是独立”的问题,常见工具是协方差矩阵或皮尔逊相关系数。但风光出力的边缘分布根本不是对称的正态分布,风电功率分布往往在低出力区集中,光伏出力则带有明显的日循环特征,这时候如果只用线性相关系数去描述相关关系,会丢掉很多尾部和分布两端的结构。Copula方法最大的好处,就是能把“每个变量自己的分布”和“变量之间的相关结构”拆开建模,相关性不局限于线性关系。
1.2 季节差异会让同一个Copula参数失真
“春夏秋冬四季”不是单纯为了把数据切四段,而是因为新能源出力的统计特性在季节间差异确实太大。
我对比过同一地区春夏秋冬四个季节的风速、辐照数据:夏季平均风速可能只有冬季的60%左右,但波动模式完全不同;光伏则在夏季出力高、日照时数长,冬季出力曲线更矮更短。如果只用一个Copula模型拟合全年数据,得到的相关矩阵其实是全年平均意义上的“伪相关”,它既不能代表夏季午后的特征,也不能代表冬季冷锋过境时的共变规律。因此更合理的做法是对每个季节分别做一轮“边缘分布估计+Copula参数估计+场景生成+Kmeans削减”。
除了风电和光伏自身分布的季节差异,风光互补性也会随季节变化。以我实际处理的数据为例,春季白天风、光容易出现同时偏大的情况,系统调峰压力更大;夏季夜间风小,光伏完全归零,晚间可靠出力偏低;冬季风电丰富但波动幅度大,光伏则受制于云量。单独建立四季模型后,这些结构才能体现出来。
1.3 场景削减究竟在保留什么
生成大量场景之后,常规做法是直接枚举所有场景去算优化问题,但这样计算量会爆炸。比如我需要生成2000个场景,如果每个场景都带入一个含机组组合的优化模型,可能跑一天都出不来。场景削减要做的事情不是“从2000个里随机挑10个”,而是尽量保留原始场景集合的概率分布特征,比如均值、方差、协方差和相关结构。
Kmeans在这里充当的角色是“聚类压缩器”。它把相似的场景归到同一类,用每类的聚类中心作为典型场景,再统计每类包含的样本数量作为该典型场景的概率。这样做之后,原来2000个场景只剩10个或20个,但概率总和仍然是1,场景均值、协方差不会出现离谱偏差。后面优化模型只需针对少量典型场景求解,再按概率加权即可。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Copula的建模链路:从历史观测到一大把模拟场景
2.1 Sklar定理怎么帮你把“边缘分布”和“相关结构”拆开
Copula背后的核心是Sklar定理,它把多维联合分布函数拆成两部分:每个变量的边缘分布 (F_1(x_1), F_2(x_2), ..., F_d(x_d)),和一个从 ([0,1]^d) 映射到 ([0,1]) 的Copula函数 C。写成公式就是:
[
F(x_1, x_2, ..., x_d) = C\big(F_1(x_1), F_2(x_2), ..., F_d(x_d)\big)
]
通俗地说,先把每个变量的观测值转换成它自己的累计概率 (u_i = F_i(x_i))。(u_i)服从0到1之间的均匀分布,它表示“这个值在所有样本中所处的位置”。然后,Copula函数C负责描述这些“位置变量”之间的相关结构。
为什么这个拆法特别适合风光场景?因为风电出力的边缘形状和光伏出力的边缘形状完全不同,但它们的相关结构可以在Copula中统一建模。比如两个变量都处于自身分布的90%分位数以上时,有没有“同时超发”的倾向,这种尾部相关性用皮尔逊相关系数很难刻画,但可以在Copula自由度或其他参数中体现出来。
2.2 Gaussian、t还是Clayton:风光场景怎么选更稳
Matlab的Statistics and Machine Learning Toolbox里,copulafit和copularnd直接支持Gaussian、t、Clayton、Frank、Gumbel等常用Copula,不需要自己手写复杂公式。选择哪一种,取决于你对数据尾部行为的要求。
我一般优先试t-Copula。t-Copula相比Gaussian Copula多了一个自由度参数ν,能刻画尾部相关,也就是说极端场景更容易一起出现。这对新能源场景很重要:大风极端天气和云量异常往往同时发生,可能带来风光出力同时处于边缘分布极端的场景。Gaussian Copula实现简单、估计稳定,适合小样本数据,但尾部相关为0,可能在极端场景生成中趋于保守。
Clayton和Gumbel这类阿基米德Copula更适合描述不对称的相关结构,比如下尾相关强或上尾相关强。对风光出力而言,这种不对称确实存在,但实际使用中需要先通过数据去判别,不能凭感觉选。我用过一个简单不严谨但很实用的判断方法:把风、光出力先各自转成累计概率,再在二维平面上画散点图,观察左下角、右下角或右上角的聚类情况。如果右上角明显有聚集,说明上尾相关强,可以考虑Gumbel;如果下尾相关强,可以考虑Clayton;如果四个角比较对称,就优先t-Copula或Gaussian Copula。
下面这个表是我常用的选型参考:
| Copula类型 | 适用特征 | 常见问题 | 在风光场景中的直观含义 |
|---|---|---|---|
| Gaussian | 相关关系对称、不强调极端共现 | 尾部相关偏弱 | 常规天气下风、光同高同低可被描述 |
| t | 对称相关且存在尾部相关 | 自由度估计不稳定 | 冷锋、台风等极端天气下更容易同时极端 |
| Clayton | 下尾相关强 | 对数据离散点敏感 | 无风同时无光的情况概率高 |
| Gumbel | 上尾相关强 | 负相关建模不方便 | 大风同时大晴天的场景概率高 |
2.3 从均匀随机数到出力模拟值的三步走
Copula生成场景的本质并不复杂,一共三个步骤。第一步,用历史风光出力数据估计各自的边缘分布;第二步,把历史数据带入边缘分布CDF,转成均匀变量u,再代入copulafit估计Copula参数;第三步,用copularnd从拟合好的Copula中抽出一组新的均匀变量,再通过边缘分布的逆CDF转回风光出力值。
这里面有个容易产生误解的细节:copularnd生成的仍然是[0,1]区间上的均匀分布样本,只是它们之间已经有了符合指定Copula的相关关系。只有经过逆变换,才能变成真正有物理意义的千瓦或兆瓦出力。如果样本量足够大,生成场景的均值、方差和相关性应当逼近原始历史样本;如果差距太大,要先回头检查边缘分布有没有拟合好,别急着抱怨Copula没用。
3. Kmeans场景削减怎么接住Copula生成的结果
3.1 聚类削减和“直接抽几个历史日”的本质区别
很多项目为了减少数据量,会直接挑选几个“典型历史日”作为场景,比如挑历史数据中出力最大的一天、最小的一天、平均的一天。这种做法在数据规律特别稳定时还能用,但如果面对的是多年风电光伏数据,直接抽日容易留下重复信息,还会丢掉概率分布中间占比最高的那些普通场景。
Kmeans聚类削减的思路完全不同:它把原始场景集合当作一堆点,通过距离把它们分成K簇,簇中心就是典型场景;簇内样本数量占比就是场景概率。这样得到的不是某一个“真实历史日”,而是一个代表同类场景平均形态的合成日。相比直接挑历史数据,聚类中心往往更平滑,不会把某一天的随机噪声当作特征保留下来。
实际操作时,我更推荐先用Copula生成一个足够大的候选场景集,比如每个季节生成2000个,再用Kmeans削减到10到20个。这样做的原因是,纯历史日的样本量往往有限,而且很多年份数据只有三年、五年,独立场景数量不够,聚类结果容易陷入局部样本的噪声。先由Copula生成能扩大样本覆盖范围,聚类时保留的是概率意义上的代表性场景,不是某个具体的历史日期。
3.2 距离特征:日曲线场景和二维出力状态要区别对待
Kmeans默认使用欧氏距离,但距离计算之前还有一个容易被忽略的点:数据是否归一化。
如果场景只有“日平均风电出力”和“白天平均光伏出力”两个特征,通常数值都在0到1之间,不需要过多归一化。但如果场景是一整条24小时曲线,前24列是风电逐时出力,后24列是光伏逐时出力,那么夜间光伏全是0,这部分数据会占据大量维度。此时如果不对特征做处理,聚类距离会被光伏夜间的零值以及风电大出力时段的数值主导,白天和夜晚的变化特征反而被冲淡。
解决方式有三种。第一种是先把每个特征和每个季节分别做z-score标准化,再进行聚类,聚类结束后把中心点反标准化回实际出力水平;第二种是给风电和光伏分别做容量基准归一化,然后拼成特征矩阵;第三种是把光伏夜间时段剔除,只保留太阳高度角大于一定阈值的时段作为特征。我实际项目中一般用第一种,最省心,逻辑也好解释。
3.3 选择聚类数K并评估削减质量,别只用轮廓系数
Kmeans要求先指定K,这个K不是越大越好,也不是越小越好。场景削减的目的是用尽量少的典型场景逼近原始分布,因此K的选取要在“计算效率”和“信息保留”之间做平衡。K太小,削减后可能出现概率权重很大但实际场景与真实分布差异明显的情况;K太大,优化模型又会被场景数量拖慢。
经验上,做规划层面研究时春夏秋冬各保留10个到20个典型场景比较常见;做日内调度校核时可能适当多一点。评估方面,我最常用的是两类指标。一类是聚类本身的指标,比如轮廓系数、戴维斯-布尔丁指数、簇内误差平方和SSE,它们可以帮助判断同一K下簇是不是分得干净;另一类是统计学指标,比如削减前后场景集合的均值向量、协方差矩阵、各时段分位数是否接近,这个更贴合场景削减的初衷。
不要只看轮廓系数,因为它衡量的是“簇内紧密、簇间分离”的程度,和场景削减要保留的概率分布不一定直接相关。我见过轮廓系数很高的聚类结果,削减后典型场景的方差大幅缩水,原因是簇内越紧密,簇中心之间的差异可能被过度平均。最终还是要回到“削减后的典型场景加权均值、加权协方差和原始场景是否接近”这个核心目标上。
4. Matlab代码骨架:按季节循环生成并削减场景
4.1 前处理:把历史数据切出春、夏、秋、冬四份
在Matlab里做这件事之前,我建议先把数据整理成统一格式。最常见的做法是准备一个矩阵或者表格,每一行代表一天的观测,包含月份、日平均风电归一化出力、白天平均光伏归一化出力;如果你有逐小时数据,也可以把每个时段扩充为独立维度,后面的代码逻辑不会本质改变。
下面我以“每个季节各生成一张日特征场景集”为例给出代码。为了节省篇幅,这里假设数据已经整理成三列:第1列是月份,第2列是风电日平均出力,第3列是光伏白天平均出力。
matlab复制data = readmatrix('daily_wind_solar.csv');
monthVec = data(:,1);
windDaily = data(:,2);
solarDaily = data(:,3);
% 春、夏、秋冬的月份划分可以按项目当地气候特点调整
springIdx = ismember(monthVec, [3 4 5]);
summerIdx = ismember(monthVec, [6 7 8]);
autumnIdx = ismember(monthVec, [9 10 11]);
winterIdx = ismember(monthVec, [12 1 2]);
seasonData = cell(4,1);
seasonData{1} = [windDaily(springIdx), solarDaily(springIdx)];
seasonData{2} = [windDaily(summerIdx), solarDaily(summerIdx)];
seasonData{3} = [windDaily(autumnIdx), solarDaily(autumnIdx)];
seasonData{4} = [windDaily(winterIdx), solarDaily(winterIdx)];
注意一个细节:冬季如果按自然月划分,12月、1月、2月会跨年,如果你的数据是多年连续记录,务必把跨年也处理到同一个冬季集合里。上面的ismember(monthVec, [12 1 2])会把所有年份的12月、1月、2月集中在一起,逻辑上没问题。如果还需要考虑气象意义的“冬前冬后”,可以自行调整月份区间。
4.2 Copula参数估计与场景采样(Matlab代码实现核心)
下面这个函数是我在项目中反复使用的核心模块,输入是某个季节的观测矩阵,输出是指定数量的Copula生成场景。为了演示清晰,我用二维风、光场景作为例子;维度更多时,只要观测矩阵的列数扩大,再对每一列分别估计边缘分布即可。
matlab复制function scenes = genWindSolarScenes(obs, nScenes, useT copula)
% obs: N x 2矩阵,第1列风电,第2列光伏
% nScenes: 需要生成的场景数量
% useTCopula: 1表示t,0表示Gaussian
% 1. 估计边缘分布,使用核分布的好处是能适应非正态形状
pdWind = fitdist(obs(:,1), 'Kernel', 'Kernel', 'normal');
pdSolar = fitdist(obs(:,2), 'Kernel', 'Kernel', 'normal');
% 2. 把历史观测转成均匀概率
u(:,1) = cdf(pdWind, obs(:,1));
u(:,2) = cdf(pdSolar, obs(:,2));
% 3. 避免CDF出现0或1的极端边界值
u(u <= 0) = 1e-6;
u(u >= 1) = 1 - 1e-6;
% 4. 估计Copula参数
if useTCopula
[rho, nu] = copulafit('t', u, 'Method', 'ApproximateML');
else
[rho] = copulafit('Gaussian', u);
nu = 0; % Gaussian Copula没有自由度参数
end
% 5. 生成新的场景
if useTCopula
uSim = copularnd('t', rho, nu, nScenes);
else
uSim = copularnd('Gaussian', rho, nScenes);
end
% 6. 逆变换回实际出力值
scenes(:,1) = icdf(pdWind, uSim(:,1));
scenes(:,2) = icdf(pdSolar, uSim(:,2));
end
这段代码有几个地方值得细说。核分布fitdist(x,'Kernel','Kernel','normal')是利用高斯核函数对数据做平滑估计,比直接用正态分布灵活得多,能适应风电在低出力区堆积、光伏接近零出力等非正态形态。但在数据样本量很小或原始数据有大量重复离散值时,核分布依然会有偏差,这点后面第5章会展开。
使用copulafit('t', u, 'Method', 'ApproximateML')时,Matlab会在所有自由度ν可能值中搜索一个使似然函数最大的结果。如果历史天数只有几十天,这个估计往往不稳,我会先锁定使用Gaussian Copula,等数据积累变多以后再切回t-Copula。代码里保留useTCopula参数,就是想方便做不同方案比较。
4.3 Kmeans削减和概率再分配
有了一大批Copula生成场景后,下一步就是用Kmeans削减。下面的函数接受场景矩阵和一个K值,输出削减后的典型场景以及对应概率。
matlab复制function [typicalScen, prob] = kmeansReduceScenes(scenes, K)
% scenes: 生成场景矩阵,行是场景数,列是特征维数
% 1. z-score标准化
mu = mean(scenes);
sigma = std(scenes);
scenesNorm = (scenes - mu) ./ sigma;
% 2. Kmeans聚类,Replicates要大于1,避免局部最优
[idx, centroidNorm] = kmeans(scenesNorm, K, ...
'Replicates', 20, 'Distance', 'sqeuclidean', 'MaxIter', 500);
% 3. 簇中心反标准化回原始场景量纲
typicalScen = centroidNorm .* sigma + mu;
% 4. 概率按簇内样本频数计算
prob = accumarray(idx, 1) / length(idx);
end
使用标准化时注意一个细节:如果某一列数据在某个季节几乎没有波动,标准差接近0,标准化的时候会产生无穷大值。风光归一化出力通常不会是常数,但如果你处理的是某一特定电站的检修月份,可能出现大量长时间零出力,要提前把这类异常数据剔掉,或者给标准差加一个小下限。
调用这些函数生成四季场景的循环可以写成:
matlab复制rng(2025);
nScenes = 2000;
K = 10;
typicalScen = cell(4,1);
prob = cell(4,1);
rawScenes = cell(4,1);
for s = 1:4
rawScenes{s} = genWindSolarScenes(seasonData{s}, nScenes, 1);
[typicalScen{s}, prob{s}] = kmeansReduceScenes(rawScenes{s}, K);
fprintf('季节%d完成:原始场景2000个,削减到%d个\n', s, K);
end
这里把随机种子固定成rng(2025),是为了让结果可复现。实际项目里如果只是初步探索,可以不固定种子,多跑几次看收敛稳定性;如果是正式报告或论文,固定种子会更方便别人复核结果。
4.4 把四季典型场景与原始场景放在一起看
削减完不能直接拿结果去用,至少要做一轮可视化检查和数值校验。可视化最简单的方法是把每个季节的原始观测、生成场景、削减后典型场景画在同一张散点图或曲线图上。
matlab复制figure;
seasonColors = lines(4);
seasonNames = {'Spring', 'Summer', 'Autumn', 'Winter'};
for s = 1:4
subplot(2,2,s);
% 绘制所有生成场景
plot(rawScenes{s}(:,1), rawScenes{s}(:,2), '.', 'Color', [seasonColors(s,:), 0.2]);
hold on;
% 绘制削减后的典型场景,按概率大小标记大小
scatter(typicalScen{s}(:,1), typicalScen{s}(:,2), 100 * prob{s} + 1, 'filled');
xlabel('Wind output (p.u.)');
ylabel('Solar output (p.u.)');
title(seasonNames{s});
grid on;
end
这样做的价值在于:你可以直观确认每个季节的场景点云是否覆盖原始观测所在的区域,削减后的典型场景是不是散布在点云概率密度高的地方。如果生成的场景出现大量历史数据里从未出现的负值或超过装机容量的值,说明边缘分布或逆CDF环节出了问题,要马上去修正预处理。
数值校验我会看两样东西。第一,削减后典型场景按概率加权的均值向量和原始场景均值向量差距是否小于某一个容忍阈值,比如5%;第二,原始场景集与削减后场景集的Pearson相关系数有没有发生明显变化。不过要注意,Kmeans是在标准化的欧氏空间里做聚类,最终的典型场景如果用于时序模拟,还需要按具体维度还原成完整的日出力曲线。
5. 我在调这三个模块时反复踩过的坑
5.1 光伏夜间零值会让Copula的连续假设当场失效
这是我在Copula建模里遇到的最大的坑。光伏出力在夜间或阴雨天会有大量0值,Matlab里如果直接把包含这些0值的光伏出力放进正态分布或核分布去拟合,CDF曲线在0附近会出现一个很陡的台阶。Copula的连续模型默认边缘分布是连续的,遇到这种离散点会导致cdf结果大量落在同一数值上,进而让相关性估计失真。
一个比较稳妥的处理方式,是把变量改成“连续且非零”的对象。比如我后来做光伏场景时,并没有直接用光伏出力作为变量,而是使用太阳高度角大于0时段的“光伏出力/晴空理论出力”的比值,也就是晴空指数。晴空指数在晴空时接近1,阴雨天明显下降,数值相对连续,再用Copula建模就合理很多。风电侧如果切入风速导致大量零出力,也要做类似处理,比如先区分“开机待风”和“正常发电”状态,而不是把零值和连续出力混在一起硬拟合。
5.2 t-Copula自由度估计在小样本下容易“抽风”
copulafit('t', u)返回的自由度参数ν有一个特点:数据量足够大、线性相关结构很强时,ν会稳定在一个较小的值,表示明显的厚尾;但如果样本量不足,ν估计可能冲到几百甚至几千,这时候t-Copula会退化成接近Gaussian Copula,或者反过来,ν估计得极小,导致生成大量极端样本。这个“抽风”现象不是Matlab代码的问题,而是统计估计在大自由度空间里天然不稳定。
对小样本场景,我给两个实用建议。第一,可以固定ν,不估计,比如用copulafit('t', u, 'Nu', 5)这样的方式固定自由度,先看场景生成效果。第二,如果数据量确实不够,直接用Gaussian Copula,同时注意它尾部相关为零,生成极端场景会偏保守。实际工程中不需要追求模型越复杂越好,能稳定描述当前样本相关结构的Copula才是最好的。
5.3 削减后的场景概率不是“均分”,要按簇内样本频数算
这是个看起来不起眼但影响很大的细节。很多人做完Kmeans聚类后,习惯性忽略概率,直接把10个簇中心各当成1/10概率去加总,结果削减前后总期望出力完全对不上。
正确做法一定是统计每个簇中实际包含的样本数量,再除以总样本数。用Matlab的accumarray(idx, 1)甚至直接histcounts(idx, 1:K+1)都能实现。如果后续要在优化模型中使用这些典型场景,建议把概率归一化检查一遍,确保所有场景概率加起来等于1,同时不允许出现概率为0的簇;若某个簇的样本数太少,可以考虑降低K值重新聚类。
5.4 四季出力模型的衔接问题
每个季节独立建模,严格来说没有障碍,但如果你希望把四季场景用于跨季节的连续时序模拟,就不得不考虑衔接问题。比如冬季最后几天和春季前几天之间,风光出力并不会在12月31日到1月1日之间突然跳变;如果独立训练四个模型,最后拼接出来的时序曲线可能在季节交界处出现不自然断点。
一种解决思路是用月份滑动窗口替代固定季节切分,比如把每个季节的边界月份重复纳入相邻季节建模,再在拼接处做加权过渡。另一种更简单的做法是,如果只做规划层面的容量评估和典型日提取,就可以忽略季节间连续衔接问题,因为典型日场景本身就不需要严格保持时间连续性。我自己做项目时,如果目标是生产模拟,会更偏向逐月分段或滑动窗口;如果目标是容量可信度和可靠性评估,就会很自然地用季度独立模型。
最后再分享一个调试技巧:写完整套代码后,先不要急着跑Kmeans,先把Copula生成出来的场景和原始历史数据一起画个散点图。如果这一关过了,说明边缘分布和相关结构都拟合对了,后面的Kmeans只是压缩问题;如果这一关的输出已经乱套,就回头检查数据预处理和边缘分布,否则在削减阶段再调参也没有意义。
