把MC(Monte Carlo,蒙特卡洛)场景生成与削减放在一起做研究,十有八九是因为你手上有个含随机性的优化问题要解。不管是电力系统的随机机组组合、微电网能量管理,还是供应链库存决策,思路都差不多:先采样出大量可能的未来场景,再通过削减把成千上万条曲线压缩到十几条,让优化模型在能算动的范围内把不同情形下的方案都考虑进去。这个思路本身不复杂,但有个东西特别容易被人忽略——时序相关性。我今天就专门聊这一点,顺带把场景生成与削减的完整技术链路拆开讲清楚。
这篇文章适合谁看?如果你正在做新能源出力场景建模、随机规划里的场景预处理,或者刚接触场景法但总感觉“生成的场景怪怪的、削减后误差很大”,那这篇内容应该能帮你避开我之前踩过的不少坑。
1. 蒙特卡洛场景方法到底在解决什么问题
1.1 为什么不能拿期望值糊弄过去
很多初学者上来就问:既然有随机性,那我直接用期望值代表随机变量不行吗?答案是:在决策层面,这往往是灾难性的。
我举个例子。某风电场额定装机900MW,某个时段的预测/期望出力是300MW。如果你拿这300MW去做备用容量配置,看起来够用了。但实际风电出力可能低到0,也可能接近900MW。期望值把这种波动全部抹平了,你算出来的备用容量大概率不够用。一旦出现低风电日,系统就得切负荷。
场景方法的本质,就是把“连续分布”这个数学上很优雅但无法直接塞进优化模型的东西,离散化成一堆带概率的确定性场景。每个场景都是一条完整的时间序列,代表一种可能发生的未来。优化模型不再针对一个“平均数”做决策,而是针对所有场景做决策——保证无论哪个场景出现,方案都不至于太离谱。
但这里有一个计算代价的问题:你采样10000个场景,每个场景96个时段,优化模型里等于有96万个变量参与约束,一般商用求解器扛不住这么大规模。这时候就需要“场景削减”,从10000个场景里挑出10到20个“最能代表整体分布”的场景,把它们的概率重新归一化,让优化模型在一个可承受的规模下运行。
1.2 场景在不确定决策流程中的位置
整个流程可以拆成四步:蒙特卡洛生成、场景削减、优化求解、方案评估。
- 生成阶段的任务是“准确地描述随机过程”,做得好不好,取决于你的概率模型有没有抓住真实数据的统计特征。
- 削减阶段的任务是“压缩概率测度”,做得好不好,取决于削减后的场景集和原始场景集在概率意义上还像不像。
- 优化求解就不多说了,那是下游任务。
- 方案评估阶段,一般会把削减后的场景再放回仿真模型里跑一遍,看看误差是否可接受。
这四步里,时序相关性在前两步各丢一次:生成时如果用了独立采样,丢一次;削减时如果距离度量只盯着逐点误差,再丢一次。很多人只盯着第二步,结果第一步就错了,后面全白搭。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 时序相关性被丢掉之后会发生什么
2.1 独立采样的典型失真案例
先说什么是独立采样,就是假设每个时段的随机变量互相独立,单独抽样再拼成一条“场景”。这种做法最容易实现,但因为丢掉时序相关性,生成出来的场景往往不符合物理规律。
以风电功率为例,真实的风速变化受大气惯性影响,相邻时段出力高度相关。你如果独立采样24个时段,生成出来的曲线会出现什么情况?凌晨1点满发,凌晨2点掉到零,凌晨3点又满发。这种剧烈爬坡在实际风电场上几乎不可能发生——大尺度天气过程不会让风功率在一小时内从900MW掉到0再弹回来。
用这种场景去做机组组合,开停机方案会变得非常激进,机组频繁启停,备用容量配置失真。你说这方案能落地吗?不能。所以“时序相关性”不是锦上添花的细节,而是决定场景集可不可用的基础条件。
2.2 三种尺度的时序相关性
我把时序相关性拆成三个尺度,这样比较好理解:
第一个尺度是短时自相关,分钟到小时级别。从电网调度角度看就是一个时段到下一个时段的出力变化不能太离谱。风电出力在一个小时内的变化率通常限制在额定功率的5%到20%以内,这个约束本质上来自大气运动的连续性。
第二个尺度是日内周期性。光伏出力有明显的“日出而作日落而息”的曲线,负荷也有早晚双峰。你做场景生成时如果丢掉这个周期特征,可能中午光伏还在满发、晚上负荷高峰光伏却为零,这种场景组合在物理上毫无意义。
第三个尺度是天气过程,天到周级别。一个天气系统过境往往持续2到4天,所以一个“低风电周”后面往往跟着另一个“低风电日”,这种持续性在长期调度里特别重要。在出力上表现为“平台—爬坡—平台”的持续特征,用独立采样根本无法复现这种持续性。
统计上,这三种尺度分别对应自相关函数(ACF)的衰减形态、周期图上的显著谱峰、以及多日尺度的协方差结构。生成场景后,我拿到数据第一件事就是画ACF图,如果ACF掉得比实际情况快太多,那说明相关性结构还是有问题。
3. 生成端:让采样结果符合时序相关结构
3.1 用协方差驱动MC采样
如果想保留时序相关性,最经典的做法是用协方差矩阵驱动蒙特卡洛采样。具体来说,把一条时间序列看作一个多维随机向量,比如24个时段就是24维。通过历史数据估计均值向量μ和协方差矩阵Σ,然后生成多维正态样本。
[\mathbf{X} = \boldsymbol{\mu} + \mathbf{L} \mathbf{Z}]
其中(\mathbf{L})是Σ的Cholesky分解下三角矩阵,(\mathbf{Z})是标准正态随机向量。这样生成的所有样本都保留了Σ所描述的线性相关结构。
这个方法的优点是计算很快。24维采样10000次,在现代机器上也就几秒钟的事。但它有个前提:假设数据近似服从多元正态分布。新能源出力通常有界(风功率0到装机容量之间),而且分布偏态,直接套多元正态会采出负值或超过装机容量的样本。
实际工程里的做法是先做正态化变换,在正态空间里采样,再逆变换回去。比如用Box-Cox变换,或者用Nataf变换。这里有个细节很多人不知道:你在原始空间估计的相关系数,在变换到正态空间之后会发生变化,需要用Nataf公式修正相关系数,否则相关结构其实是错的。这个Nataf修正公式在很多可靠性分析教材里有快速查表,但论文里经常被一笔带过,我在实际研究里发现这一步不修正,后面生成场景的相关性会明显偏弱。
3.2 Copula思路
如果数据分布过于“非椭圆”,多元正态假设容易出问题。更稳妥的方案是用Copula把边缘分布和相关结构分开建模。
Copula的核心思想是:每个时段的随机变量先各自做经验分布变换,变成均匀分布,然后用一个Copula函数把这些均匀分布变量黏在一起。采样的时候,先生成相关均匀变量(可以由多维正态变量通过概率积分变换得到),再利用各时段的逆CDF把它们映射回原始空间。
这样做的好处很明显:边缘分布可以保持真实数据的偏态、重尾、有界特征,不再被高斯分布绑架。时段之间的相依结构由Copula的具体形式决定,高斯Copula的尾部对称性不够时,可以换t-Copula,它能捕捉尾部依赖。
下面这段伪代码可以让你快速理解Copula的采样流程:
python复制from scipy.stats import norm, multivariate_normal
# 1. 对每个时段的历史数据做经验CDF,得到U
# U = empirical_cdf(X) -> shape (n_samples, n_periods)
# 2. 用高斯Copula建模:把U转成正态空间
Z = norm.ppf(U) # 逐元素应用逆正态CDF
# 3. 估计Z的协方差矩阵并修正
Sigma = np.cov(Z, rowvar=False)
# 4. 在正态空间采样
Z_new = multivariate_normal.rvs(mean=np.zeros(n_periods), cov=Sigma, size=2000)
# 5. 转回均匀空间
U_new = norm.cdf(Z_new)
# 6. 利用各时段逆经验CDF映射回原始空间
X_new = inverse_empirical_cdf(U_new)
关键在于第5步到第6步,逆经验CDF会把均匀变量映射回真实的出力区间,这样就不会再出现负的风功率了。
3.3 数据驱动生成(GAN等)的适用条件
现在很多人在钻研对抗生成网络生成场景,理由很充分:生成对抗网络不需要显式建模分布,直接从真实数据里学,理论上能学到非常复杂的时序依赖。
但以我自己的经验看,生成对抗网络这类数据驱动方法对数据量要求很高。要训练出一个稳定的风电场景生成模型,至少要有几年的小时级数据,最好上千天。而一个刚投运不久的风电场,历史数据可能只有两三百天,这时候生成对抗网络的效果反而不如统计模型。数据少,生成对抗网络很容易过拟合,生成出来的场景多样性不足,反正不如协方差驱动采样和Copula的思路稳定。
如果你数据量确实足够,可以上Wasserstein-GAN或者扩散模型,它们对模式崩塌的抑制会比普通GAN好很多。但无论用哪种生成模型,后面都得做第5节讲的指标校验,别只看生成图片“看起来像”就完事,要量化到ACF、爬坡分布这些具体指标上。
4. 削减端:不是压缩数量,是保留随机过程的特征
4.1 先把削减问题数学化
假设你已经生成了N个原始场景,每个场景带一个概率权重(p_i),现在要从中选出一个包含(N_s)个场景的子集,并给每个选中场景赋予新的概率(w_j),使得削减后的场景集在概率意义上最接近原始场景集。
数学上常用Kantorovich距离(也就是Wasserstein距离的离散版本)来度量两个场景集之间的差异。这个距离的本质是“最优传输代价”:把原始场景集的概率质量搬运到削减场景集上,最小化搬运成本。如果两个场景集在概率上很接近,这个距离就很小。
这个视角非常重要——它告诉我们场景削减是在做概率测度的近似,而不是简单的数据压缩。
4.2 聚类法:最直接的削减方式
最贴近工程实践的是聚类法。k-means或者k-medoids把N个场景聚成(N_s)类,每类的聚类中心作为代表场景,概率为类内原始场景概率之和。
用k-means操作很简单,但我建议你在实际项目中注意几个细节。
第一,代表场景最好用k-medoids而不是k-means。k-means的均值中心会把多个场景“平均”成一条很平滑的曲线,但这条平滑曲线在现实中可能根本不会出现——比如风电场景求平均后,爬坡事件被完全抹平,变成一个不痛不痒的“温和场景”。k-medoids是从实际样本里挑一个最中心的场景作为代表,能保留真实轨迹形态。
第二,距离度量可以换成动态时间规整(DTW)。欧式距离只看逐点误差,两个形状相似但时间上稍微错位的轨迹可能被判定为“不相似”。DTW对时间错位更鲁棒,适合保留形态特征。但注意,如果你的下游优化模型对时段偏移很敏感(比如机组启停按时段安排),DTW可能掩盖一些时段偏移问题,需要谨慎使用。
第三,聚类数量K怎么定。一般用肘部法则或者轮廓系数。以96时段的风电场景为例,K取10到20通常都能达到不错的代表性。K太小,尾部风险被抹掉;K太大,下游优化规模又受不了。
还有一个经验:如果原始场景里有关键的极端场景(比如极端低风电日),它们概率虽小但后果严重,我一般先把这些场景单独提出来,强制保留,然后对剩余场景做聚类。这样既保住了小概率高风险事件,又不会因为它们干扰聚类过程。
4.3 概率距离法:后向削减与快速前向选择
聚类法直观,但在严格的概率测度近似意义上,后向削减和快速前向选择是更经典的做法。
后向削减(backward reduction)的思路是:一开始让所有人的原始场景都在集合里,然后每轮删除一个“对概率距离影响最小”的场景,同时把被删场景的概率叠加到离它最近的保留场景上。重复直到只剩(N_s)个场景。
快速前向选择(fast forward selection)则反过来,从空集开始,每轮选一个“使场景集和原始场景集的距离下降量最大”的场景加入。它尤其适合最终保留场景数很少(比如5到10个)的场合,因为选择次数少,计算量小,而且不易累积误差。
给一个简化版的Python风格伪代码:
python复制def fast_forward_selection(omega, p, n_target):
# omega: 原始场景集 (N, T)
# p: 原始概率 (N,)
# 计算场景两两之间的距离矩阵 D(欧式或DTW均可)
D = compute_distance_matrix(omega)
selected = []
remain = list(range(N))
# 当前近似场景集为空,用“最中心”的场景初始化
idx_first = argmin(sum(p[j] * D[i, j] for j in remain) for i in remain)
selected.append(idx_first)
remain.remove(idx_first)
while len(selected) < n_target:
best_idx = None
best_gain = -inf
for i in remain:
# 计算把i加入后,Kantorovich距离减少量
gain = compute_distance_gain(D, p, selected + [i])
if gain > best_gain:
best_gain = gain
best_idx = i
selected.append(best_idx)
remain.remove(best_idx)
# 最终削减概率根据Voronoi图重新分配
w = reassign_probabilities(selected, D, p)
return selected, w
这个伪代码要表达的核心是:选择场景只看一件事,它对整体概率距离的贡献有多大。削减后的概率分配则基于每个原始场景最近邻是谁——如果一个原始场景离某个保留场景最近,那它相当于“合并”进后者,概率也一并转移。
从实际性能看,如果原始场景1000个、削到10个,快速前向选择只需要做10次距离计算,很快。如果削到500个,后向削减更合适,因为删除次数多但每次操作简单。
4.4 削减时怎么保住时序相关性
我见过很多人在削减阶段又栽了一次跟头——场景削减时用的距离度量只考虑逐点误差,完全不关心轨迹形态。
举个例子,一条曲线是平稳的400MW,一条曲线是400→200→600→400这样波动的。逐点欧式距离可能把它们判为“相似”,但爬坡特征完全不同。削减完之后,场景集整体看起来平均误差不大,但爬坡率分布被严重压缩,下游优化模型对爬坡约束的校验直接失效。
针对这个问题,我习惯的做法是用一个带一阶差分的距离度量:
[d(\mathbf{x}, \mathbf{y}) = \lambda \cdot |\mathbf{x} - \mathbf{y}|2 + (1-\lambda) \cdot |(\mathbf{x} - \mathbf{x}t) - (\mathbf{y} - \mathbf{y}_t)|_2]
前一项保证逐点误差可控,后一项保证变化趋势相似。λ一般取0.6到0.8,意思是重点看整体数值误差,但也要约束爬坡差异。你也可以直接对原始数据做一阶差分,把差分序列和原序列拼接在一起当作高维向量再聚类,效果类似。
另一个关键点是:削减后的概率重新分配,要基于原始场景权重加权,而不是简单地让(N_s)个场景等概率。原始场景如果本身带权重(比如按季节分层的频率权重),削减后一定得把这些权重带进去,否则边缘分布会偏移。
5. 量化削减效果:用什么指标验收
5.1 概率距离与分布检验
削减完之后,第一件事是看概率距离。用Wasserstein距离比较原始场景集和削减后场景集的差异,看它在数量级上是否可接受。
单维情况下的Wasserstein-1距离有解析形式,但场景是多维时间序列,实际计算要用Sinkhorn近似或者最优传输库。如果你不想引入额外的库,可以退而求其次,逐时段比较累计分布函数,用K-S检验看每个时段的边缘分布是否保持一致。K-S检验的好处是有p值,边界清晰;但它只看边缘分布,看不了时序结构。
5.2 自相关与爬坡特性对比
边缘分布一致不代表时序特征一致。所以我还习惯做两个对比:
- 对比原始场景集和削减后场景集的平均自相关函数(ACF)。对每个场景算ACF,取所有场景的ACF平均值,看削减后ACF的衰减速度是不是和原始一致。
- 对比爬坡率分布。计算相邻时段差值的分布(或者超过某个阈值的事件概率),如果削减后爬坡率分布的尾部被压缩,说明削减过程把动态特性抹掉了。
下面这个表是我常用来做验收的指标集:
| 指标 | 原始场景集 | 削减后场景集 | 是否可接受 |
|---|---|---|---|
| 时段均值均方根误差 | 基准 | 通常<5% | 是 |
| ACF滞后1小时差值 | 基准 | <0.03 | 是 |
| ACF滞后6小时差值 | 基准 | <0.05 | 是 |
| 爬坡率超过阈值概率 | 基准 | 差<0.5个百分点 | 是 |
| K-S检验最大统计量 | 基准 | <0.05 | 是 |
这些阈值是我自己做项目时的经验值,不是标准,但可以参考。如果你的削减后场景在这些指标上差太多,说明削减参数有问题,需要回头调整距离度量的λ、目标场景数等。
5.3 放到优化模型里做终极检验
指标再漂亮,最后还是要看下游任务的表现。我通常会做一次“全场景 vs 削减场景”的对比:先用未削减的原始场景(或者一个大容量场景集,比如1000个)跑一次随机优化,得到目标函数基准值。再用削减后的场景集跑同样的优化,对比目标函数值差异。
这个差异如果能控制在1%到3%以内,说明削减效果可以接受。如果差到5%以上,大概率是场景削减把某些关键时序特征给丢了,需要回到第4节调整削减策略。
以随机机组组合为例,我见过一个案例:未削减时优化目标为1000万元运行成本,削减到10个场景后目标变成1045万元,差异4.5%。看起来还行,但看具体调度方案发现,削减后的场景集完全没体现某天凌晨的大爬坡事件,导致备用容量少配了很大一块。这就是“指标还行、实质有问题”的典型,所以下结论前务必看看削减后场景集中有没有保留关键事件。
6. 一套可落地的流程与参数经验
6.1 从数据到结论的完整步骤
我自己跑这类研究时,梳理出一套固定流程,分享给你参考:
-
数据清洗。剔除坏数据、按时间戳对齐、处理缺失值。很多人一上来就建模,结果脏数据直接污染了协方差矩阵估计。这步做好,后面能省大量返工时间。
-
分布分析。画ACF/PACF图,看数据有没有明显的周期成分;画QQ图判断数据是否接近正态分布。这一眼定方向——数据接近正态就直接用协方差驱动采样,偏态明显就用Copula,数据量大且形状复杂再考虑生成对抗网络。
-
生成原始场景。用第3节的方法生成500到2000个场景。我的经验是:如果最终要削到10个场景,原始场景最好不要少于500个,否则场景多样性不足,削减后更没代表性。我一般采1000个,计算量和代表性比较平衡。
-
削减。先定目标场景数(通常5到20个),用聚类法或快速前向选择,距离度量里带上时序差分项,别忘了把极端场景单独保护。
-
指标校验。跑第5节的指标,重点看ACF和爬坡分布,不能满足就回炉调整。
-
下游验证。放进随机优化模型,对比全场景和削减场景的目标函数差异。
-
迭代。根据下游反馈决定是否增加目标场景数、调整λ参数或换生成模型。
6.2 常见参数的经验取值
| 参数 | 我的经验取值 | 说明 |
|---|---|---|
| 时段数 | 24(小时级)/96(15分钟级) | 按调度需求定,间隔越小相关性越强 |
| 原始场景数N | 500~2000 | 太小多样性差,太大削减耗时 |
| 削减后场景数Ns | 5~20 | 太小丢尾部风险,太大优化太慢 |
| 距离度量λ | 0.6~0.8 | λ越大越看重逐点误差,越小越看重爬坡 |
| 聚类数K | 10~20 | 与Ns大致对应;用轮廓系数辅助选 |
| 极端场景保护数 | 2~5个 | 按研究目的定,保留重要小概率事件 |
这些不是金科玉律,但直接拿去做初值基本不会翻车。
6.3 实际工作里的几个坑
第一个坑:协方差矩阵不满秩。数据量少或者时段数高时,协方差矩阵可能是奇异的,Cholesky分解直接失败。解决方案是加一个小的对角扰动,比如(\Sigma + \epsilon I),(\epsilon)取1e-6到1e-4。
第二个坑:削减后概率需要重新归一化,不是简单地等概率。叠加概率时要按原始场景的权重来,比如原始1000个场景等权重0.001,削减后某个保留场景吸收了50个原始场景,它的概率就应该是0.05左右。有些人想省事直接给每个保留场景等概率,误差一下就上去了。
第三个坑:只看边缘分布不看时序结构。这个问题我反复遇到过,K-S检验全通过,但跑机组组合发现启动次数异常。后来一查,削减后的场景集自相关函数和原始场景差别很大,爬坡事件被完全平均掉了。自从我把ACF和爬坡指标纳入验收流程后,这个问题基本不会再漏掉。
第四个坑:不要迷信生成对抗网络。生成对抗网络不是不好,而是适用场景有限。数据量不够的时候,老老实实统计建模比什么花活都靠谱。
最后再分享一段个人教训吧。我最初做风电场景削减时,第一版模型只检查了削减前后的边缘分布,觉得差异很小就放心往下游走了。结果随机机组组合跑出来一个很离谱的方案,每个小时都在启停机组,调度员看了直摇头。后来我把每个场景单独调出来看,才发现削减后的场景集里爬坡事件全被平均没了。从那以后,ACF和爬坡率分布就成了我验收削减结果不可省的两项指标。做这一类研究,数值上“差不多”是不够的,物理规律上也要说得通。
