ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DBSCAN密度聚类实现风电-负荷场景缩减的Matlab实战

DBSCAN密度聚类实现风电-负荷场景缩减的Matlab实战

做电力系统随机优化的人,大概率都绕不开一个痛点:风电出力和负荷的不确定性如果建模得太“精细”,蒙特卡洛采样出来的场景动辄成百上千,优化模型根本算不动。我自己刚开始做两阶段随机规划的时候,就是被上万条场景曲线喂懵了,求解器直接卡死。后来换成了基于DBSCAN密度聚类的场景缩减方法,总算把问题规模压到了可接受的范围。这篇博文就专门讲清楚这套东西:为啥用DBSCAN、整个场景缩减流程怎么设计、Matlab代码怎么落地、以及我踩过的那些坑。

这篇内容适合正在做风电-负荷场景生成与削减、随机优化调度、或者储能容量规划相关研究的同学参考。如果你对聚类算法有点基础,但不知道怎么把DBSCAN用在一堆时序曲线上,这篇文章可以直接当操作手册用;如果你是零基础,也没关系,我会把每一步的“为什么”讲透,代码给到能直接跑的程度。

1. 风电-负荷场景缩减的核心问题与解决思路

1.1 为什么“场景多”会成为问题

先明确一个概念。在电力系统里,“场景”指的是风电出力曲线、负荷曲线在不同时刻的取值组合。由于风功率的随机性和负荷预测误差,我们很难用一条确定性的曲线去描述未来,只能用概率的方式来描述:蒙特卡洛采样从概率分布中抽取大量样本,每一条样本就是一条场景曲线。

问题在于,抽样数量太少,概率分布的代表性不够,优化结果容易偏保守或者偏冒险;抽样数量太多,随机规划问题的变量规模和约束规模都成倍增长。比如说1000条风电场景和1000条负荷场景组合,即便做了同步回代削减,中间过程产生的计算量依然很可观。尤其在做含储能、多时段耦合的优化调度时,场景数和时段数乘在一起,会迅速撑爆内存。

场景削减的本质,就是从原始的“大而全”场景集中,选出一组“小而精”的代表性场景,使得削减前后的概率分布信息损失最小。常见的做法有几类:基于概率距离的同步回代缩减、基于K-means的聚类缩减、基于蒙特卡洛重要性抽样的缩减,以及本文要讲的基于DBSCAN密度聚类的缩减。

1.2 密度聚类解决的是K-means的“死穴”

同步回代缩减的问题是计算复杂度高,两个场景集的Wasserstein距离需要对所有场景两两匹配,大规模场景下代价太高。K-means的问题在于两点:一是必须事先给定聚类数K,这在实际建模中是个先验信息不太充足的问题;二是K-means擅长处理球状分布,遇到形状不规则、密度不均的场景集合,很容易把本应分开的簇粘在一起,或者把一个簇从中间劈开。

DBSCAN(Density-Based Spatial Clustering of Applications with Noise)走的是另一条路:聚类结构由数据的密度决定,不需要预先指定簇数,还能自动识别并隔离噪声点。

对于风电-负荷场景来说,这个特性非常贴合实际。风电场景的分布往往不是均匀的,某些天气模式下,场景点可能密集地聚在一块区域,而极端场景(比如大风切出、连续无风)则孤零零地落在边缘。这些极端场景在工程上恰恰是需要保留的,但它们很容易被K-means“平均”掉。DBSCAN把密度高且连通的区域划成簇,密度不足的区域直接标记为噪声,这样既能保留极端场景,又能让簇的形状保持真实分布形态。

从最终的随机优化效果来看,场景削减的质量决定了优化解的稳定性。用了DBSCAN之后,削减出的场景在时序上依然能抓住原始集合的主要波动特征,这是我会推荐它的最根本原因。

2. 场景削减整体流程设计

2.1 从蒙特卡洛场景生成到预处理:先把“原料”备清楚

这件事的正确顺序是:先有原始场景集,再做削减。原始场景集一般来自两类途径,一是基于某种概率分布模型(如风电出力的Beta分布、负荷预测误差的正态分布)直接抽样,二是通过蒙特卡洛模拟历史数据的误差序列生成。无论哪种,最后都会得到一个矩阵:行代表场景编号,列代表时序点,值代表该时段的风电出力或负荷大小。

