Python实现PSO-KNN光伏功率预测粒子群自动寻优K与P的完整工程实战从数据清洗、时间特征、严格时序验证到PSO参数搜索、KNN回归、误差诊断与可部署改进Python 光伏功率预测 粒子群优化 PSO K近邻 KNN 机器学习 时间序列预测 新能源 智能电网光伏功率受辐照度、组件温度、云层变化和设备状态共同影响短时预测既要处理强非线性也要避免时间序列数据泄漏。本文给出一套可复现的 PSO-KNN 光伏功率预测工程方案先完成时间对齐、异常治理、周期编码与滞后特征构造再按时间顺序划分训练/验证/测试集随后以验证集 RMSE 为适应度利用粒子群算法联合搜索 KNN 的近邻数 K 与闵可夫斯基参数 P最终在独立测试集上用 MAE、RMSE、R² 和残差图完成评价。文章同时补充真实项目中最容易被忽略的泄漏风险、夜间样本、预测步长、未来气象特征和部署边界并提供可直接改造成实际电站程序的完整代码框架。先看结论这个项目真正难的不是“把 PSO 和 KNN 拼起来”如果只把 K 和 P 交给粒子群搜索代码很快就能跑通但一个能用于真实光伏场景的预测项目核心在于三件事第一所有数据处理必须尊重时间因果关系第二特征必须对应预测时刻真实可获得的信息第三参数寻优、最终训练和测试评价必须相互隔离。任何一项做错都可能得到“指标很好、上线失效”的结果。图1 光伏功率与辐照度短时波动示意晴天时功率曲线通常接近稳定单峰多云条件下云团移动会造成分钟级辐照突降与恢复。KNN 的优势正来自这种“局部相似性”当前气象状态与历史某些时刻相似时可直接借鉴那些样本的目标功率PSO 则负责自动寻找更合适的邻域尺度与距离形状。1. 项目目标与适用边界目标是预测未来一个或多个采样间隔的交流侧有功功率。以 15 分钟采样为例forecast_steps1 对应未来 15 分钟forecast_steps2 对应未来 30 分钟。本文重点演示单步短时预测。对于更长预测时域建议引入数值天气预报NWP或未来气象预报特征而不是默认未来辐照度“已知”。场景推荐输入预测时域注意事项站内超短期当前气象历史功率15~60分钟当前观测通常可用日前/数小时NWP未来气象历史功率1~24小时不能使用未来实测气象边缘设备少量关键特征15~30分钟关注推理延迟与内存2. 为什么选择 PSO-KNNKNN 回归没有传统意义上的参数训练过程它把历史样本本身视为知识库。对待预测样本 x先计算其与训练样本的距离再选取最近的 K 个样本。采用距离加权时可写成ŷ Σ(wᵢyᵢ) / Σwᵢ其中 wᵢ 常取 1/(dᵢε)。闵可夫斯基距离 d(x,z) (Σ|xⱼ-zⱼ|ᵖ)^(1/p)P1 接近曼哈顿距离P2 为欧氏距离。图2 KNN 在标准化特征空间中寻找局部相似样本K 太小会对噪声和异常点敏感K 太大会把局部天气突变平均掉P 又直接改变“相似”的几何定义。因此本文把 K 与 P 作为二维粒子位置由 PSO 以验证集 RMSE 为目标自动寻优。3. 完整模型架构图3 PSO-KNN 光伏功率预测完整工程链路推荐把工程拆成八个可验证模块。每个模块都应留下中间结果清洗后样本数、特征列表、时间切分边界、标准化器参数来源、每轮最优 RMSE、最终超参数、测试指标和预测明细。这样一旦结果异常可以定位到具体环节而不是只看到最终 R²。4. 数据字段与质量治理字段含义单位处理建议timestamp采集时间—解析、排序、去重、统一时区irradiance组件面/水平面辐照度W/m²负值置0检查传感器饱和ambient_temp环境温度℃异常范围核验module_temp组件温度℃关注高温降额humidity相对湿度%裁剪到0~100wind_speed风速m/s核验停机/缺测power_kw交流侧功率kW负值置0上限结合额定容量4.1 一个必须修正的常见问题不要在全量数据上先 bfill对时间序列而言先在全量数据上执行 bfill后向填充再切训练集/测试集可能把未来观测值传播到过去样本形成隐蔽的数据泄漏。更稳妥的做法是先按时间切分再在每个阶段使用只依赖过去信息的填充策略或者只对极短缺口做因果插值并明确最大缺口长度。data data.sort_values(timestamp).drop_duplicates(timestamp)# 推荐优先使用只依赖过去的前向填充data[numeric_columns] data[numeric_columns].ffill(limit4)# 对仍缺失的记录根据业务规则删除或单独建模data data.dropna(subsetnumeric_columns)5. 时间特征与滞后特征图4 多源特征共同定义相似运行状态小时、月份具有周期性直接使用 23 与 0 会让模型误认为二者相距很远因此采用 sin/cos 双通道编码。短时预测还应加入 power_lag_1、power_lag_2 等滞后功率若数据量充足可进一步加入 1 小时滚动均值、辐照变化率、晴空指数、太阳高度角等物理增强特征。data[hour_float] data[timestamp].dt.hour data[timestamp].dt.minute / 60data[hour_sin] np.sin(2*np.pi*data[hour_float]/24)data[hour_cos] np.cos(2*np.pi*data[hour_float]/24)data[power_lag_1] data[power_kw].shift(1)data[target_power_kw] data[power_kw].shift(-forecast_steps)6. 严格时间顺序验证比模型本身更重要图5 训练集、验证集、测试集按时间顺序隔离训练集负责建立邻域库验证集只负责选择 K/P测试集只在所有选择完成后使用一次。StandardScaler 必须先在训练集 fit再 transform 验证集和测试集。超参数确定后可以用训练验证数据重新拟合最终 scaler 与 KNN但测试集始终不能参与任何统计量估计。7. PSO 粒子群优化机制图6 PSO 在 K-P 二维空间中的搜索示意每个粒子位置 xᵢ[K,P]速度更新采用经典形式vᵢ(t1)ωvᵢ(t)c₁r₁(pbestᵢ-xᵢ)c₂r₂(gbest-xᵢ)随后 xᵢ(t1)xᵢ(t)vᵢ(t1)。其中 ω 控制惯性c₁ 控制个体学习c₂ 控制群体学习。K 是离散变量因此评价前四舍五入并裁剪到合法整数范围P 保持连续。图7 PSO 验证集 RMSE 收敛过程合成数据演示工程上不建议盲目增加粒子数和迭代次数。KNN 的一次评估需要在验证集上执行近邻查询样本量较大时计算成本会迅速增加。可先用 15~30 个粒子、20~40 轮迭代建立基线再根据收敛曲线决定是否扩大搜索预算。8. 可直接运行的核心代码下面给出整理后的核心实现。代码重点修正了三个问题避免全量双向填充造成未来信息泄漏明确训练/验证/测试职责超参数确定后重新在训练验证集上拟合最终预处理器与模型。import numpy as npimport pandas as pdfrom sklearn.preprocessing import StandardScalerfrom sklearn.neighbors import KNeighborsRegressorfrom sklearn.metrics import mean_absolute_error, mean_squared_error, r2_scoreRANDOM_SEED 42RATED_POWER_KW 1000.0FORECAST_STEPS 1PARTICLE_COUNT 24ITERATION_COUNT 30K_LOWER, K_UPPER 2, 40P_LOWER, P_UPPER 1.0, 3.0W, C1, C2 0.72, 1.45, 1.45rng np.random.default_rng(RANDOM_SEED)data pd.read_csv(pv_power_data.csv)data[timestamp] pd.to_datetime(data[timestamp], errorscoerce)data data.dropna(subset[timestamp]).sort_values(timestamp)data data.drop_duplicates(timestamp).reset_index(dropTrue)numeric_columns [irradiance, ambient_temp, module_temp,humidity, wind_speed, power_kw]data[numeric_columns] data[numeric_columns].apply(pd.to_numeric, errorscoerce)# 因果填充只使用过去值limit 应按采样周期和业务容忍度设置data[numeric_columns] data[numeric_columns].ffill(limit4)data[power_kw] data[power_kw].clip(0, RATED_POWER_KW)data[irradiance] data[irradiance].clip(lower0)data[humidity] data[humidity].clip(0, 100)data data.dropna(subsetnumeric_columns).reset_index(dropTrue)hour_float data[timestamp].dt.hour data[timestamp].dt.minute / 60data[hour_sin] np.sin(2*np.pi*hour_float/24)data[hour_cos] np.cos(2*np.pi*hour_float/24)month data[timestamp].dt.monthdata[month_sin] np.sin(2*np.pi*month/12)data[month_cos] np.cos(2*np.pi*month/12)data[weekday] data[timestamp].dt.weekdaydata[power_lag_1] data[power_kw].shift(1)data[target_power_kw] data[power_kw].shift(-FORECAST_STEPS)data data.dropna().reset_index(dropTrue)feature_columns [irradiance, ambient_temp, module_temp, humidity, wind_speed,hour_sin, hour_cos, month_sin, month_cos,weekday, power_lag_1]X data[feature_columns].to_numpy(float)y data[target_power_kw].to_numpy(float)n len(X)train_end int(n * 0.70)valid_end int(n * 0.85)X_train, y_train X[:train_end], y[:train_end]X_valid, y_valid X[train_end:valid_end], y[train_end:valid_end]X_test, y_test X[valid_end:], y[valid_end:]scaler StandardScaler()X_train_s scaler.fit_transform(X_train)X_valid_s scaler.transform(X_valid)def fitness(position):k int(np.clip(np.rint(position[0]), K_LOWER,min(K_UPPER, len(X_train_s))))p float(np.clip(position[1], P_LOWER, P_UPPER))model KNeighborsRegressor(n_neighborsk, weightsdistance,metricminkowski, pp, n_jobs-1)model.fit(X_train_s, y_train)pred model.predict(X_valid_s)return float(np.sqrt(mean_squared_error(y_valid, pred)))lower np.array([K_LOWER, P_LOWER], dtypefloat)upper np.array([K_UPPER, P_UPPER], dtypefloat)positions rng.uniform(lower, upper, size(PARTICLE_COUNT, 2))velocities rng.uniform(-0.5, 0.5, size(PARTICLE_COUNT, 2))pbest_pos positions.copy()pbest_score np.full(PARTICLE_COUNT, np.inf)gbest_pos positions[0].copy()gbest_score np.inffor i in range(PARTICLE_COUNT):s fitness(positions[i])pbest_score[i] sif s gbest_score:gbest_score, gbest_pos s, positions[i].copy()for _ in range(ITERATION_COUNT):r1 rng.random((PARTICLE_COUNT, 2))r2 rng.random((PARTICLE_COUNT, 2))velocities (W * velocities C1 * r1 * (pbest_pos - positions) C2 * r2 * (gbest_pos - positions))positions np.clip(positions velocities, lower, upper)for i in range(PARTICLE_COUNT):s fitness(positions[i])if s pbest_score[i]:pbest_score[i] spbest_pos[i] positions[i].copy()if s gbest_score:gbest_score sgbest_pos positions[i].copy()best_k int(np.clip(np.rint(gbest_pos[0]), K_LOWER, K_UPPER))best_p float(np.clip(gbest_pos[1], P_LOWER, P_UPPER))# 最终模型只合并训练集验证集测试集仍保持完全独立final_scaler StandardScaler()X_train_valid_s final_scaler.fit_transform(X[:valid_end])X_test_s final_scaler.transform(X_test)final_model KNeighborsRegressor(n_neighborsbest_k, weightsdistance,metricminkowski, pbest_p, n_jobs-1)final_model.fit(X_train_valid_s, y[:valid_end])test_pred np.clip(final_model.predict(X_test_s), 0, RATED_POWER_KW)mae mean_absolute_error(y_test, test_pred)rmse np.sqrt(mean_squared_error(y_test, test_pred))r2 r2_score(y_test, test_pred)result pd.DataFrame({timestamp: data[timestamp].iloc[valid_end:].to_numpy(),actual_power_kw: y_test,predicted_power_kw: test_pred,error_kw: test_pred - y_test})result.to_csv(pso_knn_prediction_result.csv,indexFalse, encodingutf_8_sig)print(best_k , best_k)print(best_p , round(best_p, 4))print(validation_rmse , round(gbest_score, 4))print(test_mae , round(mae, 4))print(test_rmse , round(rmse, 4))print(test_r2 , round(r2, 4))9. 可复现实验结果先用合成数据验证流程再替换真实电站数据为了避免虚构真实电站指标下面的数值来自本文脚本生成的可复现合成数据仅用于验证程序链路是否正确不代表任何实际电站性能。真实项目应使用自己的历史数据重新运行并保留测试时段、额定容量、采样周期和天气分布说明。指标本次合成数据演示最优 K4最优 P1.0000验证集最佳 RMSE128.23 kW测试集 MAE43.56 kW测试集 RMSE67.06 kW测试集 R²0.9443固定 K5, P2 基线 RMSE74.30 kW图8 独立测试集真实功率与预测功率合成数据演示图9 残差诊断合成数据演示评价时不要只看 R²。MAE 更直观地表示平均偏差RMSE 会放大少数严重误差残差图则能帮助判断模型是否在高功率区、爬坡段或突发云遮阶段存在系统性偏差。若误差随功率增大而明显扩散可考虑按额定功率归一化或分天气类型建立子模型。10. 真实项目中最容易踩的 8 个坑未来气象泄漏预测未来 1 小时却直接使用未来 1 小时的实测辐照度会得到不可上线的“理想指标”。若未来气象未知应使用当前观测、滞后量或 NWP 预报。全量标准化在切分前对全量 X 执行 fit_transform会让测试期均值和方差进入训练流程。双向填充bfill 会用未来值补过去缺口时间序列尤其危险。随机切分train_test_split(shuffleTrue) 会把相邻时刻打散使训练集“看见”测试期附近状态。夜间样本占比过高大量零功率夜间样本会让整体 RMSE 看起来更漂亮。建议同时报告白天/有效辐照时段指标。只调 K 不管特征距离模型对特征空间极其敏感。错误特征、冗余特征和量纲问题往往比 K 的影响更大。只报告单次指标至少保留按月份、天气、功率区间的分层误差必要时做滚动回测。忽略计算复杂度KNN 预测阶段需要查询历史样本。数据规模很大时应考虑 KDTree/BallTree、样本压缩、近似近邻或分季节建库。11. 从“能跑”升级到“可部署”的增强方案增强方向做法价值物理特征太阳高度角、晴空辐照、晴空指数提升跨季节可解释性动态特征辐照变化率、功率爬坡率、滚动均值捕捉云遮突变天气分型晴/多云/阴雨分别建模减少不同分布互相干扰滚动回测按周/月向前滚动训练与验证更接近真实上线特征权重PSO 同时优化特征权重让距离度量更符合业务多步预测direct 多模型或加入 NWP覆盖30分钟~数小时不确定性近邻分布分位数/共形预测输出区间而非单点12. 工程目录建议pso_knn_pv/├─ data/│ └─ pv_power_data.csv├─ outputs/│ ├─ prediction_result.csv│ └─ figures/├─ src/│ ├─ preprocess.py│ ├─ features.py│ ├─ pso_optimizer.py│ ├─ train.py│ └─ evaluate.py├─ config.py├─ requirements.txt└─ main.py将数据治理、特征工程、优化器和评价模块拆开后后续替换算法如 PSO-SVR、PSO-XGBoost、LSTM时不必重写整个项目。生产环境还应保存 scaler、feature_columns、best_k、best_p、训练数据时间范围和额定容量等元数据确保离线训练与在线推理一致。13. 结果解释什么时候 PSO-KNN 值得用PSO-KNN 适合中小规模数据、局部相似规律明显、需要快速建立强基线的光伏预测任务。它的优势是结构透明、训练成本低、容易解释“参考了哪些历史时刻”局限是预测阶段计算量随样本数增长对特征尺度和异常样本敏感且面对从未出现过的极端天气时外推能力有限。因此它更适合作为可靠基线、边缘侧轻量模型或集成学习中的一个成员而不是在所有数据规模和预测时域上替代深度时序模型。14. 一份可直接执行的实验检查清单时间戳已排序、去重采样间隔已核验缺失值处理不使用未来信息目标 shift 与预测步长完全一致所有输入特征在预测时刻真实可获得训练/验证/测试按时间隔离StandardScaler 只在允许的历史数据上 fitPSO 只看验证集不看测试集最终指标同时报告 MAE、RMSE、R²并查看残差夜间与白天指标至少做一次分层核验保存最优参数、随机种子、数据时间范围和结果文件。15. 总结PSO-KNN 的价值不在于把两个算法简单叠加而在于建立一套“局部相似建模 自动超参数搜索 严格时序验证”的完整方法。KNN 负责从历史数据中寻找与当前状态最接近的运行片段PSO 负责自动确定邻域规模和距离形状数据治理、时间特征、滞后变量和防泄漏设计则决定这套方法能否从实验代码走向真实电站。当你把本文示例替换为真实电站数据时建议先保持模型不变优先检查数据质量和时间因果关系随后再逐步加入天气分型、滚动回测、NWP、物理特征和不确定性区间。这样得到的每一次指标提升都更容易解释、复现和验证。附录运行环境建议环境Python 3.10NumPy、pandas、scikit-learn、matplotlib。KNeighborsRegressor、StandardScaler 与误差指标均来自 scikit-learn。实际部署前请锁定依赖版本并在新版本升级后重新执行回归测试。说明文中的实验曲线与指标为固定随机种子生成的合成数据演示用于验证流程与代码结构不冒充真实光伏电站测试结果。
