简介:一份面向 Matlab 用户的完整代码包,用于基于 Copula 理论的风光联合场景生成与 K-means 聚类分析,适合电子信息、计算机、数学等专业学生在课程设计、期末大作业和毕业设计中直接使用。包内共 15 个文件,包括 1 个主程序 main.m、2 份风功率与光伏 Excel 数据、3 份 Copula 及微网优化配置相关 PDF 文档、8 张结果示意图和 1 份许可说明,整体压缩包仅 4.77MB,结构清晰便于按需查看。代码采用参数化编程,可灵活调整参数以适配不同仿真需求,并兼容 Matlab 2014/2019a/2024a。通过直接运行附赠案例数据,即可复现场景生成与聚类流程,同时借助配套文档理解 Copula 函数的选取和 K-means 的运用思路。目前已有 122 人学习下载,适合希望掌握多变量依赖建模和场景聚类方法的读者作为实操参考。
1. 为什么是 Copula + Kmeans:风光联合场景生成的第一性问题
做电力系统随机规划的人,大概率都遇到过这个场景:手里有风电和光伏的历史出力数据,想生成一批未来可能出现的“典型日”,用来跑机组组合或储能配置。如果直接把风电和光伏各自采样再拼接,后面做优化时十有八九会翻车,因为这两个场的出力并不是独立事件。Copula 函数负责把两个随机变量的相关结构单独建模,Kmeans 聚类负责把成千上万个采样场景削减成十几个具有代表性的典型场景。这套 Matlab 代码正是把 Copula 风光联合场景生成和 Kmeans 聚类削减串成一条完整链路,适合正在做微电网调度、电力系统随机规划或者风光容量配置的从业者。不需要你从头写 Copula 参数估计,也不需要手撕 Kmeans 的迭代推导,拿到手改改数据路径和场景数就能跑。
2. Copula 建模:从历史出力到相关性采样
2.1 为什么风光出力不能拆开单独建模
风电和光伏出力的“负相关”是个老话题,但很多人处理时仍然偷懒:把风电历史数据拟合一个分布,光伏历史数据拟合另一个分布,然后分别采样,最后拼在一起当成联合场景。这种做法的前提是两者独立,可现实中同一片区域的风光资源往往由同一个天气过程驱动,白天光伏出力上升时风速可能下降,夜间风速增强时光伏又归零。这种复杂关系不是皮尔逊相关系数能完全描述的,尤其是台风、寒潮这类极端天气下,风光出力可能同时跌到很低,也可能风电满发而光伏为零。
Copula 的好处是把联合分布拆成边缘分布和相关结构两个独立部分。边缘分布可以任意选择,相关结构由 Copula 函数单独刻画。换句话说,可以先分别把风电、光伏的出力分布拟合好,再用 Copula 把“它们之间是怎么联动的”这个信息补上。相比直接假设联合正态分布,Copula 允许两个变量的尾部行为不一致,比如风电大发的极端工况和光伏小发的极端工况可以有不同的同步概率。这在做可靠性分析时非常关键,因为系统最怕的往往就是极端事件同时发生。
2.2 数据预处理:把出力数据变成 [0,1] 均匀分布
Copula 定义在单位区间上,输入必须是每个边缘变量的概率积分变换结果。常见做法是用秩变换,把原始出力换算成均匀分数。我不建议直接用正态拟合后取 CDF,因为风电和光伏出力都有明显的偏态和大量零值,正态假设会直接毁掉尾部。
data = readmatrix('wind_solar.csv'); % 第一列风电,第二列光伏 wind = data(:,1); solar = data(:,2); % 经验CDF变换:tiedrank 返回秩,除以 n+1 避免0和1边界 u_wind = tiedrank(wind) / (length(wind) + 1); u_solar = tiedrank(solar) / (length(solar) + 1); U = [u_wind, u_solar];用tiedrank而不是ecdf的原因很简单:ecdf在样本点处返回的 CDF 值会包含 0 和 1,而copulafit在边界上的数值计算容易出 NaN。除以n+1是秩统计里通用的“缩尾”处理,保证所有值严格落在 (0,1) 开区间内。如果你有相同出力平局的样本,tiedrank会自动取平均秩,不会破坏单调性。这一步是后面所有计算的基石,数据没有清洗好,Copula 参数估计就是白做。
2.3 Copula 参数估计与类型选择
Matlab 的统计工具箱直接提供了copulafit,可以估计 Gaussian Copula 和 t Copula。Gaussian Copula 是最常用的基准,t Copula 多了一个自由度参数,能刻画尾部相关性。对于风光这种存在极端同步事件的场景,t Copula 通常比 Gaussian 更稳。
% 拟合 Gaussian Copula rho_g = copulafit('Gaussian', U); % 拟合 t Copula,自由度 nu 越小,尾部相关性越强 rho_t = copulafit('t', U);rho_g和rho_t都是 2×2 的相关矩阵,对角元素是 1,非对角元素就是风电和光伏之间的秩相关参数。t Copula 的自由度nu是第二个输出,如果数据里存在比较强的极端同步事件,nu通常会在 5 到 20 之间;如果数据表现得接近 Gaussian,nu会很大,比如 30 以上,这时你直接退回 Gaussian Copula 反而更省事。
除了这两种,阿基米德 Copula 也值得尝试。Gumbel 适合上尾相关,Clayton 适合下尾相关,Frank 适合对称的相关结构。风光出力之间往往是负相关,Frank Copula 的对称负相关结构比较贴合;如果你重点关心“风电小、光伏也小”这种双低情况,Clayton 更合适。怎么选?我的习惯是分别拟合后比较对数似然值,哪个高用哪个。copulafit的第三个输出就是对数似然,如果你的 Matlab 版本支持,可以在命令行里直接看。
2.4 从 Copula 采样生成大量联合场景
参数估计完,下一步就是生成海量联合场景。这里有个细节:Copula 采样得到的只是 [0,1] 上的均匀随机数,必须通过每个边缘分布的反函数还原成物理出力值。
N = 3000; % 生成场景数量 sim_u = copularnd('t', rho_t, nu_t, N); % 情况A:直接用历史数据分位数还原 sim_wind = quantile(wind, sim_u(:,1)); sim_solar = quantile(solar, sim_u(:,2)); % 情况B:用核密度分布还原,允许出现历史范围之外的值 % pd_wind = fitdist(wind, 'Kernel'); % sim_wind = icdf(pd_wind, sim_u(:,1)); % pd_solar = fitdist(solar, 'Kernel'); % sim_solar = icdf(pd_solar, sim_u(:,2)); sim_data = [sim_wind, sim_solar];用quantile的好处是简单、不会超出物理范围,但代价是生成场景被限制在历史观测区间内,等于假设未来不会出现更极端的出力。用fitdist加Kernel选项会平滑经验分布,理论上可以外推,但尾部可能出现负值或者超过装机容量,需要在后面钳制。两种方式没有绝对的对错,取决于你要做的是“保守的场景生成”还是“允许极端外推的可靠性分析”。我一般先用quantile跑通流程,确认 Copula 结构没问题后再切换到核密度版本做敏感性分析。
3. Kmeans 聚类削减:把 3000 个场景浓缩成 10 个典型日
3.1 场景削减到底解决什么问题
3000 个场景直接丢进优化模型,如果每个场景对应一组运行约束,那决策变量的规模会直接让求解器卡死。场景削减的本质是用少量有代表性的场景,去近似原始场景集合的统计特征。Kmeans 聚类的思路很直观:把 3000 个场景点分成 K 个簇,每个簇的中心作为典型场景,簇内样本占比作为该典型场景的发生概率。这样削减后的场景数量从 3000 降到 K,优化模型求解时间大幅下降。
另一个问题是场景概率。Kmeans 聚类后,每个簇的样本数除以总样本数就是典型场景的概率。这个概率不用人为设定,完全由历史数据的密度决定。概率信息在随机规划里很重要,因为目标函数是期望值,每个场景的概率权重直接决定了优化结果。
3.2 Kmeans 为什么适合场景削减
Kmeans 的目标函数是最小化簇内平方距离和,也就是让每个场景到它所属簇中心的距离尽可能小。这个目标天然和“用典型场景代表原始场景”的需求一致:簇中心越靠近聚类成员,还原误差越小。相比后向削减法(逐个删除对概率分布影响最小的场景),Kmeans 的复杂度低得多,几千个场景聚类起来也就是几秒钟的事。
但 Kmeans 也有先天弱点:它假设簇是凸的,对不规则形状的簇可能效果不好;另外它对初始化非常敏感。好在 Matlab 的kmeans函数支持多次重复初始化,用'Replicates'参数就能缓解。如果场景数据有很强的时间相关性,比如 24 小时出力曲线之间是平滑的,Kmeans 仍然能用,只是距离度量要选对。欧氏距离适合形状差异明显的场景,相关性距离适合趋势相似但幅值不同的场景。
3.3 Matlab 中的 Kmeans 代码实现
假设已经生成了sim_data,每个行向量是一个场景(比如 2 列表示两个时刻,或者 48 列表示一天 24 小时的风电加光伏)。下面的代码把 3000 个场景聚成 10 类:
K = 10; [idx, C, sumd] = kmeans(sim_data, K, ... 'Distance', 'sqeuclidean', ... 'Replicates', 20, ... 'Options', statset('Display', 'final', 'MaxIter', 1000)); % 计算每个典型场景的概率 prob = accumarray(idx, 1) / length(idx);idx是每个场景所属簇的编号,C是 K×2 的簇中心矩阵,也就是典型场景。sumd是一个 K 维向量,每个元素是该簇内所有点到中心的距离平方和,可以用来评估聚类好坏。Distance参数选sqeuclidean等价于最小化方差,是最标准的 Kmeans;如果你发现场景之间的幅值差异远大于形状差异,可以改成correlation。Replicates设为 20,意味着从 20 组不同初始点出发,最终返回目标函数最小的一组,这个参数不能省,否则很容易陷入局部最优。
3.4 如何把聚类中心还原成典型日曲线
如果sim_data的每一行是完整的一天序列,比如第一列到第 48 列是风电 15 分钟间隔出力,第 49 列到第 96 列是光伏 15 分钟间隔出力,那么C的每一行就是一条典型日曲线。把C和prob存成 .mat 文件,后续优化模型直接加载就行。
save('typical_scenarios.mat', 'C', 'prob');存完以后,记得检查sum(prob)是否等于 1。因为accumarray统计的是整数计数,概率和应该严格为 1,如果有浮点误差,在优化模型里可以先做一次归一化。另外,聚类中心不一定是原始观测值,它可能是几个场景的加权平均。这在物理上意味着典型日曲线可能比任何单天的实际出力都平滑,极端尖峰被抹掉了。如果下游模型需要保留通信约束或爬坡约束,这种平滑反而会低估系统风险,需要提前知道。
3.5 聚类数 K 怎么选
K 值不是越大越好。K 太小,典型日覆盖不了极端场景;K 太大,又回到了“没削减”的老路。工程上常用的办法是肘部法则:不断增大 K,绘制总簇内距离平方和随 K 变化的曲线,寻找拐点。
maxK = 30; total_loss = zeros(maxK, 1); for K = 1:maxK [~, ~, sumd] = kmeans(sim_data, K, 'Replicates', 5); total_loss(K) = sum(sumd); end plot(1:maxK, total_loss, 'o-'); xlabel('K'); ylabel('total in-cluster sum of distances');你会看到曲线在某个 K 值之后下降速度明显变缓,那个点就是合理的聚类数。电力系统场景削减里,K 通常在 5 到 20 之间。如果后续优化模型对求解时间特别敏感,取 10 以下;如果需要保留极端场景,可能得取 20 以上。肘部法则只提供参考,最终 K 值还要结合优化模型的规模一起决定。
4. Matlab 代码复现:目录、主脚本与参数对照
4.1 拿到资源后先看什么
你拿到的 .rar 解压后,通常不是单个文件而是完整的小工程。我拆过的类似资源基本都长这样:
main.m:主脚本,从数据读取、Copula 拟合、场景生成到 Kmeans 削减,全部串在一起copula_fit.m:函数封装的 Copula 参数估计和采样过程kmeans_reduce.m:函数封装的场景削减和概率计算data/:历史风光出力数据,可能是 .csv 或 .matfigures/:生成场景的散点图、典型日曲线图输出
拿到压缩包第一步不是双击main.m,而是先看有没有README或者注释头。很多资源作者会把参数含义和依赖工具箱写进注释。如果有缺失文件,优先检查是不是路径没加进 Matlab。常见做法是右键文件夹添加到路径,而不是 cd 进去就完事。
4.2 主脚本运行流程
main.m的骨架大概是这样的,他做的事情就是上面各个步骤的顺序拼接:
clc; clear; close all; rng(2024); % 固定随机种子,保证结果可复现 % 读取数据 data = load('data/wind_solar_data.mat'); wind = data.wind; solar = data.solar; % 1. 概率积分变换 u_wind = tiedrank(wind) / (length(wind) + 1); u_solar = tiedrank(solar) / (length(solar) + 1); U = [u_wind, u_solar]; % 2. Copula 拟合 rho_t = copulafit('t', U); % 3. 生成联合场景 N = 3000; sim_u = copularnd('t', rho_t, 10, N); % 自由度10是示例,实际用拟合出的nu sim_data = [quantile(wind, sim_u(:,1)), quantile(solar, sim_u(:,2))]; % 4. Kmeans 削减 K = 10; [idx, C, ~] = kmeans(sim_data, K, 'Replicates', 20); prob = accumarray(idx, 1) / length(idx); % 5. 保存典型场景 save('typical_scenarios.mat', 'C', 'prob');注意第 3 步里的自由度我是临时写的 10,只是展示代码逻辑。实际跑的时候一定要用第二步拟合出来的nu值,这是个最常见的低级错误。主脚本每段之间最好加上disp输出,比如disp('Copula fitting done.'),方便定位卡死在哪一步。
4.3 参数对照表
| 参数 | 位置 | 推荐值 | 影响 |
|---|---|---|---|
| N | 场景采样数 | 2000~5000 | 越大统计精度越高,但 Kmeans 时间线性增加 |
| K | 聚类数 | 5~20 | 过小丢极端场景,过大求解变慢 |
| Replicates | Kmeans 重复次数 | 10~30 | 越大越不容易陷入局部最优 |
| rng 种子 | 脚本开头 | 任意固定整数 | 固定后场景可复现 |
| Copula 类型 | copulafit 第一个参数 | 't' 或 'Gaussian' | 影响尾部相关性刻画 |
| 时间分辨率 | 数据列数 | 24 点/日 或 96 点/日 | 聚类维度越高,信息越细但也越容易维度灾难 |
4.4 常见报错与预处理
在实际跑代码时,报错集中在几种情况。第一种是缺失工具箱,copulafit、kmeans、copularnd都属于 Statistics and Machine Learning Toolbox,没有装这个工具箱会在调用时报“未定义函数”。第二种是数据里有 NaN,Copula 拟合遇到缺失值会直接输出 NaN,要先用data = rmmissing(data)清洗。第三种是维度不匹配,风电和光伏数据的长度不一致,tiedrank时就会报错。
还有一种情况是quantile在输入非常接近 0 或 1 的时候返回历史最小值或最大值,导致生成场景里出现大量重复边界值。这不是 bug,而是分位数函数的固有行为。如果你发现重复值太多,说明采样点可能取到了极端尾部,建议检查一下 Copula 自由度是否太小,或者数据本身零值比例太高,属于正常现象。
5. 避坑指南:Copula 拟合和 Kmeans 聚类的五个常见坑
5.1 坑:直接把原始出力塞给 copulafit,结果全是 NaN
现象:copulafit运行后返回的rho是 NaN,或者在拟合过程中报警告。
原因:Copula 的输入必须是 [0,1] 区间上的均匀分布随机变量,直接拿原始出力数据进去,数值尺度不对,算法的迭代过程会发散。
解决:先做概率积分变换。用tiedrank(data) / (length(data) + 1)得到每个变量的边际均匀分布。注意不是tiedrank(data) / length(data),因为除以n会产生精确的1.0,而 copulafit 对边界值非常敏感。这个小细节我踩过不止一次。
5.2 坑:边缘分布用正态拟合,生成场景出现负出力
现象:生成的风电或光伏场景里有负值,明显违反物理常识。
原因:用了fitdist(wind, 'Normal')做边缘分布,正态分布的左尾会延伸到负数区域,尤其当出力均值小、方差大的时候,负值比例还不低。
解决:改用fitdist(wind, 'Kernel')或者直接用quantile(wind, u)。如果非要用参数分布,可以尝试'Weibull'或带有非负支撑的分布。生成场景后,建议加一行sim_wind = max(0, sim_wind);做物理约束,虽然这会让边缘分布略微偏差,但能保住优化模型的可行性。
5.3 坑:Kmeans 聚类时没有标准化,光伏场景被风电吃掉
现象:削减出来的典型日里,光伏出力曲线全部是平庸的平均形状,细节完全丢失。
原因:风电出力数值范围可能达到 0~2000MW,光伏只有 0~200MW,欧氏距离计算时风电贡献的误差远大于光伏,聚类中心自然会被风电主导。
解决:聚类前对sim_data按列做归一化,或统一除以各自装机容量。如果装的不是实际容量而是历史最大值,也可以用zscore标准化。标准化后再聚类,得到的C是标准化空间里的中心,需要先还原到物理空间再保存。
sim_norm = zscore(sim_data); [~, C_norm, ~] = kmeans(sim_norm, K, 'Replicates', 20); C = C_norm .* std(sim_data) + mean(sim_data);5.4 坑:K 值选太大,削减等于没削
现象:K 取到 30,优化模型求解时间翻了好几倍,结果和 K=20 几乎一样。
原因:K 值没有结合下游优化模型的复杂度和场景本身的多样性去选。场景样本本身可能只需要 8 个簇就能解释大部分方差,多加 20 个簇只是在细分噪声。
解决:用肘部法则先扫一个 K 的范围,观察总簇内平方和的拐点。同时计算轮廓系数,如果某个 K 值下轮廓系数明显高,优先选它。工程上风光联合场景生成一般推荐 K 在 5~20,超过 20 先怀疑自己 K 选得是不是过度了。
sil = silhouette(sim_data, idx); fprintf('K=%d, mean silhouette=%.3f\n', K, mean(sil));5.5 坑:场景生成结果每次都不一样,论文图不能复现
现象:同一套代码,隔一天跑出来的典型日曲线完全对不上,写论文时图都没法用。
原因:copularnd和kmeans内部都依赖随机数生成器。copularnd每次抽样不一样,kmeans的初始簇中心也是随机的,不固定种子就必然复现不了。
解决:在脚本开头强制rng(2024);。Kmeans 的Replicates虽然能降低初始化随机性,但不能完全消除,只有固定种子才能保证结果一致。我自己跑做对比实验时会对每个 K 值分别固定不同的种子,比如rng(K*100),这样不同 K 之间的比较不会受到同一种子偶然性的影响。
6. 场景质量验证:用轮廓系数和统计检验给削减结果打包票
很多人跑完 Kmeans 削减,只看聚类中心曲线顺不顺眼。我现在的习惯是先看轮廓系数,再看削减前后的统计量有没有跑偏。轮廓系数怎么算?sil = silhouette(sim_data, idx); mean(sil)会在 [0,1] 之间,越接近 1 说明簇内紧密、簇间分离。一般 0.6 以上能用,低于 0.4 就要重新调 K 或换距离度量。
统计检验这里不用小样本的 KS 检验,因为削减后的典型场景只有 10 个点,样本量太小检验功效不够。更实际的是比较削减前后的一阶矩和二阶矩,也就是均值与标准差。相对的误差控制在 5% 以内,基本可以认为典型场景继承了原始场景的统计特征。我在完成 Kmeans 后都会跑一段验证代码,把结果贴到脚本末尾:
orig_mean = mean(sim_data, 1)'; red_mean = C' * prob; mean_rel_err = abs(red_mean - orig_mean) ./ orig_mean; orig_std = std(sim_data, 1)'; red_std = sqrt(sum((C - red_mean').^2 .* prob, 1))'; std_rel_err = abs(red_std - orig_std) ./ orig_std; disp('Mean relative error:'); disp(mean_rel_err); disp('Std relative error:'); disp(std_rel_err);这段代码先计算原始场景集合的风电和光伏均值,再用典型场景的加权均值做对比。标准差那一行稍微绕一点,先算每个簇中心相对加权均值的偏差平方,再按概率加权求和,开方后就是削减场景的标准差。如果这两个指标在可接受范围内,说明典型场景不仅形状像原始场景,统计特性也没被 Kmeans 的平滑效应带偏。
有一次我没做这个验证,直接把 K=7 的场景丢进储能容量优化,结果算出的配置比 K=20 时少了 15%。后来排查发现是聚类中心把几个弱风弱光场景平滑掉了,概率还分配得偏高。从那以后,我每次跑场景削减都会强制走一遍轮廓系数和统计量对比,两个指标都过了再往下做优化。希望帮到你。
本文还有配套的精品资源,点击获取