1. 项目到底在解决什么问题
1.1 风光出力的不确定性从哪来
风电和光伏的出力都看天吃饭。风速不是平稳的,辐照度更是随着云层、季节、时段急剧变化。这种随机性落在电力系统里,就会变成“明天这一时刻,某台风机到底能发多少电”这个问题。相比传统火电的可调度性,风光电源说白了就是“有多少吃多少”,不稳定、不可控、还带强波动。
做规划或者调度的人,天天要面对一个问题:我不能用一组固定数值来描述未来的风光出力,因为那是假的;也不能把过去一整年每一天的曲线全塞到优化模型里,因为计算量直接爆炸。于是就有了“场景法”这个折中方案——用有限几个有代表性的出力曲线,代替无穷多种可能,把随机优化问题转成确定性的多场景问题。
这里面的核心矛盾就是:场景太少,代表性不够,计算结果偏乐观或偏悲观;场景太多,计算量扛不住。所以场景生成和场景削减,本质上是在“精度”和“计算代价”之间做一次理性权衡。
1.2 为什么不能直接拿历史数据用
很多人第一个想法是:直接拿过去三年的实测数据,每天一条曲线,一年365天,三年差不多一千条,不就有场景了吗?
理论上是能这么干,但问题很现实。第一,历史数据是已经发生的,未必覆盖未来可能出现的极端情况;第二,数据量太大,直接丢给优化求解器跑,轻则内存爆掉,重则几天几夜算不完;第三,也是最关键的——风电和光伏之间、不同风电场之间、不同光伏电站之间存在空间相关性,过去某一天的数据只是那一次“联合实现”,无法穷尽所有可能的联合情况。
比如说,一个区域电网里风资源和光照资源往往存在互补性:白天光伏出力大但风可能小,夜里风大但光为零。这种“耦合特性”如果靠堆历史数据来表达,效率极低。而恰恰Copula函数就是干这个的——描述两个或多个随机变量之间的相关结构,把边缘分布和相依结构拆开,然后分别建模。
1.3 Copula和K-means在这里各自扮演什么角色
一句话概括:Copula负责“造场景”,K-means负责“砍场景”。
先说Copula。它解决的是数据生成问题。我用历史数据拟合出风速、辐照度各自的分布(边缘分布),再用Copula函数刻画它们之间的相关关系,接下来就可以通过蒙特卡洛采样生成成千上万组“虚拟的但统计特征与真实数据一致”的风光联合出力样本。想要多少有多少,覆盖场景比历史数据更全。
再说K-means。采样生成一万组数据,优化模型可吃不下。K-means算法把这一万个样本分成几十个簇,每个簇的中心作为“典型场景”,簇内样本的多少作为该场景的权重。这样原来一万个场景就被削减成了二三十个典型场景,信息损失控制在一个可接受的范围内。
这两个算法组合在一起,构成了一个非常经典的风光联合场景生成与削减框架:先建概率模型,再采样扩充,最后聚类压缩。这个框架在学术界被大量使用,在工程实践中也有相当强的可落地性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 工具选型与核心原理
2.1 为什么选Copula而不是联合正态分布
很多人可能会问:风速和辐照度能不能直接用一个二维正态分布来建模?其实不行,原因有两个。
第一个原因是边缘分布大概率不是正态的。风速一般服从Weibull分布,辐照度更复杂,晴天是接近Beta分布的形状,阴天又完全不一样。如果强行用正态分布拟合,尾部大概率失真,极端场景缺乏覆盖。第二个原因是变量之间的相关性不是简单的线性相关。风速和辐照度之间的相关关系往往是非线性的,甚至在不同区间强度不一样。Copula的好处就在于:我可以先选择最合适的边缘分布拟合各自数据,再用一个灵活的Copula函数把两者“绑”起来。建模是分两步走的,哪一步出错都可以单独检查和修正。
2.2 Copula的核心逻辑与常用类型
Copula函数从数学上说,是一个把多维分布拆成边缘分布+相关结构的连接函数。Sklar定理告诉我们:任何多维联合分布函数,都可以写成边缘分布和一个Copula函数的复合形式。
举个例子,两个变量的联合分布F(x, y)可以被表示为:
F(x, y) = C(F₁(x), F₂(y))
其中F₁和F₂是两个边缘分布,C就是Copula。“U₁ = F₁(x)、U₂ = F₂(y)”都属于[0, 1]区间,所以Copula本质上是把相关性映射到了一个均匀空间里来研究。
实际工程中常用这么几种Copula:
| Copula类型 | 特点 | 适用场景 |
|---|---|---|
| Gaussian Copula | 对称、无尾部相关,计算简单 | 通用默认选项,样本量不够时的安全牌 |
| t-Copula | 对称但包含尾部相关 | 需要捕捉极端同出现象时 |
| Clayton Copula | 非对称,下尾相关强 | 重点关注低出力场景(比如无风无光) |
| Gumbel Copula | 非对称,上尾相关强 | 重点关注高出力场景(比如大风大光同来) |
| Frank Copula | 对称、无尾部相关 | 相关性较弱的情况 |
在风光场景里,我最常用的是Gaussian Copula打底,然后对比一下Clayton。因为对于电网安全分析来说,低出力场景往往更棘手——风小光弱时,系统需要别的电源顶上,这时候无风无光同时出现的概率大小,直接影响备用容量的配置方案。
2.3 K-means聚类为什么适合场景削减
K-means的思路很简单:在样本空间里找K个中心点,让每个样本到其所属中心的距离平方和最小。算法步骤就是四步走:初始化中心→分配样本到最近中心→更新中心位置→重复迭代直到收敛。
用在场景削减上,K-means有几个天然的适配点。
第一个是样本形态。我们生成的场景是多维向量(每个时段的出力值),欧氏距离天然能应对这种高维向量场景。第二个是聚类结果可直接用。聚类中心本身就是一个“平均情况”场景,簇内样本数量与总样本数的比值就是该场景的发生概率。第三个是计算效率高。即使一万个24维样本,K-means在sklearn里面也就是毫秒到秒级的事。
但K-means也有自己的短板。初始中心选不好可能陷入局部最优,需要配合K-means++初始化策略;K值需要预先指定;对噪声敏感。这些问题在实操作中都有对应的处理方式,后面详细说。
3. 实操过程与关键环节实现
3.1 数据准备与边缘分布拟合
以项目数据为例,假设我有某风电场和光伏电站同期的历史出力数据,时间分辨率取1小时,那么每天就是24个时段。为了让场景直接服务于调度,通常直接使用出力数据而不是风速和辐照度原始数据,这样省掉一个“功率曲线换算”步骤,也避免引入风机功率曲线的误差。
数据清洗这一步建议做个比较细致的处理。首先要剔除异常值,比如出力为负或超过装机容量的记录;其次要处理通信中断造成的连续零值或NaN值;最后还要注意不同季节的出力分布差异很大,如果数据量足够,建议分季节建模、分季节生成场景,不然一个年度的模型会把季节特征全部磨平。
边缘分布拟合是整个Copula建模的基础。这里有个容易踩的坑:不要想当然地认为风速服从Weibull分布,就套一个两参数Weibull上去完事。实测出力数据的分布形态在0附近往往有一个很大的尖峰(因为风机有切入风速,光伏夜间出力为0),这个尖峰很难用标准参数分布拟合。
我实操中比较推荐的做法是用核密度估计(KDE)。虽然KDE在数据稀疏区间容易过拟合,但配合带宽参数调整,拟合效果通常优于参数分布。如果数据量大(5000个点以上),KDE的表现非常稳定。
python复制import numpy as np
from sklearn.neighbors import KernelDensity
# wind_power 和 solar_power 为清洗后的历史出力序列(0~1标幺值)
# 转为 [0, 1] 区间,避免量纲影响
wind_data = wind_data.reshape(-1, 1)
solar_data = solar_data.reshape(-1, 1)
kde_wind = KernelDensity(kernel='gaussian', bandwidth=0.05).fit(wind_data)
kde_solar = KernelDensity(kernel='gaussian', bandwidth=0.05).fit(solar_data)
# 计算CDF:通过积分方式近似
u1 = np.array([np.exp(kde_wind.score_samples(np.array([[x]])))
for x in np.linspace(0, 1, 1000)]).cumsum()
# 归一化到[0,1]
u1 = u1 / u1[-1]
注意这里是先算PDF再累计得到CDF的近似形式。如果需要更精确的做法,可以直接用经验CDF替代。但经验CDF有个毛病:在样本边缘处是断的,样本最大值对应的CDF值直接是1.0,这在后续采样时会把采样值截断在历史极值以内。
这个细节很关键——如果边缘分布的尾部被截断了,生成的场景就永远无法超过历史最大值,极端场景覆盖率会大打折扣。为了解决这个问题,可以在经验CDF尾部做线性延伸,或者直接用核密度估计来拟合CDF。这也是我更喜欢KDE的原因之一。
3.2 Copula参数估计与最优类型选择
有了边缘分布后,把历史数据通过各自的CDF映射到U空间:
python复制# 将历史出力值映射到 [0,1] 均匀空间
u_wind = empirical_cdf(wind_data) # 自定义函数
u_solar = empirical_cdf(solar_data)
这时的(u_wind, u_solar)就构成了U空间里的一组样本。Copula参数估计本质上就是对这组样本做分布拟合。对于Gaussian Copula,参数就是相关系数矩阵,直接用Pearson相关估计即可;对于阿基米德Copula则用极大似然估计。
工程上更省事的思路是用现成的Copula库。Python生态里目前比较顺手的是copulas库,它封装了Gaussian、Clayton、Gumbel、Frank等常用Copula的拟合、采样和评估接口:
python复制from copulas.multivariate import GaussianMultivariate
from copulas.univariate import KDEUnivariate
# 构造多变量Copula模型
copula_model = GaussianMultivariate()
copula_model.fit(np.column_stack([u_wind, u_solar]))
# 生成5000组联合场景
samples = copula_model.sample(5000)
但如果要对多种Copula做横向对比,copulas库的模型种类还不够全,这时可以用copulae库,或者直接用数值方法自己实现几种Copula的似然函数来拟合。
多模型对比时用AIC或BIC来选。这里特别提醒一句:AIC最小的模型不一定是实际用起来最好的模型。因为AIC衡量的只是拟合优度与参数复杂度的平衡,但在场景生成任务中,我们更关心的是采样生成的场景是否覆盖了“关键风险区”。
比如Clayton Copula对下尾相关刻画更强,如果系统更关注“风小光弱”这类场景,哪怕它的AIC比Gaussian Copula略高,我建议仍然选Clayton。模型服务于场景,场景服务于决策目标,这是整个框架里最容易被人忽略的原则。
3.3 蒙特卡洛采样生成大规模联合场景
Copula模型拟合好后,生成大规模场景就是一个标准操作:
- 从Copula中采样N组均匀随机数(u₁, u₂),N建议取5000到10000;
- 对每个u值,用对应边缘分布的逆CDF转换为实际出力值。
python复制# 从拟合好的Copula中采样
samples = copula_model.sample(5000) # 形状为(5000, 2)
# 逆CDF变换:从均匀分布转回实际的出力值
# 这里用KDE拟合的逆CDF函数
wind_scenarios = inverse_cdf_wind(samples[:, 0])
solar_scenarios = inverse_cdf_solar(samples[:, 1])
# 组装为场景矩阵:每个场景是一天的24时段曲线
# 假设每个时段独立采样,这里简化示意为直接按日组织
wind_daily = wind_scenarios.reshape(-1, 24)
solar_daily = solar_scenarios.reshape(-1, 24)
这里要注意一个细节:如果把24个时段当成独立变量来处理,那每个场景就是24维的风电向量和24维的光伏向量拼成48维向量。但如果直接独立采样每个时段,就会破坏时序上的连续性——比如风电出力在第5时段很高、第6时段就突变成0,这在物理上是不可能的。
解决这个问题有几种做法。最简单的处理是保持历史数据的时序相关性:在U空间里,不是对每个时段单独采样,而是把整个时段序列作为一个高维向量来拟合Copula。但48维的Copula拟合需要海量数据支撑,数据量不够时容易过拟合。
更工程化的做法是:先针对“全天的平均出力水平”或“每天几个关键时段的出力水平”建一个二维或三维的Copula模型,生成日级别的出力场景,再依据历史数据中的典型日内曲线形态,把日级别场景展开成时段级曲线。这样既保留了日间相关性,又不至于维度爆炸。
这一步是场景生成中的关键难点,也是我在项目里花时间最多的地方。对于大部分实际项目,二维Copula(风速+辐照度)加条件采样(在给定日平均出力水平下恢复时序曲线)的组合,是一个效果和可解释性都相对均衡的方案。
3.4 K-means削减与典型场景提取
生成一万个场景后,接下来就是从重到轻的过程——K-means削减。
先说预处理。场景矩阵中风电和光伏的量纲虽然都是标幺值(0到1),但两者的方差结构差异很大。风电出力波动通常比光伏更剧烈,直接丢进K-means会导致聚类结果被方差大的变量主导。需要先做标准化(Z-Score Normalization),或者至少各自除以标准差。
python复制from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
# X_daily: 形状为(N_samples, 48),前24维是风电,后24维是光伏
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_daily)
# K-means聚类,K值先用肘部法则粗选
k = 30
kmeans = KMeans(n_clusters=k, init='k-means++', n_init=20, random_state=42)
kmeans.fit(X_scaled)
# 提取聚类中心和权重
centers_scaled = kmeans.cluster_centers_
centers = scaler.inverse_transform(centers_scaled) # 还原为真实出力值
# 计算每类场景的权重
labels = kmeans.labels_
weights = np.bincount(labels) / len(labels)
聚类完成之后,每个聚类中心就是一条典型的风光联合出力曲线(24维风电+24维光伏),权重就是这类场景出现的概率。这些“中心+权重”对可以直接代入后续的优化模型。
比如一个两阶段随机规划问题里,决策变量包括“现在就要决定的机组启停状态”和“等待场景明确后再决定的出力调整”,每个场景带着权重参与目标函数计算。三十个场景的随机优化计算量,和单场景确定性问题的差距并不大,现代求解器完全能处理。
3.5 场景质量评估
场景生成和削减不能做完就完了,至少要回答两个问题:削减后的场景是否能代表原来的概率分布?生成场景是否覆盖了历史数据的关键统计特征?
我通常会用三个指标来评估:
第一个是削峰系数或期望值误差。对比削减前后总体的期望出力、方差、各时段均值等统计量,误差控制在5%以内一般认为是可接受的。
第二个是概率分布拟合度。把原始历史数据的出力分布与削减后场景的出力分布做KS检验(Kolmogorov-Smirnov Test),p值越大说明两个分布越接近。
第三个是场景间距离。计算各典型场景之间的最小距离,如果距离太小,说明聚类中存在冗余场景,可以考虑减少K值;如果最大距离过大,说明存在离群场景,需要检查是否合理(有时极端场景确实存在,不应直接删)。
python复制from scipy.stats import ks_2samp
# 对比历史数据和生成场景在各时段的分布
for t in range(24):
stat, p_value = ks_2samp(historical_wind[:, t], wind_scenarios[:, t])
# 记录p值,p值越小说明分布差异越显著,需要排查
这里有个经验值分享:如果绝大多数时段的KS检验p值都小于0.05,那大概率是边缘分布拟合出了问题,而不是聚类的问题。先把边缘分布调好,再去看聚类的参数。
4. 常见问题与排查技巧实录
4.1 边缘分布拟合失控
KDE的带宽参数非常敏感。带宽设得太小,拟合曲线会在数据密集的地方出现很多毛刺,导致“过拟合”,生成场景会在某些出力值附近集中爆量;带宽设得太大,又会让分布变得太平滑,丢失峰谷特征。建议先把带宽在0.03到0.1之间扫一遍,看拟合曲线和生产场景的CDF与经验CDF的误差曲线上限,选择一个误差最小的带宽。
另外还有一类数据容易出问题:光伏夜间出力为零。如果样本中有一半的点是0,KDE拟合出来的分布在0附近会有一个很高的尖峰。在逆CDF采样时,这个尖峰会转化成大量采样值为0的场景。这是合理的物理特征,但如果比例过高,会导致削减后光伏低出力场景过重。这时可以考虑分时段建模——白天和夜间分开拟合边缘分布,采完再拼回去,效果更好。
4.2 K-means聚类距离选择
通常用欧氏距离就可以了,但这背后有个假设:各时段之间独立同权。实际上,在对场景做削减时,不同时段的误差对后续优化结果的影响是不同的。比如早晚高峰时段的误差对机组组合的影响,远大于凌晨低负荷时段。如果希望削减结果对特定时段更精确,可以对各时段施加不同权重,再进行加权欧氏距离聚类。
如果场景是复杂形态的时序曲线(比如某些时段出力大起大落),可以尝试用DTW距离替代欧氏距离。DTW能更好衡量曲线形状的相似性,但计算量大得多,且scikit-learn的KMeans不直接支持自定义距离矩阵,需要配合tslearn库的TimeSeriesKMeans来做。大部分项目用普通欧氏距离就足够了,DTW属于“知道有这回事,但用不上”的工具。
4.3 K值到底选多少
这个问题没有标准答案。场景削减后的K值本质上是“计算资源”和“精度”之间的折中指标。我见过有人用肘部法则,有人用轮廓系数,还有人直接凭经验定。
肘部法则的操作方式是:对K从5到50逐一聚类,记录每个K对应的簇内误差平方和(SSE),画折线图,找拐点。但这个拐点往往不是特别明显,反而有点“哪里都像拐点”的感觉。轮廓系数也是类似情况。
我自己的经验是:先确定优化模型能接受的场景上限(比如30个),再在这个上限内用增量方式测试。从10个场景起步,逐步增加到20、30,观察目标函数值的变化幅度。如果从20加到30时目标函数变化已经很小(比如小于1%),那么20就是合理的K值。这种做法摆脱了“统计学指标”的抽象感,直接服务于工程决策。
4.4 Copula类型选错会怎样
Gaussian Copula是默认选择,但它的对称性注定了它无法刻画非对称的相关结构。对于风光联合出力,极端情况往往是“无风且无光”这种同低出力事件,这在Copula的术语里叫“下尾相关”。如果相关性主要体现在下尾,Clayton比Gaussian更合适。反之,如果关心的是“大风且大光”的极端高出力场景,Gumbel更合适。
判断方法很简单:画出历史数据在U空间的散点图。如果散点在(0,0)附近明显密集,说明下尾相关强;如果在(1,1)附近密集,则是上尾相关强;如果两边都密集,用t-Copula;如果均匀分布,用Gaussian或Frank。
这个散点图也可以顺便用来检查U空间数据是否真的服从均匀分布。如果明显偏向某一边,说明边缘分布的CDF变换有问题,先去查边缘分布,然后再谈Copula。
4.5 采样规模和削减规模的平衡
采样量太小,生成的场景可能没有覆盖极端情况;采样量太大,K-means计算时间上来了,但也没必要。我的经验是:采样数量取最终需要场景数的30到50倍。比如需要30个典型场景,就采样1500到3000个。如果采样太多(比如十万个),K-means会花很长时间,而且聚类结果并不会比三万个样本显著更好。
但要注意,K-means聚类的簇大小和簇之间的分离度受样本量影响。样本量过小时,某些稀疏区域的场景可能被聚类算法当成离群点处理而无法形成独立簇,导致极端场景丢失。如果特别关注极端场景,可以适当增大采样量,或者在聚类前先把极端场景单独筛选出来保留,再对剩余场景做K-means削减,最后合并。
5. 从复现到落地的一些个人体会
这套Copula+K-means的框架,在风电光伏联合场景生成这个方向上算是比较成熟和通用的做法了。它不需要太多的数学背景就能上手,但每个环节都有值得深挖的细节。复现完整的框架不难,难的是把你自己的数据特性摸清楚,在不同的建模决策点上做出合适的选择。
我在实操中还有个体会:场景生成的最终目的是服务于电力系统的优化决策,不要把太多精力花在追求统计指标的完美上。AIC低0.5、KS检验p值高0.05,这些对最终优化结果的影响可能微乎其微;相反,很多建模上的“粗糙但合理”的简化,比如分季节建模、分时段建模、手动校准极端场景,反而能把结果往更可靠的方向推。
最后分享一个小技巧:生成场景后,不要急着丢进优化模型,先画几张图——风电和光伏的联合散点图、各典型场景的时序曲线、削减前后的累计分布对比图。做这些图花不了十分钟,但往往能一眼看出模型哪里不对劲。数据和模型之间的偏差,画图看远比看数字来得直观。