这里有一个很重要的预处理步骤:归一化。风电出力的量纲是MW或p.u.,负荷也是MW,看起来单位一样,但数值范围不同。风电的典型范围是0到装机容量,负荷的范围可能是几百到几千MW,如果不归一化,距离计算会让负荷维度完全“支配”风电维度,聚类结果几乎等于只用负荷信息做聚类,风电信息被压没了。

我在实际代码里用的是Matlab的归一化函数,把每个维度映射到0到1区间:

scenes = [windScenes, loadScenes]; % 每个场景是 wind+load 拼接的行向量 scenesNorm = normalize(scenes, 'range', [0 1]); % 按列归一化到[0,1]

不建议只做 z-score 标准化而不考虑后续距离计算,z-score会把离群值拉得很大,对DBSCAN的epsilon参数更敏感。min-max归一化在这个场景下更稳,前提是你对风电出力的上下限有明确物理约束(0和装机容量),这比数据驱动的min-max更可靠。直接把风光数据除以装机容量得到p.u.值,也是一种有效方案,代码层面更简单。

2.2 DBSCAN聚类的输入与参数预处理

DBSCAN的输入是场景间的距离矩阵。场景数一旦上千,两两距离矩阵就是百万量级浮点数,Matlab处理起来没压力。但如果场景数上万,距离矩阵就会占掉几百MB内存,这时候需要分块计算距离,或者用pdist2配合稀疏化处理。

聚类之前,最关键的一件事是确定epsilon和minpts。很多当时刚上手DBSCAN的人在这里卡住,下文我会单独开一节详细讲参数的选取逻辑。这里先说明它们在流程里的位置:epsilon是邻域半径,决定了“多近算邻居”;minpts是判定核心点的最少邻居数,决定了“多少点能成一个簇”。

在我给一个模拟的500条风电场景集做测试时,先计算了距离矩阵,画出k距离曲线(第minpts近邻距离排序图),观察曲线拐点处大概对应0.08左右的距离值,于是epsilon设成0.08,minpts设为5,效果就很好。这个调参过程不是玄学,本质是看数据的密度分布。

2.3 削减结果如何转化为“代表性场景+概率”

将场景分为若干簇之后,就需要为每个簇选一个代表场景,并为每个代表场景计算对应的概率权重。

代表场景的选取,最简单的做法是在簇内找距离质心最近的实际场景。这个“质心”不是DBSCAN里的概念,而是指簇内所有场景点的均值向量。这样选出来的场景依然是真实可解释的物理场景曲线,而不是一条数学上平均出来、实际不可能发生的曲线。注意,选“离质心最近”的实际场景,而不是直接用均值场景,这点在处理超过出力上限的伪曲线时非常关键。

概率重分配的公式为:

[ p_i = \frac{\text{簇 } i \text{ 的场景数}}{\text{总场景数}} ]

为什么不用簇内场景数的占比?因为蒙特卡洛采样的样本本身是按概率密度进行抽取的,每个样本等概率发生,所以簇的场景数占比就是簇的概率。这是蒙特卡洛+聚类型场景削减的基石。

代表场景集合加上概率权重,就是后续随机优化的输入。执行流程大概如下:

  • 生成或读入原始场景集(N × T维度矩阵)
  • 归一化处理并拼接风电-负荷维度
  • 计算距离矩阵,选定epsilon和minpts
  • 运行DBSCAN获得簇标签和噪声标签
  • 对每个簇提取代表场景,计算概率权重
  • 输出削减后的场景集及概率,供随机优化使用

3. DBSCAN关键参数与算法选型解析

3.1 epsilon和minpts到底怎么定

epsilon(邻域半径)是DBSCAN唯一真正“敏感”的参数,因为它直接决定了哪些点会被视为邻居。epsilon设小了,密度稍低的簇会被拆成碎片,噪声点变多;epsilon设大了,不同的簇可能被“一座桥”连在一起。

最常用的一套经验方法是画k距离图。具体操作如下:对每个场景点,计算它到第minpts个最近邻居的距离,将所有点这个距离按从小到大排序,画出一条曲线。曲线出现明显“肘部”的位置,对应的距离值就是较合理的epsilon。

