说到光伏时间序列聚类,很多人第一反应是“不就是把曲线归归类”,但真正上手做一次基于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_clusters=k, init='k-means++', n_init=20, max_iter=300, random_state=42) kmeans.fit(X_scaled) sse.append(kmeans.inertia_) # 打印SSE和下降率 for i, k in enumerate(k_range): if i == 0: print(f'k={k}, SSE={sse[i]:.2f}') else: drop_rate = (sse[i-1] - sse[i]) / sse[i-1] * 100 print(f'k={k}, SSE={sse[i]:.2f}, 较k={k-1}下降{drop_rate:.2f}%')运行结果通常显示K从2到4时SSE下降明显,从4到5之后下降率明显变缓,这就是肘部所在。但肘部法只能给出一个区间,不能给出确定的“最优K”,这时候要靠轮廓系数和业务约束来收敛。
轮廓系数衡量的是样本与自己簇内样本的相似度大于与最近簇样本的相似度的程度,取值在-1到1之间,越大越好。我通常计算K在2到10之间的平均轮廓系数,选出最大值对应的K,再和肘部法的结果交叉验证。如果肘部法指向K=4而轮廓系数指向K=5,我会进一步结合业务需求判断——如果聚类结果是要做天气类型自动识别,那4类正好对应晴天、多云、阴天、雨天,业务上自洽;如果是为预测模型做分类型训练,K=5虽然统计指标更好,但第五类样本量可能很小,训练出的模型反而不可靠。
3.2 初始化与迭代:k-means++与Mini-Batch
标准K-means对初始聚类中心敏感,随机初始化可能导致收敛到局部最优解。实际项目中我几乎不使用默认的随机初始化,而是始终指定init='k-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_col='timestamp', parse_dates=True) # 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, dx=0.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-means,K=4 kmeans = KMeans(n_clusters=4, init='k-means++', n_init=20, max_iter=300, random_state=42) 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(f'Calinski-Harabasz指数: {ch:.2f}') print(f'Davies-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_components=2) 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], c=colors[i], label=f'Cluster {i}', alpha=0.6, s=20) plt.xlabel('PC1') plt.ylabel('PC2') plt.legend() plt.title('K-means聚类结果可视化') plt.grid(alpha=0.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值跨季节不稳定。年初定的K=4在夏季跑出漂亮的聚类,到了冬季可能因为样本结构变化导致某个类别近乎消失或者两个类别合并。应对方案是定期重新聚类,或者采用滑动窗口方式,用近半年的数据聚类,逐月更新聚类中心和类别定义。这类模型维护层面的工作容易被忽视,但实际生产环境中特别重要,我见过很多团队花大力气做了一次聚类就上线使用,半年后模型退化还不自知。
我还想提醒一点,聚类完成后务必检查每个簇的样本数量。光伏领域的经验法则是每个簇的样本占比最好在10%以上,最低不低于5%。如果一个簇只占总样本的2%,它对应的天气类型过于稀缺,分类型预测模型在这个簇上很难训练出可靠的参数,业务决策也会被极端样本干扰。遇到这种情况,宁可把这个簇合并到最相似的相邻簇中,也不要强行保留。
做光伏时间序列聚类,很多时候难点不在算法本身,而在于对数据语境的深刻理解。我自己的体会是,K-means只是工具箱里的一把扳手,关键在于你想拧紧哪个螺丝——是为了天气类型识别做特征工程,还是为了预测精度做数据分流,不同的业务目标直接决定了特征构造、K值选择、评估指标和落地方式。先想清楚“聚类给谁用、怎么用”,再动手跑代码,这个顺序不能反。如果你也在做类似方向,建议从今天的数据出发,先提取业务特征,再跑一轮K-means,画一张典型日曲线图看看,你会发现很多数据里的秘密是之前从未注意到的。