说到光伏时间序列聚类很多人第一反应是“不就是把曲线归归类”但真正上手做一次基于K-means的光伏功率数据聚类你会发现坑比想象中多得多。数据切分、特征构造、K值选择、评估指标每一步都藏着细节走错一步聚类结果就会“看起来好看、用起来没用”。这篇文章把我实际跑过的方案完整梳理一遍从问题拆解到优化细节再到业务落地全程用代码和实例说话适合正在做光伏功率预测、新能源数据分析和时间序列挖掘的同行参考。1. 光伏时间序列聚类的任务拆解与方案选型1.1 这个研究到底在解决什么问题光伏电站的出力数据本质上是气象条件在电气侧的映射。晴天、多云、阴天、雨天这四种典型天气下的出力曲线形态差异极大晴天是一条平滑的单峰曲线中午前后达到峰值多云天则是高频波动叠加在缓慢升降的趋势上阴雨天出力整体很低峰值不明显甚至整天都是贴近零轴的平线。如果把这些差异巨大的日曲线全部丢进同一个预测模型模型会为了拟合不同天气形态而互相“打架”预测精度自然上不去。聚类的目标就很清晰了把形状相似的日曲线归到同一类后续针对每一类分别训练预测模型、分别统计误差、分别制定运维策略。这就是“分而治之”的核心思想也是这个研究最根本的出发点。另一个常被忽略的应用是异常识别。光伏逆变器故障、组串遮挡、通信丢数都会让日曲线形态偏离它本该所属的类别聚类模型训练好后新来的样本如果被分到奇怪的类或者距所有聚类中心都很远这就是一个明确的异常信号可以直接触发告警流程。1.2 为什么选K-means而不是DBSCAN、层次聚类聚类算法一堆为什么选K-means我当时的筛选逻辑很简单数据规模、参数敏感度、结果可解释性、计算成本四条标准卡下来K-means是最务实的选项。先说数据规模。光伏电站的数据采集频率通常是15分钟一次一天96个点一年365天就是35040个点。如果用层次聚类需要计算所有样本两两之间的距离矩阵复杂度是O(n²)样本量上万之后就很难受了。DBSCAN虽然能处理不规则形状的簇但它的两个核心参数eps和min_samples非常敏感光伏日曲线在高维空间里分布密集且形状相近调参成本极高而且DBSCAN对密度不均的数据会直接把稀疏区域的样本全判为噪声这在光伏场景下意味着大量正常但不出众的阴天样本被丢弃业务上无法接受。K-means的复杂度是O(n·k·t)n是样本数k是类别数t是迭代次数在样本量几万、k值两位数的情况下计算速度几乎可以忽略不计。再加上sklearn里成熟的k-means初始化和Mini-Batch优化百万级样本也能跑得动。可解释性方面K-means聚类完成后每个簇的中心向量就是一条有物理意义的“典型日曲线”直接可视化给业务人员看他们能立刻明白每一类代表什么天气不需要解释复杂的密度概念或树状图结构。2. 数据预处理与特征构造聚类效果的前置条件2.1 光伏时间序列的三项特殊“毛病”直接拿原始96维序列喂给K-means之前必须先处理光伏数据特有的三个问题。第一个问题是零点堆积。光伏出力在夜间恒为0如果直接把原始曲线拼接起来聚类算法会发现绝大多数样本在夜间段距离为0这个共同特征会主导距离计算结果就是聚类把所有样本都往“夜间为零”这个方向上压缩白天形态的差异反而被稀释了。我在实际测试中对比过不做夜间截断的聚类结果类别之间主要区分的是“夜间有没有出力”——这对光伏来说毫无意义因为夜间出力本身就应该为0除非是储能放电或反送电的异常场景。第二个问题是量纲差异。辐照度序列的取值范围是0到1000W/m²功率序列是0到额定容量而温度序列可能是负10到40度的范围。如果把这些不同量纲的特征直接拼在一起做欧氏距离计算绝对值大的变量会主导距离量纲小的变量即使携带了重要的天气区分信息也会被淹没。对于只做功率序列聚类的情况不存在这个问题但一旦引入气象特征做联合聚类标准化就是必须的。第三个问题是缺失值和坏数据。光伏电站的数据采集链路长传感器故障、通信中断、逆变器停机都会造成数据缺失或异常。如果用线性插值填补大段缺失会人为制造出平滑但虚假的曲线如果不填补样本维度不齐聚类无法进行。这是一个典型的先有鸡还是先有蛋的问题我的做法是先做严格的坏数据清洗再按需插值。2.2 实用的特征工程方案很多人第一次做光伏聚类直接拿96维原始序列作为特征输入K-means结果发现类别不清晰、轮廓系数低。我后来在实践中总结出两个方向一个是降维后再聚类另一个是构造业务特征替代原始序列。两种方案都可行但适用场景不同。降维方案用的是PCA。96维的原始序列先标准化再用PCA压缩到15到20维保留95%以上的方差然后对降维后的特征做K-means。这个方案的优点是保留了曲线形态的完整信息缺点是降维后的特征失去了物理可解释性聚类中心投影回原始空间才能看到曲线形状。业务特征方案更受运行人员欢迎。我从每条日曲线中提取一组物理含义明确的特征日发电量曲线积分、峰值功率、峰值出现时刻、出力波动率相邻点差值的标准差、晴空指数实际发电量占理论晴空发电量的比例、上升速率、下降速率。这7个特征构成的向量维度低、噪声少、业务解释性强聚类结果可以直接对应到“晴空满发”“上午晴下午多云”“全天阴雨”这类自然语言描述。两个方案我都跑过最终项目落地用的是业务特征方案。不是PCA方案效果不好而是业务方更信任能看懂的特征。特征构造的价值在于把高维曲线压缩成有语义的低维向量让K-means的聚类过程从“形状匹配”变成“语义匹配”结果自然更符合业务预期。3. K-means的优化细节从初始点到距离度量3.1 K值怎么选肘部法、轮廓系数与业务约束K-means的K值需要预先指定这是算法本身最大的痛点。我常用的方法是“三路交叉验证”肘部法看趋势、轮廓系数看质量、业务约束看合理性。肘部法的逻辑很简单随着K增大每个样本离所属聚类中心的距离平方和SSE单调下降但下降速率会变缓形成“拐点”。实操中我用如下代码import numpy as np from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler sse [] k_range range(2, 11) for k in k_range: kmeans KMeans(n_clustersk, initk-means, n_init20, max_iter300, random_state42) kmeans.fit(X_scaled) sse.append(kmeans.inertia_) # 打印SSE和下降率 for i, k in enumerate(k_range): if i 0: print(fk{k}, SSE{sse[i]:.2f}) else: drop_rate (sse[i-1] - sse[i]) / sse[i-1] * 100 print(fk{k}, SSE{sse[i]:.2f}, 较k{k-1}下降{drop_rate:.2f}%)运行结果通常显示K从2到4时SSE下降明显从4到5之后下降率明显变缓这就是肘部所在。但肘部法只能给出一个区间不能给出确定的“最优K”这时候要靠轮廓系数和业务约束来收敛。轮廓系数衡量的是样本与自己簇内样本的相似度大于与最近簇样本的相似度的程度取值在-1到1之间越大越好。我通常计算K在2到10之间的平均轮廓系数选出最大值对应的K再和肘部法的结果交叉验证。如果肘部法指向K4而轮廓系数指向K5我会进一步结合业务需求判断——如果聚类结果是要做天气类型自动识别那4类正好对应晴天、多云、阴天、雨天业务上自洽如果是为预测模型做分类型训练K5虽然统计指标更好但第五类样本量可能很小训练出的模型反而不可靠。3.2 初始化与迭代k-means与Mini-Batch标准K-means对初始聚类中心敏感随机初始化可能导致收敛到局部最优解。实际项目中我几乎不使用默认的随机初始化而是始终指定initk-means。k-means的核心思想是让初始聚类中心互相远离第一个中心随机选取后续每个中心以正比于样本到最近已有中心距离的概率被选中。这样初始中心分布更均匀收敛速度更快结果更稳定。除了初始化的优化n_init参数也值得留意。它表示用不同初始中心运行K-means的轮数算法会从所有运行轮次中选SSE最小的那一次作为最终结果。我习惯设为20再叠加random_state固定随机种子确保实验结果可复现。这一点在做研究和写报告时尤其重要我踩过不固定种子导致两次运行聚类结果完全不同的坑后来所有聚类实验都固定了random_state。当样本量达到数十万量级时标准K-means每次迭代都要计算所有样本到所有聚类中心的距离计算量会变得可观。这时改用Mini-Batch K-Means是一个高效的选择它每次随机采样一个小批量样本更新聚类中心计算开销大幅下降聚类质量损失在可接受范围内。我在处理三年以上的历史数据时用Mini-Batch方案实测几万样本从几十秒缩短到几秒内完成。3.3 距离度量的选择K-means默认使用欧氏距离这在多数场景下够用但光伏场景有一个特殊问题需要谨慎对待。如果特征向量中包含“峰值出现时刻”这种周期性变量比如13点和14点的数值差异不大但0点和23点之间差了23个小时直接套欧氏距离计算会把周期性错误地放大。解决方案有两种。一种是放弃绝对时刻改用相对时刻。把峰值出现的原始时刻转换为相对于正午的小时偏差比如正午前后各6小时偏差范围是-6到6这样12点偏差-1和14点偏差1之间距离为2符合直觉而不会因为跨午夜边界出现巨大跳变。另一种思路是直接用相关系数距离定义两个样本间的距离为1减去它们的相关系数。相关系数距离天然对幅值不敏感只关注曲线形状是否相似这在光伏场景下非常有用——同样是晴天夏天功率高、冬天功率低幅值差异大但形状相似用欧氏距离可能把它们分到不同簇用相关系数距离就能正确归类。不过相关系数距离不能直接用sklearn的KMeans需要自己实现迭代逻辑用scipy的层次聚类或者自写EM式迭代。我的经验是特征向量中同时包含幅值型和形态型特征时用欧氏距离配合标准化更稳妥只关心曲线形态归一化对比时用相关系数距离更有优势。具体选哪种取决于后续应用是功率预测需要幅值信息还是形态归类只看形状。4. 实操代码与聚类结果评估4.1 从原始数据到聚类结果的完整代码这里给出一个完整的实操流程数据是某分布式光伏电站一年的输出功率数据采集间隔15分钟每天96个点。import pandas as pd import numpy as np from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.metrics import silhouette_score, calinski_harabasz_score, davies_bouldin_score # 1. 读取原始数据日期为索引列为每天96个时刻点 df pd.read_csv(pv_power_data.csv, index_coltimestamp, parse_datesTrue) # 2. 按天重采样成96维特征向量 daily_data df[power].resample(D).apply(list).dropna() X_raw np.array(daily_data.tolist()) # 3. 坏数据清洗夜间为零是正常但白天出现连续0值视为异常剔除 def is_valid_day(row): daytime row[6*4:18*4] # 6:00-18:00 return np.sum(daytime 0) 20 # 白天至少要有20个非零点 mask np.array([is_valid_day(row) for row in X_raw]) X_raw X_raw[mask] print(f清洗后有效天数: {len(X_raw)}) # 4. 提取业务特征 features [] for row in X_raw: # 这里简化提取实际可按需求扩展 daily_energy np.trapz(row, dx0.25) # 数值积分估算日发电量 peak_value np.max(row) peak_time np.argmax(row) / 4.0 # 转换成小时 volatility np.std(np.diff(row)) features.append([daily_energy, peak_value, peak_time, volatility]) X_feat np.array(features) # 5. 标准化 scaler StandardScaler() X_scaled scaler.fit_transform(X_feat) # 6. 跑K-meansK4 kmeans KMeans(n_clusters4, initk-means, n_init20, max_iter300, random_state42) labels kmeans.fit_predict(X_scaled) # 7. 聚类效果评估 sil silhouette_score(X_scaled, labels) ch calinski_harabasz_score(X_scaled, labels) db davies_bouldin_score(X_scaled, labels) print(f轮廓系数: {sil:.4f}) print(fCalinski-Harabasz指数: {ch:.2f}) print(fDavies-Bouldin指数: {db:.4f})4.2 聚类评估指标怎么解读聚类评估是“无监督中的有监督”不能用准确率来衡量只能通过内部指标判断聚类结构的紧凑性和分离度。常用的三个指标各有侧重。轮廓系数的范围是-1到1大于0.5说明聚类结构良好0.25到0.5说明有重叠但可接受低于0.25说明数据本身没有清晰的类簇结构或特征选择不当。在我做过的光伏日曲线聚类中使用业务特征方案时轮廓系数通常在0.45到0.55之间这已经是不错的结果。Calinski-Harabasz指数CH指数的计算逻辑是簇间离散度与簇内离散度的比值数值越大说明聚类效果越好。这个指标没有绝对的上限更适合用来比较不同K值、不同特征组合的相对优劣。Davies-Bouldin指数DB指数则相反数值越小越好。它衡量的是每个簇与其最相似簇之间的平均相似度最小化簇内离散度同时最大化簇间距离才能得到较低的DB值。这三个指标很可能给出不同的排序结论这是正常现象。我在实践中以轮廓系数为主、另外两个为辅轮廓系数反映样本级聚类质量CH指数和DB指数更多是趋势参考。最终算法参数的选择还是要回到业务目标上一个轮廓系数略低但类别特征鲜明、业务解释清晰的聚类结果比一个纯粹统计指标好看但类别边界模糊的结果更有实用价值。4.3 降维可视化与聚类中心分析聚类完成后必须做可视化验证否则无法判断聚类结果是否真的符合物理直觉。由于特征维度超过三维直接用散点图无法直观呈现通常用PCA将特征投影到二维平面。pca PCA(n_components2) X_pca pca.fit_transform(X_scaled) import matplotlib.pyplot as plt plt.figure(figsize(10, 6)) colors [#1f77b4, #ff7f0e, #2ca02c, #d62728] for i in range(4): cluster_points X_pca[labels i] plt.scatter(cluster_points[:, 0], cluster_points[:, 1], ccolors[i], labelfCluster {i}, alpha0.6, s20) plt.xlabel(PC1) plt.ylabel(PC2) plt.legend() plt.title(K-means聚类结果可视化) plt.grid(alpha0.3) plt.show()可视化只是辅助聚类中心的物理分析才是关键。把每个簇的中心向量映射回96维原始空间画出典型日曲线我通常会在图中标注出每个簇的中心曲线以及该簇内几条代表性的原始曲线。如果聚类效果理想4个簇的中心曲线应该分别呈现出平滑饱满的单峰曲线晴天、顶部扁平或有小锯齿的曲线晴间多云、宽而扁的低矮曲线阴天、几乎贴近零轴的短线雨天。有一次聚类结果中两个簇的中心曲线形状接近区别只在幅值高低这意味着特征工程出了问题——模型把“高辐照晴天”和“低辐照晴天”拆成了两类业务上这是完全没必要区分的。后来我在特征中增加了归一化处理把发电量特征改为“实际发电量占当日理论发电量比例”聚类结果才回归合理。5. 聚类结果在光伏业务中的深度应用与踩坑记录5.1 天气类型自动识别与气象数据交叉验证聚类结果最直接的应用是自动识别天气类型。将聚类得到的簇标签与电站当地气象站的实测天气数据做交叉验证可以验证聚类模型是否真的学到了天气规律。气象数据包括每日总辐照量、平均温度、降水量等。我通常的做法是构建一个混淆矩阵类别的表格统计每一簇中晴天、多云、阴天、雨天的占比。如果聚类正确晴天样本应该主要集中在某一簇。交叉验证还有一个额外的好处发现气象数据与聚类结果不一致的样本。这些样本往往是光伏电站出力异常而气象数据正常的情况比如组串被遮挡导致晴天出力偏低被聚类模型正确识别为“非晴天”类但气象站记录却是晴天。这类样本正是运维需要关注的疑点值得人工复核。基于同样的逻辑聚类结果还可以为空头辐照预测模型做质量标记。辐照预测模型给出的预报值需要与聚类结果对比如果某天预报晴天但实际聚类结果为阴天类说明预报模型有较大偏差可以记录并作为模型迭代的训练样本。5.2 分类型功率预测与异常检测聚类最典型的下游应用是分类型功率预测。操作流程是先对全部历史日曲线做聚类得到4类曲线然后为每个类别单独训练一个功率预测模型LSTM、XGBoost或者简单的线性回归都可以预测时先用当天的气象预报判断天气类型选择对应的模型做预测。相比一个全局模型分类型模型的优势在于每个模型只专注学习一种天气形态下的出力规律拟合精度和预测稳定性都有明显提升。我在某分布式电站的实验中对比了全局LSTM和分类型LSTM分类型方案在晴天和多云天两类样本上的预测误差降低了15%到20%阴雨天因为本身出力低、绝对误差小提升不明显但相对误差也略有改善。当然这个提升幅度和电站所在地区的气候特点相关云量变化剧烈的地区提升更显著常年晴天的干燥地区提升有限。异常检测是另一个实用方向。K-means聚类完成后每个样本到其所属聚类中心的距离就是它的“离群得分”。对于新获取的日曲线先计算它离所属簇中心的距离如果距离超过该簇内95%样本的距离分布范围就标记为疑似异常。这种方法和纯粹基于阈值的检测相比胜在自适应——不同天气类型的正常波动范围不同晴天的波动小多云的波动大聚类方法能区分开这两种情况分别设定异常阈值。5.3 实操中常见的坑与解决方法最大坑来自于缺失数据的插值处理。我早期用线性插值连续填补3小时以上的数据缺失结果聚类出来的“晴天”类里混入了一些实际是阴天但因插值而变得平滑的曲线。后来改成规则少于4个连续缺失点用线性插值超过4个连续缺失直接剔除当天样本。这个规则在样本量充足一年365天即使剔除大半个月仍有足够样本的情况下完全可行。第二个坑是类别不均衡。在日照资源丰富的地区晴天样本可能占70%以上其他三类合计不足30%。这种不均衡会让小类别的聚类中心被大类别的样本“拽偏”。解决思路是调整K值让大类别的内部细分得到保留或者对小类别样本做过采样——具体做法是为少数类增加合成样本或者干脆调整业务目标的粒度接受“晴天内部还有细分”的结果。第三个坑是季节因素干扰。同一个晴天冬天的出力曲线峰值低、持续时间短夏天的峰值高、持续时间长如果不做季节维度的处理会把“冬晴”和“夏晴”分成两类。处理方式是在特征中显式加入月份或季节信息或者将功率值归一化为占当日理论晴空出力比例后再聚类这样季节性幅值差异就被消除了形态特征主导聚类结果。第四个坑是K值跨季节不稳定。年初定的K4在夏季跑出漂亮的聚类到了冬季可能因为样本结构变化导致某个类别近乎消失或者两个类别合并。应对方案是定期重新聚类或者采用滑动窗口方式用近半年的数据聚类逐月更新聚类中心和类别定义。这类模型维护层面的工作容易被忽视但实际生产环境中特别重要我见过很多团队花大力气做了一次聚类就上线使用半年后模型退化还不自知。我还想提醒一点聚类完成后务必检查每个簇的样本数量。光伏领域的经验法则是每个簇的样本占比最好在10%以上最低不低于5%。如果一个簇只占总样本的2%它对应的天气类型过于稀缺分类型预测模型在这个簇上很难训练出可靠的参数业务决策也会被极端样本干扰。遇到这种情况宁可把这个簇合并到最相似的相邻簇中也不要强行保留。做光伏时间序列聚类很多时候难点不在算法本身而在于对数据语境的深刻理解。我自己的体会是K-means只是工具箱里的一把扳手关键在于你想拧紧哪个螺丝——是为了天气类型识别做特征工程还是为了预测精度做数据分流不同的业务目标直接决定了特征构造、K值选择、评估指标和落地方式。先想清楚“聚类给谁用、怎么用”再动手跑代码这个顺序不能反。如果你也在做类似方向建议从今天的数据出发先提取业务特征再跑一轮K-means画一张典型日曲线图看看你会发现很多数据里的秘密是之前从未注意到的。