实际操作里,我自己会把minpts先固定住,再调epsilon。minpts的经验值一般取特征维数的两倍或者直接用5。特征维数是指时序点数量,比如24时段的风电曲线,如果用完整曲线做距离,维度就是24,minpts取5到10这个范围都算常见。需要注意,维度过高时,欧氏距离的意义会被“维度灾难”稀释,这也是后面要讲的问题。

epsilon选好之后,可以先用一个小子集测试聚类效果,观察噪声比例、簇数、以及有没有把明显不同的曲线强行并簇。如果噪声比例过高(比如超过30%),说明epsilon偏小,需要调大;如果所有点都聚成一簇,说明epsilon偏大,需要调小。

3.2 距离度量:标准化欧氏距离是最稳妥的起点

DBSCAN默认的距离度量就是欧氏距离,但场景曲线的距离计算有一个“高级话题”:到底是逐点欧氏距离,还是用动态时间弯曲(DTW)距离?

逐点欧氏距离的假设是曲线每个时刻的取值在空间上对应,风电曲线如果一直有波动,欧氏距离会把“形状相似但时间错位”的场景判得很远。DTW能解决时间错位和曲线伸缩的问题,但计算复杂度很高,即使做了复杂度优化,在5000条曲线以上的场景集中也够你等一阵子的。

做风电-负荷场景削减的时候,我一般建议先用标准化欧氏距离。原因有三:

  • 风电出力数据的“错位”现象并不严重,因为所有场景共享同一时间轴,风功率的时间偏差通常来自预测误差,这种误差依然是同相位比较的;
  • 标准化处理能让距离计算稳定,避免量纲干扰;
  • 欧氏距离配合良好选择的epsilon,已经能在绝大多数情况下给出足够好的聚类质量。等模型结果有问题了,再考虑DTW不迟。

Matlab的dbscan函数支持自定义距离,但大多数情况下,默认的欧氏距离加标准化预处理就已经够用了。

3.3 为什么不是K-means,也不是层次聚类

K-means在很多场景削减文献里被大量使用,原因很朴素:实现简单、速度快、有成熟的评价指标(轮廓系数)。但它的约束是“簇数K要自己给”,这个K和后续随机优化问题的规模直接挂钩——你给K=5,优化就有了5个场景;给K=10,就有了10个场景。到底多少场景能平衡精度和计算量,本身就是一个需要试出来的问题。

DBSCAN的优势是簇数自动生成。只要数据里有明显密度差异,它就能识别出该有的簇数,不需要你预先假设。这个特征在做探索性分析时特别有用:你可以先用DBSCAN看看这些场景数据“天然长什么样”,再回过头决定后续用多少个场景进入规划模型。

层次聚类理论上也能做,而且不要求预设K,但层次聚类需要构造一棵全局合并树,复杂度是O(n²log n),在场景规模上千时,内存和时间开销都很可观。DBSCAN的复杂度虽然也是O(n²)(主要是距离矩阵的构建),但聚类本身速度很快,整体在万级样本内可以接受。

给个简明的对比:

算法是否需要预置簇数簇形状支持噪声处理大规模场景表现
K-means是球状无快
层次聚类否任意无慢
DBSCAN否任意自动隔离中(距离矩阵为主)

3.4 密度聚类与“确定性场景缩减”的关系

“确定性场景缩减”这个表述很关键。它意思是,削减出来的场景是以确定的代表性曲线形式存在,而非继续以概率分布的形式存在。也就是说,经过DBSCAN+概率重分配,得到的是一组“确定的场景曲线+确定的概率值”。

这套输出可以直接套进确定性等价的随机优化模型中:每个代表场景是一条确定性的风电-负荷曲线,权重是确定的概率,模型求解时只需要加权求和,不需要再做场景内部的规划。

相比之下,如果做分布鲁棒优化,你可能还需要为每个簇拟合一个不确定集合。DBSCAN本身也适合做这件事:每个簇可以作为不确定集合的支撑样本,簇中心的统计矩可以用来定义模糊集。这是这套方法在随机优化之外的扩展用法,想深入做鲁棒优化的同学可以留意。

4. Matlab代码实现:从聚类到输出的全流程

4.1 第一步:场景数据生成与预处理代码

场景数据的来源可以是你自己仿真生成的,也可以是历史数据加扰动的结果。我这里给一段示例性代码,用于构造一个可测试的用例:随机生成500条24时段风电场景和500条24时段负荷场景,把它们拼接成N×48的矩阵。

