做光伏电站数据分析一年半,我踩过最大的坑就是拿到SCADA系统里几十万条功率曲线后,急着把模型往上一扔就开始训练。后来发现,不管做超短期光伏功率预测、异常检测还是储能充放电策略,第一步都应该先把历史数据里的“典型天气形态”捞出来。我的做法是把每天的功率曲线当成一条时间序列,用K-means聚类算法自动归纳出晴天、多云、阴雨这几类典型形状,再把这个聚类结果作为上游特征喂给下游任务。这篇博文就是这套流程的完整复盘,覆盖数据清洗、特征工程、K值选择、效果评估和落地代码,并附上我在实际项目中踩过的几个坑。
1. 项目整体思路与方案设计
1.1 光伏时间序列聚类的三类典型业务场景
光伏功率时间序列之所以值得做聚类,因为它几乎是所有上层应用的“地基”。第一个场景是超短期功率预测。如果直接用全量历史数据训练预测模型,晴天和阴天的曲线形态会互相干扰,模型学到的混合分布既不像晴天也不像阴天。但按聚类得到的天气类别分别建模,或者把类别标签作为外部特征输入,预测误差会有肉眼可见的下降。
第二个场景是光伏电站运行异常检测。聚类得到的质心曲线就是典型行为画像,某天的实际出力曲线偏离本类质心超过一定阈值,就说明可能有组件脏污、逆变器限功率或者通信丢数的问题。这比单纯设定上下限要灵活得多,因为每一类天气的“正常范围”是完全不同的。
第三个场景是光储联合调度。说白了,储能什么时候充、什么时候放,取决于明天是晴天还是阴天。聚类得到天气类型后,调度策略就可以分场景预设:晴天类多储多发,阴雨类少储少发。这和逆变器侧选择不同的MPPT控制策略、并网控制参数是同一套逻辑——先认天气,再谈控制。
1.2 为什么是K-means,而不是DBSCAN或层次聚类
很多朋友一上来就问我,为什么不试试DBSCAN?说实话,我在这个项目里做过对比。DBSCAN最大的问题是它基于密度的假设,而光伏功率时间序列在经过归一化之后,不同天气类别的密度差异非常大,阴雨天的样本点分布稀疏,往往被当成噪声丢掉。层次聚类倒是能给出完整的层次关系,但日均几千条曲线时,距离矩阵的内存开销就非常难看了,运维老哥看了直接摇头。
K-means在这个场景下胜出的核心原因有三点:
- 计算复杂度是O(n·k·t),n是样本数,k是类别数,t是迭代次数。几万条日曲线跑下来也就几十秒,不需要GPU。
- 输出的是硬分类,每一条曲线只属于一个类,业务上解释起来非常干脆——“今天是多云类”比“今天是多云类概率60%、晴天类30%”更容易被运营团队接受。
- K-means假设簇近似球形,而光伏典型天气曲线在经过特征工程和归一化之后,样本分布相对紧致,基本满足这个假设。
当然,K-means的短板也很明显:对初始质心敏感、K值不好确定、原始高维曲线直接聚类容易失效。这些痛点我在后面都给出了对应的解决方案。
1.3 聚类不是终点,而是特征工程的一环
我必须强调一个容易被忽略的定位:聚类本身并不直接产生业务价值,它的价值在于为后续任务提供更干净的输入。在我的项目里,聚类标签被用于三类下游任务:第一类是把每个聚类的质心曲线作为典型日曲线库,用于超短期预测模型的特征拼接;第二类是把样本到本类质心的距离作为异常分数;第三类是把“天气类型转移矩阵”用于日内滚动预测,比如今天聚类结果是晴天,明天的超短期预测就优先参考历史晴天类的出力规律。
想清楚这个定位,后面所有设计决策就都有了明确的判据:聚类效果好坏的最终标准,是下游预测误差是否下降、异常检出是否合理,而不是某一个人工指标是否漂亮。
2. 数据预处理与特征工程:决定聚类效果的两块基石
2.1 原始数据清洗:先去掉夜晚的“假零”
光伏电站SCADA系统返回的数据一般是15分钟一条,一天96点。第一条要处理的坑是夜间和凌晨的数据几乎全是0值。如果直接把96个点全部扔进K-means,欧氏距离会严重看向后半夜那一堆零值,聚类结果大概率变成“零值多”和“零值少”的划分,而不是天气形态的划分。
我的做法是先切出有效发电区间。用辐照度阈值判断每天的实际发电时段,比如辐照度连续低于30W/m²的时间段直接裁掉。不同季节日照时长不同,所以还要把裁出来的曲线统一插值到同一个长度,比如48个点。这一步做完,数据量从96维降到48维,而且每一条曲线都是从“早上升功率”到“晚上降功率”的完整有效形态。
另外一个细节是坏数的识别。如果某一天有效数据点不到总长度的三分之二,或者全天最大功率低于装机容量的5%,这条曲线直接丢弃。补出来的数据形态是失真的,补了反而污染聚类质心,丢掉是最省心的选择。
注意:清洗的目标不是数据量最大化,而是样本形态保真。光伏聚类对脏数据的容忍度非常低,一条坏曲线混进晴天类,质心曲线就会被拉歪。
2.2 特征工程:不能只取统计量,也别裸用整条曲线
业内做时间序列聚类通常有两条路线。一条是直接对原始曲线聚类,但48维甚至96维的数据在欧氏距离下区分度很差,特征空间太稀疏,聚类结果会非常不稳定。另一条是只取统计特征,比如日均功率、峰值功率、峰值时刻、总发电量,这类特征能区分晴天和阴天,但完全分不清“上午晴下午阴”和“上午阴下午晴”。
我最后采用的特征向量是“归一化曲线+统计特征”的双通道方案。以48点有效曲线为例,特征向量一共53维:
- 48维归一化曲线,把曲线形态完整保留下来;
- 1维峰值功率,代表当天最大出力水平;
- 1维平均功率,代表整体发电量水平;
- 1维波动率,用相邻点差绝对值之和除以峰值功率,用来描述云遮挡造成的抖动幅度;
- 1维峰值出现时间占比,用来区分上午峰值和下午峰值;
- 1维正午前后对称性指标,直接计算上午均值和下午均值的差值比。
这里最特殊的是保留归一化曲线。原因很简单,晴天曲线的“单峰拱形”和阴天曲线的“平台形”之间的差异在统计量上未必显著,但欧氏距离能够捕捉。归一化的方式是每个样本除以自身最大值,这样形态可以放在同一尺度下比较,而峰值功率和平均功率这两个统计特征负责表达“大小”差异。两者互补之后,K-means的聚类结果才真正具备天气语义。
实操心得:归一化这里的坑是“样本内归一化”和“样本间归一化”容易搞混。只做样本内归一化,聚类看的是形态;只做样本间归一化,看的是绝对大小。通常应该是样本内归一化让曲线形态对齐,再把峰值功率等统计量作为额外维度重新参与标准化。两边信息都不能丢。
2.3 平滑与重采样:抑制云遮挡尖刺
光伏曲线最常见的问题是云遮挡导致功率剧烈抖动,比如原本平缓的晴天曲线上突然出现几个刺。这些高频尖刺对聚类形态影响很大,处理方式是对每条曲线做滑动平均,窗口选3个点(45分钟)。窗口太大形态会被抹平,太小又起不到抑制尖刺的作用。实测下来,3点窗口对云遮挡抖动的抑制效果最好,同时不会破坏晴天曲线的峰值形状。
另外,重采样时我建议统一采用线性插值,不要用多项式插值。光伏日曲线形状接近多个分段线性段,多项式插值容易在端点产生龙格现象,引入不存在的形态特征。
3. K值选择与聚类优化:从肘部法则到业务校准
3.1 手肘法则和轮廓系数要组合看,不能只看一个
K值选择是所有聚类项目里最让人头秃的问题。我在项目里用了三层方法,第一层是手肘法则,画出K从2到8的SSE(簇内误差平方和)曲线,找下降趋势的拐点。光伏数据的SSE曲线通常在K=3或K=4时出现明显拐点,因为天气类型本身就大概分成晴、多云、阴雨三大类。
第二层是轮廓系数,取轮廓系数最大的K。但这个指标有个毛病,它经常会评分K=2,因为两个大簇分开时轮廓最明显,却忽略了内部的天气细节。所以第三层就是业务校准——把K=3、4、5时的聚类中心曲线画出来,和气象站的实际天气记录对比。
3.2 K值的业务语义:3类可解释,5类更适合预测
我实际测试下来,K=3时聚类结果非常干净,对应晴、多云、阴雨三种天气形态,解释性极强,适合做业务汇报和规则说明。但做超短期功率预测时,我发现K=5效果更好,因为K=3把“强晴天”和“一般晴天”混在了一起,而这两者的爬坡率差别很大,混在一起会导致预测模型在强晴天的中午时段系统性偏低。
K=5的类别会自然拆成“强晴天”“一般晴天”“晴间多云”“阴天”“雨天”。代价是部分相邻类别之间的轮廓系数下降,样本分割变细。所以在我的项目里,如果是做调度策略解释,用K=3;如果是做超短期预测的输入特征,用K=5。
3.3 优化手段:K-means++、多次重启和Mini-Batch
K-means对初始质心敏感是出了名的,我的处理方案有三个:
- 初始化方式务必要用K-means++,让初始质心彼此尽量拉开,避完全随机初始化导致落入局部最优。
- 设置
n_init=30,跑30次随机初始化,保留SSE最小的那一次。在几条万条曲线的规模上,这个成本可以忽略,但稳定性提升很大。 - 如果数据量到百万级别,可以切到Mini-Batch K-means,每次迭代只采样一小批样本更新质心。实测下来200万条曲线、K=5,Mini-Batch K-means比标准K-means快了近一个数量级,聚类中心曲线基本一致。
实操心得:做聚类一定要固定随机种子。我在项目里固定
random_state=42,否则每次跑出来类别序号都是乱序的,比如今天1号类是晴天,明天1号类变成阴天,下游自动化的规则就全乱了。
4. 聚类评估与结果分析:指标只是及格线,业务验证才是加分项
4.1 结构化指标怎么看
评估聚类效果我用三个指标一起看,不单独看某一个:
- 轮廓系数(Silhouette Coefficient):范围-1到1,光伏数据K=3时一般能到0.45-0.55,说明簇内紧致、簇间分离都尚可。低于0.3说明特征工程可能出了问题。
- Davies-Bouldin指数(DBI):越小越好,光伏数据K=5时0.7左右是正常状态。如果超过1.0,说明类间重叠严重。
- Calinski-Harabasz指数(CH):越大越好,这个指标对簇数不敏感,可以配合轮廓系数做交叉参考。
三个指标之间如果出现矛盾(比如轮廓系数说K=2好,CH说K=6好),不用纠结,这时候以业务语义和下游任务表现为准。
4.2 业务验证:让聚类结果自己“说话”
指标只能证明聚类在数学上合理,但我要的是聚类结果能解释普遍规律。我常用的业务验证方法是交叉表。把聚类的类别标签和气象站当天实际天气记录做交叉统计,比如聚类出5类,天气记录有“晴、多云、阴、小雨、大雨”5类,一个高质量的聚类结果会让交叉表呈明显的对角占优。
另一个方法是画每类的平均曲线和标准差范围带。平均曲线应该形似典型的天气功率曲线,标准差带则反映类内样本的离散程度。如果某一类的平均曲线形态诡奇怪异,比如上午出现横线,多半是特征工程里有问题,需要回头检查。
4.3 稳定性评估:换个随机种子结果不能崩
最后一定要做的评估是稳定性。我用Adjusted Rand Index(ARI)来比较不同随机种子下聚类结果的一致性。同一份数据,随机种子换一换,ARI应该接近0.9以上;如果两次聚类结果ARI只有0.5,说明数据本身结构不够清晰或者特征区分度不足,聚类结果缺乏可复现性,下游自动化任务会非常痛苦。
4.4 聚类结果如何落地到预测与异常检测
聚类尘埃落定之后,我建议把聚类中心提取为“典型天气曲线库”,这个库可以做很多事情:
- 超短期预测:把“当前时刻功率曲线最接近哪个质心”作为离散特征输入,或者按类分别训练多个轻量预测模型,推理时先分类再预测。
- 异常检测:计算新一天曲线到本类质心的动态时间弯曲距离或欧氏距离,超过类内距离分布的95分位数就告警。
- 逆变器控制:识别出多云高波动类之后,提前把MPPT的扰动步长调小,或者在并网控制的功率平抑策略里切换更适合波动场景的参数。这是光伏板到逆变器到储能到配电箱整条链路里,聚类结果直接参与控制决策的典型方式。
5. 完整代码实战:从模拟数据到聚类评估的一次跑通
5.1 代码结构总览
这一节给出一份可直接运行的完整代码。为了演示流程,我用numpy生成三条基线的模拟光伏日功率曲线,合成了1000条样本,其中45%晴天、30%多云、25%阴雨。真实电站SCADA数据的接入方式是对应位置替换成读取数据文件的步骤。整个流程包括数据生成、有效区间切分、特征构建、K值评估、聚类与评估、典型曲线输出六个部分。
5.2 数据生成与有效区间切分
import numpy as np import pandas as pd from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler from sklearn.metrics import (silhouette_score, davies_bouldin_score, calinski_harabasz_score, adjusted_rand_score) import matplotlib.pyplot as plt # ---------- 1. 模拟光伏日功率曲线 ---------- np.random.seed(42) days = 1000 # 模拟1000天 points = 96 # 15分钟采样,一天96点 # 真实标签:0晴天,1多云,2阴雨 true_labels = np.random.choice([0, 1, 2], size=days, p=[0.45, 0.3, 0.25]) time_idx = np.linspace(0, 1, points) def base_curve(c): t = time_idx if c == 0: # 强晴天:单峰拱形 curve = np.clip(np.sin((t - 0.22) * np.pi * 1.05), 0, 1) ** 1.1 peak = np.random.uniform(0.9, 1.0) elif c == 1: # 多云:波形叠加高频扰动 curve = np.clip(np.sin((t - 0.22) * np.pi * 1.05), 0, 1) ** 1.3 peak = np.random.uniform(0.55, 0.8) wave = 1 + 0.4 * np.sin(7 * t + np.random.uniform(0, np.pi)) curve *= wave * np.random.uniform(0.8, 1.2) else: # 阴雨:低矮平缓 curve = np.clip(np.sin((t - 0.22) * np.pi * 1.05), 0, 1) ** 2.0 peak = np.random.uniform(0.2, 0.5) curve *= np.random.uniform(0.7, 1.0) curve += np.random.normal(0, 0.03, size=points) curve = np.clip(curve, 0, 1) return np.round(peak * curve, 4) data = np.array([base_curve(c) for c in true_labels]) df = pd.DataFrame(data, columns=[f"t{i}" for i in range(points)]) df["true_weather"] = true_labels # ---------- 2. 切分有效发电区间 ---------- # 这里用索引模拟“8:00-17:00”,对应32到68点 work_raw = df.iloc[:, 32:69].values # 每天37点 valid_mask = work_raw.mean(axis=1) > 0.05 work_curves = work_raw[valid_mask] true_labels_valid = true_labels[valid_mask] print("有效样本数:", work_curves.shape[0])5.3 特征工程:形态特征与统计特征拼接
# ---------- 3. 特征工程 ---------- # 3.1 样本内归一化,保留形态特征 peak_val = work_curves.max(axis=1, keepdims=True) norm_curves = work_curves / (peak_val + 1e-9) # 3.2 统计特征 peak_series = peak_val.ravel() mean_series = work_curves.mean(axis=1) diff_sum = np.abs(np.diff(work_curves, axis=1)).sum(axis=1) volatility = diff_sum / (peak_series + 1e-9) peak_time = np.argmax(work_curves, axis=1) / work_curves.shape[1] # 正午前后对称性:上午均值和下午均值差比 half = work_curves.shape[1] // 2 symmetry = (work_curves[:, :half].mean(axis=1) - work_curves[:, half:].mean(axis=1)) / (mean_series + 1e-9) X = np.hstack([norm_curves, peak_series.reshape(-1, 1), mean_series.reshape(-1, 1), volatility.reshape(-1, 1), peak_time.reshape(-1, 1), symmetry.reshape(-1, 1)]) # 3.3 标准化:所有特征缩放到同一尺度 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) print("特征矩阵维度:", X_scaled.shape)# ---------- 4. K值评估(肘部法则 + 轮廓系数) ---------- sse_list, sil_list = [], [] K_range = range(2, 9) for k in K_range: km = KMeans(n_clusters=k, init="k-means++", n_init=30, random_state=42) y = km.fit_predict(X_scaled) sse_list.append(km.inertia_) sil_list.append(silhouette_score(X_scaled, y)) # 输出K值评估表 for k, sse, sil in zip(K_range, sse_list, sil_list): print(f"K={k} SSE={sse:.1f} 轮廓系数={sil:.4f}")运行这段代码,输出会类似:
K=2 SSE=5214.3 轮廓系数=0.5832 K=3 SSE=3891.5 轮廓系数=0.5136 K=4 SSE=3224.7 轮廓系数=0.4619 K=5 SSE=2655.2 轮廓系数=0.4431 K=6 SSE=2311.4 轮廓系数=0.4123手肘的位置出现在K=3,因为天气类型天然是三分类。虽然轮廓系数在K=2时更高,但结合业务需求,K=3是“晴天/多云/阴雨”,可解释性最好。
5.4 聚类与多维度评估
# ---------- 5. 最终聚类与评估 ---------- K = 3 km_final = KMeans(n_clusters=K, init="k-means++", n_init=30, random_state=42) y_final = km_final.fit_predict(X_scaled) # 结构化指标 sil = silhouette_score(X_scaled, y_final) dbi = davies_bouldin_score(X_scaled, y_final) ch = calinski_harabasz_score(X_scaled, y_final) print(f"轮廓系数={sil:.4f}, DBI={dbi:.4f}, CH={ch:.1f}") # 稳定性评估:不同随机种子聚类一致性 from sklearn.metrics import adjusted_rand_score km_again = KMeans(n_clusters=K, init="k-means++", n_init=30, random_state=2024) y_again = km_again.fit_predict(X_scaled) ari = adjusted_rand_score(y_final, y_again) print(f"两次随机种子聚类ARI={ari:.4f}") # 交叉表:聚类标签 vs 真实天气标签 cross = pd.crosstab(y_final, true_labels_valid, rownames=["聚类标签"], colnames=["真实天气"]) print(cross)典型输出如下表所示(模拟数据的随机种子固定,每次结果可复现):
| 聚类标签 | 0(晴天) | 1(多云) | 2(阴雨) |
|---|---|---|---|
| 0 | 435 | 12 | 0 |
| 1 | 8 | 278 | 5 |
| 2 | 0 | 6 | 243 |
这张交叉表说明聚类结果和真实天气的匹配度很高,0号类主要对应晴天,1号类主要对应多云,2号类主要对应阴雨。
5.5 提取典型天气曲线库
# ---------- 6. 绘制每类的典型出力曲线 ---------- fig, ax = plt.subplots(figsize=(10, 4)) for i in range(K): members = work_curves[y_final == i] mean_curve = members.mean(axis=0) ax.plot(mean_curve, label=f"聚类{i}典型曲线") ax.legend() plt.title("三类别典型光伏出力曲线") plt.show() # 储存质心曲线到csv,后续预测模型直接读取使用 pd.DataFrame(work_curves[y_final == 0].mean(axis=0)).T.to_csv("cluster_0.csv", index=False)这一步输出的三张典型曲线图,应该分别对应“高而窄的晴天单峰”“中等高度的波动型曲线”“低而平的阴雨型曲线”。如果符合这个直觉,说明特征工程没问题。
6. 常见问题与排查技巧实录
6.1 聚类结果被“零值多少”主导,而不是天气形态
如果你发现聚类结果里普天气型形貌模糊、几十个簇中心的差异只在绝对功率大小,最可能的原因是有效区间切分不到位或者夜间零值没有被裁干净。我排查时会先画几条不同簇的平均曲线,如果发现曲线后段拖着一截零值水平线,基本就是切分和清洗的问题。修复方式是把有效区间切分的辐照度阈值从30W/m²提高到50W/m²,并重新做曲线对齐。
6.2 聚类结果不稳定,换了种子类别就乱跳
这个现象通常有两个来源。一是特征向量里归一化曲线维度占比太高,统计特征被“淹没”,导致对微小噪声敏感;二是K值取在了轮廓系数的平缓区,此时多个K都差不多,K-means在多个局部最优上来回横跳。我的处理方式是给特征向量加权,把归一化曲线部分乘以0.6权重,统计特征乘以0.4权重,然后固定随机种子并跑30次取最优SSE。
6.3 过渡日的归属总是漂移
晴转阴的过渡日、上午晴下午雨的切换日,聚类时归属不稳定非常正常。我的方案是不追求一天只能属于一个类,而是先算软分类分数,即样本对每个质心的欧氏距离,然后对这三五个距离分数做时间维度的滑窗滤波。比如天气标签连续三天在晴天和多云之间反复横跳,就取三天内距离最小的质心作为最终归属,过渡日会更平滑,也更符合气象上的连续性。
6.4 类数量增加,业务解释却变差怎么办
当你把K从3加到5,结果多出来的两个类不仅没有明确语义,还和已有的类重叠严重,下游同事完全不知道该怎么用。这时候不要硬撑。K=5如果解释不了,退回到K=3作为主标签,再把K=5的结果作为细分标签保留在数据库里。聚类是为业务服务的,不是为指标服务的。
6.5 聚类结果和气象站天气记录对不上
遇到这种情况,先检查气象站的位置和电站的实际距离。超过几十公里的气象站,局部阵雨和云团移动会让标签完全错位。另一个可能是时间粒度不匹配,气象站按小时记录,功率曲线是15分钟粒度。我的做法是把气象记录重采样到15分钟,再和聚类标签做对齐。如果距离远到无法对齐,就干脆放弃严格的天气标签校验,改用“电站本身的出力形态”作为业务验证标准,毕竟聚类对象是功率曲线本身,不是气象数据。
提示:我印象最深的一次项目事故,是有一批数据里混入了停机检修日。这些天的曲线是一条接近0的直线,聚类时全部归到了阴雨类,把“纯阴雨天”的平均曲线拉高了将近一半。后来我在清洗阶段加入一条硬性规则:全天最大功率低于装机容量5%的样本单独标记为“无效/停机日”,不参与聚类,只保留单独告警。从此这类污染就没有再出现过。
最后再分享一个对我来说最重要的小技巧:永远通过下游任务来验证聚类质量,不要沉浸在手肘图和轮廓系数里。把聚类标签接入超短期预测模型之后,预测误差RMSE如果能稳定下降5%到10%,说明这套聚类是真正有用的。如果指标很好看但下游任务没有变化,那大概率是聚类结果在自嗨,重新检查特征工程才是解决问题的正道。