资讯动态

基于Frank-Copula的风光出力场景生成:Matlab实战与踩坑解析

发布时间:2026/9/17 12:13:02 来源:尧图企业网站定制
风光出力场景生成这件事我最早接触时踩了不少坑。那时候做配电网随机规划需要同时给风电场和光伏电站生成出力序列第一反应是分别拟合分布、独立抽样。结果算出来的方案要么过于乐观要么保守到没法用直到我把风和光的出力散点图放在一起看才意识到问题不在单个分布拟合得好不好而在两个变量之间那种说不清道不明的“联动”被漏掉了。后来我改用Copula方法用二元Frank-Copula来捕捉风光出力的不确定性和相关性生成场景的效果才真正对了路。这篇博客把我实际跑通的整套流程、Matlab代码和遇到的问题完整写出来给正在做新能源出力建模、随机优化或者储能容量配置的同学一个可以直接上手的参考。1. 风光出力为什么非要“牵手”建模从独立抽样到Copula1.1 单变量拟合都做对了联合场景仍然失真风电出力和光伏出力单独看都是典型的间歇性随机变量。风电贴着Weibull分布走光伏在晴空模型附近波动很多人拿到历史数据后的第一件事就是分别拟合这两条曲线拟合优度甚至能做到相当漂亮。但接下来的抽样阶段就会出岔子如果两个边缘分布独立抽样然后随机配对等于默认了任意时刻“风大”和“光强”是互不相干的。真实情况显然不是这样。同一片区域里大气过程同时驱动风速和云量大风过境时往往云层增厚光伏出力被压低而晴朗静稳天气下光伏出力高风速又往往起不来。独立抽样会把大量实际很少出现的“大风加晴天”组合当成正常场景送进优化模型最后算出的容量配置、调度策略自然偏离现实。1.2 相关性的三个层次为什么不能只盯线性相关系数很多人一提到相关性下意识就是算Pearson线性相关系数。但对于风光出力这种强非线性、非椭圆分布的数据Pearson相关系数有两个先天不足它只捕捉线性关联而且对边缘分布的形式极其敏感。你先把数据做了某种非线性变换Pearson值就可能大变可变量之间的内在关联未必真的变了。处理这类问题更合适的指标是秩相关系数包括Kendall秩相关系数和Spearman秩相关系数。它们只关心两个变量排序是否一致对单调变换保持不变。在Copula框架里相关结构本质上就是秩相关结构跟边缘分布形态无关。这个性质非常关键它意味着你可以放心地把“单变量分布形状”和“变量间关联模式”拆开处理。1.3 Sklar定理把联合分布拆成两个独立零件Copula方法能成立靠的是Sklar定理任意二维联合分布都可以写成H(x, y) C(F(x), G(y))其中F和G是两个边缘分布函数C就是Copula函数它完全描述了两个随机变量之间的依赖结构。反过来说如果你给定了边缘分布和一个合适的Copula就可以构造出任意的联合分布。这个“解耦”思路在工程上的价值是决定性的。以前建风电光伏联合出力模型要么假设联合正态根本不符合实际要么硬套一个复杂的参数联合分布参数多到没法估计。现在只需要分别拟合好两个一维边缘分布再单独估计Copula的参数问题瞬间降维。这也是为什么近些年新能源出力场景生成、电力系统随机优化里Copula几乎成了标配工具。2. 为什么偏偏选Frank-Copula数学内核与参数物理意义2.1 Frank-Copula的表达式和生成元Frank-Copula属于阿基米德Copula族它的函数形式是C(u,v) -1/θ · ln[ 1 (e^(-θu)-1)(e^(-θv)-1) / (e^(-θ)-1) ]这里的θ是相关参数取值范围是全体非零实数。θ大于0对应正向关联θ小于0对应负向关联θ趋近于0时Frank-Copula退化为独立情形。阿基米德Copula族都有一个共同的“生成元”函数把多维联合分布表示成单变量函数的形式Frank的生成元是φ(t) -ln[ (e^(-θt)-1) / (e^(-θ)-1) ]这个形式不只是数学上的优雅它直接决定了抽样和参数估计时能不能写出解析表达式。Frank-Copula的密度函数、条件分布函数和条件分布逆函数都有闭式解这在后面的Matlab实现里会省掉很多数值求根的时间。2.2 参数θ与Kendall秩相关系数的换算关系估计出θ之后要判断它对应的相关性到底有多强直接看θ本身是不直观的需要换算成Kendall秩相关系数。对于Frank-Copula两者的关系是τ 1 4/θ · [ 1/θ · ∫₀^θ t/(e^t-1) dt - 1 ]里面的积分是Debye函数没有初等表达式但数值求解很容易。如果只想快速估算可以直接在Matlab里数值积分再配fzero反解θ。我实际做项目时更推荐直接走极大似然估计得到θ然后算一次对应的τ用于结果汇报这样既保持了估计精度又能向非专业的人解释清楚“这个参数到底意味着多大的相关性”。2.3 Frank、Gumbel、Clayton和t-Copula怎么选做风光出力建模时候选的阿基米德Copula不止Frank一个常见的还有Gumbel和Clayton外加椭圆族里的t-Copula。它们的主要区别在尾部相关性上Copula类型上尾相关下尾相关适用场景Gumbel有无变量容易同时冲高比如极端大风时多个风电场同时满发Clayton无有变量容易同时走低比如无风无光同时出现导致出力双双归零Frank无无相关结构整体对称中间段关联明显两端极端联动较弱t-Copula有有两个尾部都需要刻画但参数多、小样本估计方差大风光出力有个特点极端情况下往往是一方高、另一方低真正“同时极端高”或“同时极端低”的概率并不比中等强度联动高太多。Gumbel和Clayton都只偏重一个尾部容易把极端关联估计过头t-Copula虽然灵活但小样本条件下自由度参数估计不稳定。Frank-Copula的对称相关结构反而更贴合风光出力的实际依赖形态这也是我最终选择Frank而不是其他族的原因。3. 场景生成四步法从历史数据到未来出力样本3.1 第一步数据清洗与边缘分布拟合无论用什么Copula第一步都必须把边缘分布处理好。我习惯的顺序是先拿到同一风电场和同一光伏电站的同一时段历史出力数据时间分辨率可以是15分钟、1小时或更长但必须保证两者采样时间戳严格对齐然后做三件事。第一是剔除异常点。停机检修、通讯中断、限电导致的出力平台这些数据虽然看起来“真实”但它们不是气象波动造成的会污染Copula的参数估计。判断标准很朴素突然掉到零且持续数小时不动大概率不是自然出力变化。第二是归一化。把风电出力除以装机容量、光伏出力除以装机容量让变量落到[0,1]区间方便后续概率变换。第三是拟合边缘分布。这里我推荐非参数方法直接用核密度估计或经验分布不要一上来就假设一定是Weibull或Beta。参数分布虽然形式简洁但一旦假设错了后续所有结果都会被系统性偏差带偏。具体到Matlab里用ksdensity函数估计经验CDF或者直接用ecdf都是成熟可靠的方案。用参数分布时还可以分别拟合wblfit和betafit然后做K-S检验确认假设是否成立。3.2 第二步概率积分变换与Frank-Copula参数估计边缘分布拟合完成后要对原始出力数据做概率积分变换把每个观测值映射到[0,1]区间上的均匀分布随机数。这一步的目的就是把“原始出力值”变成“分位数”让数据只保留排序信息丢掉原始量纲和分布形态之后才能进入Copula的建模空间。有了变换后的序列(u_wind, u_solar)以后Frank-Copula的参数估计就变成标准的统计推断问题。最高效的路径是直接用Matlab统计工具箱的copulafit函数指定Frank族返回的就是极大似然估计下的θ。如果手头没有工具箱也可以自行实现对数似然函数再用fminsearch求最大值结果基本一致。3.3 第三步从Frank-Copula中抽取相关随机数参数确定之后用copularnd(Frank, theta, N)就能生成N个[0,1]²上的相关均匀随机数。这N个点已经携带了Frank-Copula定义好的依赖结构它们不是独立散点边缘近似均匀但整体排列呈现出跟历史数据一致的关联模式。如果没有工具箱条件分布法也能实现同样功能。先抽u1服从均匀分布再从Frank-Copula的条件分布函数反解出u2。Frank-Copula的好处就在于条件分布逆函数有解析式不需要逐点数值求根速度很快。3.4 第四步逆变换还原风力和光伏出力场景随机数本身不是场景必须通过边缘分布的逆变换把它还原成出力值。这一步用到的核心工具是分位数函数对于第i个样本点风力出力等于风力边缘分布的u1分位数光伏出力等于光伏边缘分布的u2分位数。如果前面用的是经验分布Matlab里的quantile函数直接可用如果用的是kde拟合再取CDF变换那就需要用ksdensity得到反函数或者直接使用ecdf逆如果用的是参数分布则有wblinv、betainv这类专用逆函数。注意分位数计算时数组排列必须一一对应u1和u2不能各自排序后再组合否则就又把相关性破坏了。4. Matlab落地核心代码逐段拆解4.1 用统计工具箱实现完整场景生成流程如果你的Matlab装了Statistics and Machine Learning Toolbox那整个流程可以用简洁的脚本跑通。下面这段代码我按步骤拆开来写每段的作用都注在注释里。% 1. 加载历史数据 % data为hours×2矩阵第一列风电归一化出力第二列光伏归一化出力 load(wind_solar_history.mat); x_wind data(:,1); x_solar data(:,2); % 2. 概率积分变换从出力值映射到[0,1]分位数 u_wind ksdensity(x_wind, x_wind, function, cdf); u_solar ksdensity(x_solar, x_solar, function, cdf); % 为防边界出现严格0或1做微小截断 u_wind max(min(u_wind, 1-1e-6), 1e-6); u_solar max(min(u_solar, 1-1e-6), 1e-6); % 3. 用极大似然估计Frank-Copula参数 theta copulafit(Frank, [u_wind, u_solar]); fprintf(Frank-Copula参数 theta %.4f\n, theta); % 4. 生成1000个相关均匀随机数 N 1000; U copularnd(Frank, theta, N); % 5. 逆变换到出力场景 wind_scenarios quantile(x_wind, U(:,1)); solar_scenarios quantile(x_solar, U(:,2)); % 6. 检查生成数据的秩相关是否接近历史 tau_hist corr(u_wind, u_solar, Type, Kendall); tau_sim corr(U(:,1), U(:,2), Type, Kendall); fprintf(历史Kendall tau %.4f, 生成场景Kendall tau %.4f\n, tau_hist, tau_sim);两步最容易出错的地方一是ksdensity返回的CDF在数据极值附近可能出现0或1不截断的话后面的对数运算会直接NaN二是quantile函数默认的分位点算法可能和ecdf逆略有差异但样本量足够大时影响可以忽略。4.2 不依赖工具箱的自定义Frank-Copula抽样没有统计工具箱的同学不用慌Frank-Copula的抽样完全可以自己写。关键是利用条件分布法也就是先抽u1再根据条件分布函数反解u2。我推导过Frank-Copula的条件分布反函数表达式为u2 -1/θ · ln[ (A(1-t) t·e^(-θ)) / (A(1-t)t) ]其中A e^(-θ·u1)t是在[0,1]上独立抽取的均匀随机数。对应Matlab代码function U frank_rnd(theta, N) % 从二元Frank-Copula生成N个样本 % theta: 相关参数必须非零 % U: N×2矩阵每一行是一对相关均匀随机数 u1 rand(N, 1); t rand(N, 1); A exp(-theta * u1); denom A .* (1 - t) t; numer A .* (1 - t) t .* exp(-theta); u2 -1/theta .* log(numer ./ denom); U [u1, u2]; end这段代码我实测过在θ绝对值不超过20时数值稳定性都很好。θ太大时e^(-θ)会下溢建议在函数内加一个判断当θ超过某一阈值时改用极值近似或直接调用工具箱。用这个函数替换copularnd后续的逆变换流程完全一致。4.3 场景可视化与相关结构验证生成场景后不要急着直接用先画图检查一遍。一张是原始数据的散点图一张是生成场景的散点图两张图放在一起对比能非常直观地看出Copula有没有把相关结构复制过来。figure; subplot(1,2,1); scatter(x_wind, x_solar, 10, filled, MarkerFaceAlpha, 0.4); xlabel(风电出力(p.u.)); ylabel(光伏出力(p.u.)); title(历史数据); axis equal; grid on; subplot(1,2,2); scatter(wind_scenarios, solar_scenarios, 10, filled, MarkerFaceAlpha, 0.4); xlabel(风电出力(p.u.)); ylabel(光伏出力(p.u.)); title(Frank-Copula生成场景); axis equal; grid on;如果图形上生成场景的分布形态和历史数据有明显差异问题大概率出在边缘分布拟合上而不是Copula本身。这一点我在下一节详细展开。5. 踩坑实录Frank-Copula在Matlab中的三个典型问题5.1 ksdensity算出的CDF出现0和1导致NaN我第一次跑通全流程时copulafit直接返回NaN当时整个人是懵的。排查过程并不复杂先检查输入向量有没有NaN结果没有再查u_wind和u_solar的范围发现里面有严格等于0和1的值。ksdensity对数据范围有敏感性当历史出力数据集中在某一区域时CDF估计在边界上会直接变成0或1这些值进入Frank-Copula的对数项后就会触发NaN。解决方案就是在概率积分变换后加一个截断处理。我通常把上下界设为1e-6和1-1e-6既不会影响秩相关结构的估计也避免了数值溢出。如果历史数据量很大可以考虑把截断阈值调小到1e-8但1e-6在绝大多数情况下都够用。5.2 生成的场景边缘分布看起来不像历史数据另一个常见症状是Copula散点图的相关性看着对了但生成的风电场景分布形状和历史数据出入很大比如最大值不够大、低出力占比过多。这个问题的根因十有八九在逆变换这一步而不是Frank-Copula本身。我排查过一次发现我直接用ksdensity输出的CDF反函数出了问题——ksdensity的function,icdf语法要求输入的是概率值但我误传了原始出力值。还有一种常见误区是有人用norminv去反变换非正态边缘分布那结果必然失真。正确做法是用什么方法拟合的边缘分布就用什么方法的逆函数去还原。经验分布对应quantile核分布对应ksdensity的icdf选项参数分布对应各自的专用逆函数。5.3 相关性强度的估计明显偏离经验值有时候copulafit返回的θ对应的Kendall τ与直接在数据上计算的Kendall τ对不上偏差甚至超过0.1。这通常不是算法问题而是边缘分布变换阶段引入了额外噪声。比如大量重复值常见于出力为0的情形会让经验CDF在0附近产生一个平台概率积分变换后变成一堆相等的分位数这些重复点会干扰Copula的参数估计。处理方法是把“零出力”和“非零出力”分开建模。可以先用一个伯努利变量描述是否出零再用Copula只对正出力部分建模最后组合成完整场景。这种处理既保留了零出力概率又不会让零值点扭曲相关结构。如果只是粗略模拟也可以给零出力值加一点随机抖动但精度要求高的场景还是建议做两阶段建模。5.4 削减后场景丢失相关性优化结果失真还有一个很隐蔽的坑不在生成环节而在场景削减环节。有人用kmeans把1000个场景聚成10个典型场景后发现典型场景之间的Kendall τ明显小于原始场景。原因是kmeans按欧氏距离聚类会倾向于把空间上接近的点归为一组但相关结构和欧氏距离不是一回事聚类中心的秩相关天然会被稀释。解决思路是在聚类特征里显式加入相关结构的信息。我会在原始出力数据之外同时把概率积分变换后的u1和u2也放进聚类特征向量让聚类算法同时兼顾边缘分布形态和依赖结构。也可以改用基于概率距离的场景削减算法比如快速前向选择这类方法的目标函数里直接包含了场景概率和距离测度对保持相关结构更友好。6. 场景生成之后评估、削减和扩展方向6.1 用哪些指标检验场景质量生成场景不能只靠肉眼判断我一般会用三个指标做量化验证。第一是秩相关指标对比历史数据和生成场景的Kendall τ或Spearman ρ偏差控制在0.05以内基本可接受。第二是边缘分布指标把生成场景的均值、标准差、分位数和历史数据做对比偏差不应该超过工程允许范围。第三是联合分布的拟合优度检验把历史数据与生成数据同时落入某些二维区间的频率做对比比如“风电低于0.3且光伏低于0.3”的比例如果生成场景在此区间的频率和历史数据偏差很大说明联合分布的尾部刻画有问题。6.2 从数据相关性到调度场景的落地衔接场景生成本身不是终点它是随机规划、鲁棒优化等下游模型的输入。在把场景喂给优化模型之前最好先做一轮场景削减把上千个场景浓缩成几十个具有概率权重的典型场景。削减方法上kmeans简单易用但要注意我在5.4节提到的相关结构稀释问题快速前向选择法在电力系统场景削减中更常用Matlab里可以自己实现也可以在优化工具箱里找场景削减的第三方函数。削减完成后还要给每个典型场景重新归一化概率权重保证所有场景概率之和为1。6.3 从二元Frank到更复杂的扩展这套方法虽然叫“二元Frank-Copula”实际项目里经常要扩展。一个自然的升级方向是把风电、光伏加上负荷做成三维Copula或者按照季节分别建模用不同季节的θ来体现风光相关性的时变特征。另一个方向是混合Copula用Frank和Gumbel、Clayton的加权组合来同时刻画对称相关和尾部相关权重和参数一起做极大似然估计。这些内容足够再写一整篇博客了但核心思想不变边缘分布描述单变量的不确定性Copula描述变量之间的相关性两者解耦后再组合生成联合场景。我实际跑完这套流程后最大的体会是Copula方法的数学门槛不算高真正的门槛在数据预处理和工程判断。生成场景的可靠性很大程度取决于边缘分布拟合得是否合理、数据清洗是否彻底、参数估计是否稳健这些都是枯燥但绕不开的功夫。如果你刚开始做风光出力场景生成建议先用1000个场景跑通上面所有代码观察散点图和秩相关系数是否符合预期再逐步扩大样本量、加入更多约束。过程中遇到生成场景和历史数据不一致时别急着调Copula先回头检查边缘分布和逆变换那两步大概率问题都在那里。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价