rng(2025); N = 500; % 场景总数 T = 24; % 时段数 % 风电场景:用beta分布模拟出力在0到1之间 windScenes = betarnd(2, 3, N, T); % 负荷场景:基准曲线加正态扰动 loadBase = 100 + 50 * sin((1:T) / T * 2 * pi)'; % 模拟日内波动 loadBase = repmat(loadBase', N, 1); loadScenes = loadBase + randn(N, T) * 10; % 拼接 scenes = [windScenes, loadScenes];

这段代码生成的场景集是模拟的,但结构和你实际拿到的数据完全一致:每一行是一条完整场景。接下来做归一化:

scenesNorm = normalize(scenes, 'range', [0 1]);

这里必须提醒一点:normalize函数默认是按列归一化,也就是对每个时序位置独立做min-max。如果你的风电数据已经缺少极端值(比如某几列全是0.2到0.8之间的值),归一化后依然会映射到0到1区间。对于实际风电数据,更推荐按物理范围归一化,比如所有风电维度统一除以装机容量,所有负荷维度统一除以峰值负荷。这样不会改变不同时序位置的相对关系。

然后计算距离矩阵。数据量在5000条以内时直接两两算就行:

D = pdist2(scenesNorm, scenesNorm, 'euclidean');

如果场景数到了1万以上,建议用pdist然后squareform转方阵,内存占用更高效;再大的话,可以考虑基于KD树近似地做邻居搜索,但Matlab自带的dbscan不支持在计算距离矩阵时省这个开销,需要自己造轮子。

4.2 第二步:k距离曲线选epsilon

在跑DBSCAN之前,先把epsilon选出来。这里给出选参的辅助代码:

minpts = 5; % 对每个点找第minpts近邻的距离 kDist = zeros(N, 1); for i = 1:N distSorted = sort(D(i, :)); kDist(i) = distSorted(minpts + 1); % 第+1是因为自身距离为0占据第一 end kDistSorted = sort(kDist, 'descend'); plot(kDistSorted); xlabel('场景点索引(按距离排序)'); ylabel(sprintf('第%d近邻距离', minpts));

观察绘图结果的肘部拐点。如果拐点不明显,可以尝试调整minpts后重画,一般minpts取2、5、10这三个档位各看一次就能出结果。拐点处对应的纵轴距离值就是epsilon。注意横轴的“场景点索引”不是场景编号,只是排序后的序号,不要误解。

4.3 第三步:调用DBSCAN聚类

Matlab的统计和机器学习工具箱从R2019b开始内置dbscan函数,直接喂距离矩阵或原始数据即可。喂距离矩阵时,参数写法和喂原始数据略有不同。这里给用距离矩阵方式:

epsilon = 0.08; % 按k距离图结果调整 idx = dbscan(D, epsilon, minpts, 'Distance', 'precomputed');

其中idx是一个N×1的列向量,每个元素取值从1到某一最大值,噪声点的标签为-1。

如果你不想用距离矩阵,也可以直接传归一化后的场景矩阵让dbscan内部算欧氏距离,但要记得'Distance'参数保持默认的欧氏距离,结果和上述方式一致。“precomputed”方式的好处是:如果要反复试不同epsilon和minpts,距离矩阵只算一次,后面反复用,能省下大量重算时间。

聚类跑完之后,先做一个快速检查:

unique(idx) % 看簇编号 sum(idx == -1) % 噪声点数量 histogram(idx); % 看簇规模分布

如果显示所有点都是-1,说明epsilon太小或者minpts太大;如果只有一个簇,说明epsilon太大。

4.4 第四步:从簇内选代表场景并计算概率

这是整个方法的核心逻辑实现。完整代码块如下:

function [repScenes, repProbs] = extractRepScenes(scenesNorm, scenes, idx) % 输入: % scenesNorm : 归一化后的场景矩阵,N×dim % scenes : 原始场景矩阵,N×dim(用于输出真实曲线) % idx : dbscan聚类标签,-1为噪声 % 输出: % repScenes : 代表性场景矩阵(原始值),K×dim % repProbs : 概率向量,K×1 clusterIds = unique(idx); clusterIds(clusterIds == -1) = []; % 剔除噪声 K = length(clusterIds); repScenes = zeros(K, size(scenes, 2)); repProbs = zeros(K, 1); for i = 1:K cid = clusterIds(i); inCluster = find(idx == cid); % 簇质心(在归一化空间) centroid = mean(scenesNorm(inCluster, :), 1); % 找离质心最近的真实场景 distToCentroid = sum((scenesNorm(inCluster, :) - centroid).^2, 2); [~, minPos] = min(distToCentroid); repScenes(i, :) = scenes(inCluster(minPos), :); % 概率权重 repProbs(i) = length(inCluster) / sum(idx ~= -1); end end

