光伏电站的出力曲线,做过新能源数据分析的人应该都不陌生:每天从日出开始爬坡,午间达到峰值,傍晚回落归零。表面看起来都是“一座山”,但你要是把一整年的曲线叠在一起看,会发现形态千差万别——晴天是干净利落的单峰,多云天是锯齿状的起伏,阴雨天是又矮又平的“小土丘”。
这篇内容想聊的就是怎么用MATLAB把这些曲线自动分类,核心方法就是用K-means聚类。把每条日曲线当成一个样本,让算法自己找出“晴天类”“多云类”“阴雨类”,并且能直接落地的完整流程。对做光伏功率预测、电站运行分析、调度策略研究的同学来说,这套东西几乎是必备工具。我尽量把预处理、特征工程、K值选择、代码实现和踩坑经历都讲透。
1. 光伏曲线聚类到底在解决什么问题
1.1 聚类结果能用在哪些地方
先说个实际场景。你在做光伏功率预测,最简单粗暴的做法是用历史数据训练一个模型,然后不管明天什么天气,往里一扔就出预测值。结果往往是晴天预测得不错,一到多云天误差大得离谱,阴雨天又出现系统性偏低。
原因其实不复杂——光伏出力曲线在不同天气下的形态差异太大了。晴天的曲线基本是平滑单峰,多云的曲线频繁波动,阴雨天的曲线整体低平。用同一个模型去描述这三种完全不同的规律,本身就是为难它。
如果在建模之前先做一次聚类,把历史曲线分成几个典型模式,然后针对每个模式单独建预测模型,效果会明显改善。这是聚类在光伏领域最常见的应用,也是我最初做这个项目的原因。
聚类结果还有几个很实用的方向:
- 调度与并网:识别出极端天气日(连续阴雨、剧烈波动),提前安排储能充放电策略或备用容量。
- 电站体检:同一区域、同一时段的电站曲线,如果某个电站经常被分到和自己历史模式不同的类别,大概率是设备故障、遮挡或者数据采集问题。
- 典型日选取:做容量规划、仿真计算时不需要几千天的数据,每个聚类簇抽一条中心曲线,就能代表该区域的主要出力模式。
1.2 为什么选K-means而不是其他聚类方法
既然要聚类,为什么一定是K-means?这个选择我是综合考量过的。
光伏日曲线是高维数值型数据。一条按5分钟间隔采样的日曲线有200多个点,K-means处理这种数据非常自然——直接计算欧氏距离,迭代更新簇中心,速度很快,几百上千条曲线几秒钟就能出结果。MATLAB里kmeans函数封装得相当成熟,自带Replicates、MaxIter等参数,写起来几行代码就完事。
对比其他方法:DBSCAN能处理不规则簇形,但对高维数据距离度量敏感,参数调节很麻烦;层次聚类结果直观,但计算复杂度偏高。至少在光伏曲线这个场景,K-means的性价比是最高的。
当然,K-means也有明显的限制——它倾向于发现球形簇,对噪声敏感,而且K值需要自己定。这些问题不是无解的:我们可以在预处理阶段把曲线归一化、剔除异常值,用肘部法则和轮廓系数来确定K,再配合kmeans++初始化策略避免陷入局部最优。后面我会逐个展开。
2. 拿到数据别着急跑算法:预处理与特征工程
2.1 原始数据里的那些坑
很多新手拿到数据,直接把全部曲线丢给kmeans函数,结果跑出来乱七八糟。问题往往不在算法,而在数据本身。
光伏出力数据常见的污染来源:
- 采样间隔不一致:有的电站5分钟一个点,有的15分钟,有的甚至前半年5分钟、后半年改成了1分钟。必须统一重采样到同一时间轴。
- 夜间零值:光伏夜间出力为0,这些点对聚类没贡献,反而拉低距离计算的有效性。建议只截取日出到日落时段,比如夏季的06:00到19:00。
- 缺失和负值:逆变器故障、通信中断会导致整段缺失;电流传感器偏移可能产生微小的负功率读数。负值直接置0或剔除,缺失较多的天数直接从样本里删掉。
- 异常跳变:水平轴上偶尔出现突然冲到满发又在几分钟内回落的“毛刺”,大概率是数据误码,需要用滑动窗口做平滑或设置功率变化率阈值。
我的处理习惯是这样的:先用30天数据画一个堆叠图,肉眼扫一遍整体形态,再看单条曲线是否有明显异常。堆叠图在MATLAB里就是循环plot而已,但这一步能帮你建立对数据的直觉,后面调参会省很大力气。
2.2 归一化怎么做才公平
归一化是光伏曲线聚类里最容易被低估的一步。我见过不少项目,聚类出来的簇中心曲线形状几乎一样,只是高低不同——这就是归一化没做好的典型表现。
同一个光伏电站,夏天晴天的峰值功率可能是10MW,冬天晴天可能只有6MW。如果我们不做归一化,K-means会倾向于把相同量级的曲线聚在一起,从而把“夏季晴天”和“冬季晴天”拆成两个簇,而多云天因为峰值低、整体数值偏低,可能被错误地和阴雨天分到一组。
解决方法是按天归一化,每条曲线都除以当天的峰值(或者除以装机容量)。归一化之后,我们比较的是“形状”,而不是“大小”。这样晴天无论冬夏,曲线都是单峰形态,自然聚到一起;阴雨天无论峰值多低,只要形状相似,都会进同一类。
有一点要注意:如果做跨电站聚类,最好用装机容量做标幺值,而不是按天最大值归一化。因为不同电站的装机容量不同,按最大值归一化之后数值都在0到1,等于丢掉了电站规模信息,但很多分析场景需要保留这个差异。
2.3 特征选择:直接用全序列还是提炼特征
K-means可以直接跑原始曲线,一条157维的向量就是一个样本。这种方法叫“全序列聚类”,优点是信息损失少,缺点是维度高、计算慢、容易引入噪声。
我在实际项目中一般两种都试,但更推荐先用全序列跑一版,再根据业务需要做特征聚类。全序列的结果可以作为基准,特征聚类的优势是解释性强、抗噪能力强。
常用的特征维度包括:
- 峰值大小:一天的最高出力,反映辐照度和天气状况。
- 峰值出现时间:正常情况下在正午前后,若偏移明显可能是云遮挡。
- 日均出力或总发电量:积分面积,反映全天整体资源水平。
- 波动次数:出力曲线在一天内增减剧烈变化的次数,多云天这个数值会显著偏高。
- 平均爬坡速率:单位时间内出力的变化幅度,对储能调度很有参考价值。
特征不是越多越好。特征之间相关性太强等于重复计入权重,我建议先用corrplot看一下特征相关系数,保留相互独立的主成分。如果仓库里有PCA函数,也可以用PCA降到3到5维,再做K-means,效果往往也不错。
3. K值怎么定、距离怎么算
3.1 肘部法则和轮廓系数结合判断
K-means最尴尬的问题就是“你到底想要几类”。这个问题没有绝对标准,但有两种工具组合使用,基本能给出合理的参考区间。
第一种是肘部法则。计算不同K值下的总类内距离SSE,随着K增大,SSE必然下降,但下降的速度会越来越慢。画一条K-SSE曲线,找那个“拐点”——拐点之前下降很快,之后趋于平缓,这个拐点对应的K就是比较合理的值。在MATLAB里可以用silhouette或自己写循环跑kmeans,把不同K的SSE存下来画图。
第二种是轮廓系数(Silhouette Coefficient)。取值范围在-1到1之间,越接近1说明样本和自己的簇越紧密、和其他簇越疏远。MATLAB自带的silhouette函数可以直接返回每个点的轮廓值,取平均就得到整体轮廓系数。
实际操作中我的习惯是:先跑K从2到10,记录SSE和平均轮廓系数,画在一张图里看趋势。选K时如果肘部法则指向4,轮廓系数指向3,我会倾向于取较小的值。光伏曲线的业务解释能力比统计学上的“最优K”更重要,分5个簇却解释不了第4和第5类的区别,这个聚类对业务就是失败的。
3.2 欧氏距离快,但DTW更懂曲线
K-means默认使用欧氏距离,计算效率高,实现简单。但欧氏距离是点对点逐个比较,这意味着它要求两条曲线在时间上严格对齐。
光伏曲线有一个本质特征:每天时序上的相位并不固定。同样是晴天,一片云可能上午10点飘过来,另一天下午2点飘过来,导致曲线的波动位置完全不同。用欧氏距离比较这两条“波形相同但位置不同”的曲线,距离会被拉得很大,算法会认为它们不是同一类。
动态时间规整(DTW)可以解决这种相位偏移问题。它允许曲线在时间轴上“扭曲”后找相似性,非常适合光伏这类具有形变特征的曲线。我在小数据集上测试过,DTW聚类的轮廓系数比欧氏平均高10%左右。
问题在于DTW的计算复杂度是O(n²),几百个点一天还扛得住,几千条曲线循环跑K-means就非常慢了。工程上的折中方案是:先用欧氏距离跑一遍,如果轮廓系数不理想,再把距离度量换成FastDTW近似算法,或者先降采样再计算。只追求精度而忽视算力,在真实项目中是走不远的。
3.3 初始化方式直接影响稳定性
K-means对初始质心非常敏感。初始质心选得不好,很可能收敛到局部最优解,每次跑出来的聚类结果都不一样。这会让结果很难复现,写论文、做报告都不好交代。
解决办法有两个层面。
一是用kmeans++初始化。kmeans++的核心逻辑是让初始质心之间尽量分散,从而显著降低收敛到局部最优的概率。MATLAB的kmeans函数默认使用的就是这种策略,所以不用自己手写。
二是多次重复运行。设置Replicates参数为10或者20,算法会从不同初始点出发跑多轮,最后返回所有轮次中SSE最小的结果。虽然计算时间增加了,但换来的是稳定的结果。
还有一个小技巧:设置随机种子。在调用kmeans之前执行rng(42),可以固定随机过程,让每次运行结果完全一致。写论文的时候这招特别有用,审稿人复现你的结果不会发现差异。
4. MATLAB完整实现与代码详解
4.1 整体框架设计
整个项目我分成了三个模块:数据准备、K值评估、聚类与可视化。这样拆的好处是调试方便,改参数不用从头跑,也方便后续接入真实数据。
- 数据准备:加载光伏日曲线数据,重采样、剔除异常、按天归一化,输出一个
N天 x M点的二维矩阵。 - K值评估:用肘部法则和轮廓系数绘制曲线,结合业务确定K。
- 聚类与可视化:执行K-means聚类,绘制簇中心曲线、各簇样本叠影图和PCA降维散点图。
4.2 模拟数据准备脚本
为了让你能直接跑通整个流程,我用MATLAB生成了三类模拟光伏曲线:晴天、多云、阴雨。每类数据在形状、峰值、波动程度上都有差异,算法分起来比较清晰。
%% 生成模拟光伏日曲线数据 clear; clc; rng(42); % 固定随机种子,保证结果可复现 % 时间轴:06:00 到 19:00,5分钟间隔 t = 6:5/60:19; n = length(t); % 157个采样点 % 基准曲线:钟形,正午达到峰值 base = exp(-((t-12.5).^2) / 8); base = base / max(base); % 每类样本数量:晴天80条,多云60条,阴雨45条 NperClass = [80, 60, 45]; X = []; trueLabels = []; for c = 1:3 for i = 1:NperClass(c) curve = base; switch c case 1 % 晴天:平滑单峰 + 轻微噪声 curve = base .* (0.85 + 0.15 * rand(1, n)); case 2 % 多云:叠加随机波动 ripplePeriod = randi([8, 15]); ripplePhase = 2 * pi * rand; ripple = 0.45 * sin((1:n) / ripplePeriod * 2 * pi + ripplePhase) .* base; curve = base + ripple; curve = max(curve, 0); case 3 % 阴雨:整体低矮 curve = 0.35 * base .* (0.6 + 0.4 * rand(1, n)); end X = [X; curve]; trueLabels = [trueLabels; c]; end end fprintf('数据规模:%d 天,每天 %d 个采样点\n', size(X, 1), n);4.3 确定K值
这段代码会循环K从2到10,计算每种情况下的总SSE和平均轮廓系数,并且画出趋势图。
%% 通过肘部法则和轮廓系数确定K值 Krange = 2:10; SSE = zeros(length(Krange), 1); avgSil = zeros(length(Krange), 1); for i = 1:length(Krange) k = Krange(i); [~, C, sumd] = kmeans(X, k, 'Replicates', 10, 'MaxIter', 500); SSE(i) = sum(sumd); % 轮廓系数 [~, s] = pdist2(C, X, 'euclidean', 'Smallest', 1); % 这里用MATLAB自带silhouette函数更直接 sidx = kmeans(X, k, 'Replicates', 10, 'MaxIter', 500); s = silhouette(X, sidx); avgSil(i) = mean(s); end figure; subplot(1, 2, 1); plot(Krange, SSE, 'o-', 'LineWidth', 1.5); xlabel('K'); ylabel('SSE'); title('肘部法则'); subplot(1, 2, 2); plot(Krange, avgSil, 's-', 'LineWidth', 1.5); xlabel('K'); ylabel('平均轮廓系数'); title('轮廓系数');代码里有个小冗余说明一下:前面跑了一次kmeans取SSE,后面又跑了一次取轮廓系数。实际可以合并成一次调用,同时返回idx和centers。贴这段主要是为了体现思路,正式写的时候建议优化一下。
4.4 执行聚类并可视化
选定K=3后,正式执行聚类,输出簇中心曲线图和各簇样本叠影图。
%% 执行K-means聚类(K=3) K = 3; [idx, centers] = kmeans(X, K, 'Replicates', 20, 'MaxIter', 500, 'Display', 'final'); % 按簇内样本数排序,方便后续解释 [~, order] = sort(histcounts(idx, 1:K+1), 'descend'); centers = centers(order, :); idx = arrayfun(@(x) find(order == x), idx); %% 可视化:簇中心曲线 figure('Color', 'w', 'Position', [100, 100, 1000, 500]); for k = 1:K subplot(1, K, k); hold on; % 该簇所有样本的叠影 samples = X(idx == k, :); for j = 1:size(samples, 1) plot(t, samples(j, :), 'Color', [0.7, 0.7, 0.7]); end % 簇中心曲线 plot(t, centers(k, :), 'r-', 'LineWidth', 2.5); xlabel('时刻'); ylabel('归一化出力'); title(sprintf('簇%d(样本数:%d)', k, sum(idx == k))); xlim([6, 19]); ylim([0, 1.2]); grid on; hold off; end运行这段代码后,你应该会看到三个明显的簇:一个中心曲线高且光滑,对应晴天;一个形状相似但带有明显锯齿,对应多云;一个整体低平,对应阴雨。样本叠影图能直观反映每个簇内部的形态一致性。
4.5 PCA降维散点图
高维曲线没法直接画散点图,但可以先用PCA降到前两个主成分,再按簇标签着色,观察聚类在低维空间的分布情况。
%% PCA降维可视化 [coeff, score] = pca(X); figure('Color', 'w', 'Position', [100, 100, 800, 600]); gscatter(score(:, 1), score(:, 2), idx, 'rgb', 'o', 8); xlabel('PC1'); ylabel('PC2'); title('K-means聚类结果的PCA投影'); legend({'簇1', '簇2', '簇3'}); grid on;如果聚类效果好,你会看到三个点群在低维空间里彼此分离,边界相对清晰。如果散点图上有明显重叠,说明这组聚类结果可能不够稳定,需要重新考虑归一化方式、特征选择或K值。
4.6 快速检验聚类效果的指标
除了可视化,我还会顺手输出一些量化指标,方便写入报告。比如每个簇的样本数占比、簇内平均距离、Davies-Bouldin指数等。轮廓系数前面已经算过,这里补充一个簇内紧密度统计。
%% 聚类效果统计 fprintf('\n===== 聚类结果统计 =====\n'); for k = 1:K samples = X(idx == k, :); center = centers(k, :); dists = sqrt(sum((samples - center).^2, 2)); fprintf('簇%d:样本数=%d,平均欧氏距离=%.4f\n', ... k, size(samples, 1), mean(dists)); end这段统计的价值在于:如果你发现某个簇样本数极少(比如只有一两条),同时平均距离又特别大,那这个簇很可能是异常值包,K值可能选大了。
5. 聚类结果的判读与实际应用
5.1 怎么给每个簇起名字
聚类算法只负责分组,不负责解释。簇分出来之后,我们得根据簇中心的形态和业务知识,给每个簇赋予一个可理解的名字。
以模拟数据为例,结果很典型:
| 簇编号 | 簇中心特征 | 天气类型 | 典型应用 |
|---|---|---|---|
| 簇1 | 峰形高尖,曲线平滑,样本数最多 | 晴天/少云 | 预测精度最高,可简化调度策略 |
| 簇2 | 峰形较完整,锯齿波动明显 | 多云/阵雨 | 预测难度大,需要额外气象数据修正 |
| 簇3 | 整体低平,峰值很低 | 阴雨/连续阴天 | 出力低,储能需提前充电备用 |
命名不是随便贴标签,而是要和气象记录做对照验证。如果手头有当天的气象数据(辐照度、云量、降水),可以抽样核对,看聚类结果和气象记录是否吻合。吻合度高说明聚类有效,吻合度低要先检查数据预处理。
5.2 聚类结果怎么反哺业务
拿到了几个簇之后,接下来的应用就有方向感了。
功率预测方面,最直接的做法是“分簇建模”。用天气数值预报数据判断明天大概属于哪个簇(或者计算明天曲线与各簇中心的距离),然后用对应的预测模型做预测。我看到的很多案例里,分簇预测比统一预测的RMSE能降低5%到10%,在极端天气日提升幅度更大。
调度方面,簇中心曲线就是典型出力场景。比如簇3(阴雨)代表低出力,调度系统可以提前规划储能充电策略,而不是当天临时应对。簇2(多云)波动大,就需要更频繁的AGC调节和爬坡备用。
运维方面,把某个电站历史曲线逐日聚类后,如果某天同类曲线较少,可以调出原始数据排查。遇到过的情况是,数据采集器故障导致曲线形态异常,被聚类算法单独分了出来,反而成了故障预警信号。
6. 常见问题与排查技巧实录
6.1 问题速查表
下面这张表是这些年在光伏曲线聚类上踩过的坑,按出现频率排序,对照排查基本能解决大部分问题。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 每次运行聚类结果不一样 | 初始质心随机导致局部最优 | 设置rng固定种子,增大Replicates |
| 某个簇只有一个样本 | K选得过大,或该样本是异常值 | 减小K值,或者先剔除异常日曲线 |
| 簇中心曲线形状相似,只是高低不同 | 没有按天归一化,量级主导了聚类 | 按天最大值或装机容量归一化 |
| 多云曲线的波动特征被吞掉 | 特征不够敏感,或全序列维度太高 | 增加波动次数、爬坡率等特征 |
| 连续阴雨天被分到多个簇 | K值偏大,导致同类型被拆散 | 参考肘部法则适当减小K |
| 聚类结果和天气记录不符 | 数据时间轴对齐出问题 | 检查时区和采样间隔,统一重采样 |
| 计算速度太慢 | 数据量大且用了全序列特征 | 降维到5维以内,或降低采样分辨率 |
6.2 几个容易忽略的细节
关于归一化的细节再啰嗦一句。不要用全局Min-Max归一化,比如把所有天的曲线除以整个数据集的最大值。这种全局归一化会保留“晴天比阴雨天数值大”的信息,但K-means会优先根据量级分组,而不是形状。光伏聚类的核心是挑形状模式,按天归一化才能把重点放在形态上。
关于聚类的“可解释性”。算法的评价指标再漂亮,如果业务方看不懂每个簇代表什么,项目就很难推进。我每次交付聚类的产出,都会配一张簇中心曲线图和一张典型样本图,用天气场景来解释每个簇的含义。用业务语言翻译算法结果,是这类项目落地的关键步骤。
关于DTW的使用时机。如果样本量不大,比如几百条,在K-means里换成DTW距离矩阵完全可行。MATLAB里可以先算一个DTW距离矩阵,再配合层次聚类或者谱聚类使用。实测在1000条以内数据量上,这种做法的精度提升还挺明显。数据量超过2000条就不建议了,耗时指数级上升,收益反而不明显。
6.3 一个我强烈推荐的小工具
最后分享一个自己一直在用的习惯:聚类跑完别急着走,再做一步“簇中心与真实样本的对比图”。把每个簇的中心曲线和簇内离中心最近、最远的三条样本曲线叠在一起。这张图能快速暴露聚类里的边界问题——如果最远的样本看起来已经非常不像该簇了,说明这个簇太松散了。
这一步不需要写多复杂的代码,就是在前面的叠影图基础上多画几条突出颜色的曲线。但是它对结果判断的帮助非常大,比任何聚类评价指标都直观。我在给电网客户汇报时,也经常直接贴这张图,对方看完基本不需要更多解释就能理解聚类结果。
写在最后的一些体会
这套K-means光伏曲线聚类流程,我在真实电站数据上跑过很多遍。最大的感受是,聚类这门课在学校里学的是算法推导,到了工程里难的是数据预处理和结果解释。同样的代码换个数据集,可能结果就完全不一样,关键还是你对自己手里数据的理解有多深。
如果你手头有真实的光伏数据,我建议拿到数据先做一个全年的日曲线堆叠图,肉眼看完再跑算法。你会发现K-means给你的答案,往往和你第一眼的直觉是吻合的——这个算法最擅长的就是把你能感觉到但说不清的模式,用一条簇中心曲线清晰地表达出来。