这个函数有几个细节值得展开讲:

第一,选择离质心最近的原始场景,而不是质心本身。质心可能是一条不会实际发生的“平均曲线”,用于分析可以,用于优化建模会导致一些约束的可行性判断失真。选离质心最近的原始场景,既能代表这个簇的平均状态,又能保证它是一条真实可发生的物理曲线。

第二,概率权重用了剔除噪声点之后的基数。这里的逻辑是:噪声点是无法被平滑归入任何簇的孤立场景,在工程上要么是极端情况,要么是采样异常。在做代表性削减的时候,我们保留簇内场景的占比作为概率,噪声点相当于“不进入代表性集合”,但这不等于要扔掉它们的信息——如果噪声点过多,说明原始场景生成模型本身有异常波动,需要回去检查参数。

第三,函数返回的代表场景是原始值(用了scenes而不是scenesNorm)。后续做随机优化时,直接使用这些原始值建模,不需要再去还原归一化。

4.5 第五步:削减效果的评价指标

削减完了,不能光看聚类图说“效果不错”,要有量化指标。我推荐两个常用指标:

第一个是削减前后的经验分布差异,用Wasserstein距离或KL散度近似估计。对于离散场景集,Wasserstein距离可以近似为:

% 简单近似:按场景顺序配对计算 function wdist = wDistApprox(setA, setB) wdist = mean(vecnorm(setA - setB, 2, 2)); end

如果是原始场景集和代表性场景集(数量不一致),可以先在代表性场景集上做蒙特卡洛重采样,使之达到相同样本数,再计算距离。也可以用分布层面的距离函数,Matlab的pdist2仍然够用。第二个指标是削减后优化结果与原始场景集优化结果的偏差,如果偏差在可接受范围内,说明削减质量高。

我在实际中还会用生成“日总出力累积分布”对比图:把原始1000条场景的日总风电出力直方图,和削减后场景按概率加权的柱状图叠在一起看,直观判断分布形状有没有被破坏。这也是给导师或项目汇报时最有说服力的图。

4.6 一个完整的可运行脚本串联

为了让你能快速跑起来,这里把上述过程组装成一个完整的脚本:

%% 场景削减主脚本 clear; clc; close all; rng(2025); N = 500; T = 24; %% 1. 生成模拟场景 windScenes = betarnd(2, 3, N, T); loadBase = 100 + 50 * sin((1:T) / T * 2 * pi)'; loadBase = repmat(loadBase', N, 1); loadScenes = loadBase + randn(N, T) * 10; scenes = [windScenes, loadScenes]; %% 2. 归一化 scenesNorm = normalize(scenes, 'range', [0 1]); %% 3. 距离矩阵 + 选epsilon D = pdist2(scenesNorm, scenesNorm, 'euclidean'); minpts = 5; kDist = sort(sort(D, 2), 1); figure; plot(sort(kDist(:, minpts), 'descend')); %% 4. DBSCAN聚类 epsilon = 0.08; % 按k距离图人工确认 idx = dbscan(D, epsilon, minpts, 'Distance', 'precomputed'); %% 5. 提取代表场景和概率 [repScenes, repProbs] = extractRepScenes(scenesNorm, scenes, idx); %% 6. 输出简报 fprintf('原始场景数: %d\n', N); fprintf('削减后场景数: %d\n', length(repProbs)); fprintf('噪声场景数: %d\n', sum(idx == -1));

这段脚本在500条模拟场景下,跑完只需要几秒钟。重点在epsilon那行,需要根据第3步的k距离图人工调整,我不建议完全不看图就设参数。

5. 参数调优、常见问题与避坑经验

5.1 epsilon选不准,聚类结果两极分化

DBSCAN最容易踩的坑就是epsilon。我在一个包含历史风电出力的真实数据集上测试时,epsilon设0.06得到12个簇,设0.09直接变成3个大簇,中间差异巨大。遇到这种情况,不要在一个值上死磕,而是要做“epsilon扫描”:在k距离图拐点左右各取几个值分别聚类,观察簇数和噪声比例的变化趋势。

场景削减应用里有一个实用经验:簇数不应该太少,否则代表性会不足;也不应该太多,否则削减意义不大。根据我的测试经验,500条原始场景削减到15到30个代表场景是比较合理的区间。如果epsilon扫描结果在这个区间内,就不用再精细调了。如果原始场景数是1000条,对应的合理簇数大概在20到50之间,这个比例可以作为一个粗略参考。

5.2 minpts对聚类稳定性的影响

minpts增大时,聚类会变得更加“苛刻”,因为一个点要拉更多邻居才能成为核心点,这会导致簇数减少、噪声增加。minpts减小时,簇数会增加,但聚类的抗噪性下降,容易出现一两个点自成一簇的情况。

实践经验里,minpts取5是个分水岭。大于5时,聚类结果通常更稳定;小于5时,容易出现小碎簇。对于场景曲线来说,我觉得minpts=5到10是比较合理的。但要注意,特征维度很大的时候(比如96时段曲线),minpts可能需要适当调大一些,让簇的形状更平滑。一种快速判断方式是:如果聚类出来的簇中,某些簇只有一个场景,就说明minpts太小了。

5.3 归一化方式不当,聚类被量纲带偏

这可能是比epsilon更隐蔽的坑。如果你跳过归一化直接聚类,风电出力范围在0到100MW,负荷在500到1000MW,距离就会被负荷的波动完全主导。DBSCAN找到的邻域关系,可能仅仅反映了负荷相近的场景,而风电的差异被无视。

所以再强调一遍:聚类前务必先归一化。要么按物理边界统一除以容量得到p.u.值,要么做全维度min-max归一化。这两种我都试过,p.u.化之后有个额外好处是,你可以对风电和负荷维度分别设置不同的权重,比如认为风电不确定性更重要,就给它乘2的权重,再把两者拼接起来。这在min-max归一化里反而不太好操作。

5.4 噪声场景怎么处理:是丢弃还是保留

很多DBSCAN用户会纠结噪声点怎么办。从场景削减的工程角度看,我的建议是:先看噪声比例,再决定。如果噪声比例低于总场景数的5%,直接忽略它们,代表性场景的概率按非噪声场景重新归一化即可。如果噪声比例超过了20%,这就不能当噪声看了,说明原始场景的采样模型有问题,或者epsilon选得太小。

极端场景(比如风电为零的连续静稳场景)在DBSCAN中有可能被判成噪声,有一个可行的处理方式是单独把这些极端场景显式加进代表性集合,再为它们分配小权重。这样既利用了DBSCAN的密度结构,又保住了调度问题必须覆盖的极端工况,这种“密度聚类+人工接管”的混合模式是我比较推荐的做法。

5.5 距离矩阵内存溢出怎么办

当场景数上了5千,距离矩阵大小是5000×5000,double类型占200MB;到1万就是800MB。即便现代电脑内存还能扛,运算时间也明显增加。两个解决方向:

一是分块计算距离。比如每次只计算某几个子集的pdist2,聚类时用近似算法。Matlab的dbscan没有内置分块选项,但可以直接用knnsearch算邻居,再手动实现DBSCAN核心逻辑(这需要自己写队列BFS,代码不复杂,但这里不展开)。

二是先用粗粒度方式把场景数压到合理范围,再跑DBSCAN。比如先用K-means粗聚类到1000条,再在这个基础上做DBSCAN精细削减。这样距离矩阵规模就在可控范围内,同时利用了DBSCAN的自动定簇特性。我做过测试,这种两级削减方案能在保证质量的前提下处理数万条场景。

5.6 一个隐蔽的坑:归一化里的信息泄漏

如果你对场景集整体做min-max归一化,这里有个隐患:当新增场景或更新数据时,如果还按旧数据的min-max范围去归一化新数据,新场景可能超出[0,1]范围;如果重新对整个数据集做归一化,则旧聚类结果无法复用。

解决方法:在实际工业应用中,我会先从历史数据里确定风电出力最大值、负荷峰值的物理上界,以此作为固定归一化参数。这样无论数据集怎么换,归一化后的空间是稳定不变的,聚类结果可复现、可增量式更新。这虽然不算一个高深的技巧,但在项目落地时非常管用。

6. 从聚类到优化:后续衔接的几点经验

6.1 代表性场景如何进入随机优化模型

最后再说说削减完的成果怎么用。把代表性场景的时序曲线按概率加权,代入目标函数时,每个场景对应一套决策变量组合。对于两阶段随机优化,第一阶段决策(比如机组启停)必须对所有场景一致,第二阶段决策(比如实际出力调整)可以随场景变化。

用DBSCAN削减后,每个场景对应一个“簇概率”,模型里需要给每个代表性场景分配一个乘子。这一点和K-means削减后的处理完全一致,没有任何额外的复杂性。如果你是新手,可以先拿一个简单经济调度模型试,把场景数从几百个压到20个左右,对比结果曲线的包络是否还完整。

6.2 场景削减的稳定性检查

削减算法的稳定性检查,是指对同一原始场景集,多次运行聚类(随机初始化参数不变时DBSCAN是确定的,不需要担心),或者对不同的随机采样得到的两组场景集分别做削减,检验输出代表场景的分布是否一致。如果结果漂移明显,说明你的epsilon和minpts对样本波动太敏感,需要重新审视参数。

我在实际中用一个简单方法做稳定性检验:把原始场景集分成两组子集,分别做削减,然后对比两组代表场景的日总出力CDF差异。如果两组CDF的差距在可接受误差内,说明这套参数是稳的。DBSCAN的天然优势在于它是确定性算法,给定同一距离矩阵和参数,结果完全可复现,这一点比K-means(受初始点影响)更让人放心。

6.3 有没有比DBSCAN更适合场景削减的变体

如果你场景集里既有风电又有负荷,且两者相关性明显,直接拼接再聚类是一种方式;另一种更高级的做法是先做PCA降维,提取前几个主成分再聚类。PCA让DBSCAN在低维空间工作,不仅速度快,对epsilon的敏感度也下降。我在数据处理里会把风电和负荷曲线拼好之后,先pca降维到前10到20个主成分再聚类,效果一般都不错。但代价是解释性变差:簇的物理含义变得不太好说明。

还有一类变体是HDBSCAN,它用层次密度对epsilon做了自适应处理,理论上不用调epsilon。Matlab没有原生实现,需要自己找第三方代码或者迁移到Python。如果你对Matlab限定没那么死,可以考虑在Python的scikit-learn里用HDBSCAN做对比实验。插一句,Python生态里场景聚类的库确实更全,Matlab的优势主要是内置函数和矩阵运算方便。

6.4 一个可扩展的改进方向

当前这套确定性缩减方法,还可以在概率分配上进行平滑改进。比如一个簇内的场景虽然被压缩成一个代表场景,但簇内的方差信息被完全丢弃了。如果你想保留一些不确定性的轮廓,可以把每个簇的协方差矩阵导出,作为分布鲁棒优化里的不确定集合形状。

这个思路在风电场景削减的文献里有不少变体,具体做法是:用DBSCAN识别簇,再对每个簇拟合一个椭圆不确定集,优化模型里多一组对不确定集内最坏情况的鲁棒约束。好处是把聚类的结构信息用得更充分,但模型求解复杂度也会显著增加。适合后续做进阶研究的同学尝试,我自己的项目里也正在往这个方向走。

7. 写在最后

说几句实在话。场景削减这件事,公式和算法看起来不难,真正花时间的永远在数据预处理和参数调优上。我用DBSCAN做风电-负荷场景缩减已经有几个项目的积累了,最深的体会是:先把场景的物理边界理顺,再谈聚类算法;先把归一化做好,再谈epsilon选多少。很多文章会把重点放在算法原理上,但实际工程中,离聚类效果影响最大的恰恰是那些不起眼的细节。

如果你在实现过程中遇到聚类结果忽好忽坏、噪声比例过高这类问题,建议先回到数据归一化和k距离图这两个环节检查,绝大多数问题都能在那里找到原因。这套DBSCAN方案虽然不复杂,但它让我在处理几百上千条场景时,第一次觉得随机优化的规模不再是瓶颈。希望这篇记录也能帮你少走几步弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进