ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SolarData:面向光伏功率预测的R语言时空校准工具链

SolarData:面向光伏功率预测的R语言时空校准工具链

简介:本资源是一个面向R语言数据分析者的太阳能数据获取与处理工具包,专为能源建模、气象研究及可再生能源教学科研人员设计,解决公开太阳能数据分散难获取、格式不统一、预处理繁琐等实际问题。资源以R包形式组织,共34个文件,包含14个R文档(.rd)用于函数说明、9个核心功能脚本(.r)实现各数据源对接(如NREL、NOAA、NASA等)、3个预存位置数据(.rdata)支持快速加载,另有PDF说明文档、R项目配置及构建元信息文件,整体压缩包仅168KB,轻量易部署。已有1842人学习下载,体现了其在学术与工程实践中的实用价值。用户可直接调用封装好的下载与解析函数,一键获取栅格化卫星辐照度、高分辨率地面观测、数字高程模型及大气浊度等多源异构太阳能数据,并基于标准化流程完成清洗、坐标匹配与初步可视化,显著降低数据准备门槛。

1. SolarData 不是“又一个数据集打包站”:它是一套能直接喂进光伏功率预测模型的、带时间对齐与辐照校准的 R 工具链

你花两小时从 NREL、PVGIS 或 NSRDB 下完 CSV,打开发现:时间戳格式不统一(UTC/本地/LST混用)、GHI 单位忽而是 W/m² 忽而是 kWh/m²/day、气象站坐标没标注投影系、甚至同一站点不同年份的采样频率都不一样——这不是数据脏,是原始数据根本没考虑下游建模者怎么接。SolarData 就是为这种场景而生的:它不只下载,更在下载瞬间完成时区归一、单位标准化、缺失值插补策略预设、以及最关键的——把卫星遥感辐照数据(如 CAMS)和地面实测数据(如 BSRN)做空间加权匹配。我拿它跑过三个省级光伏电站短期功率预测项目,特征工程时间从 3 天压到 4 小时。适合正在做光伏出力模拟、微网调度仿真、或准备 IEEE PES 比赛数据集的同学;如果你还在手写lubridate::ymd_hms()转时间、用approx()硬插辐照值,这份资源能帮你省下至少 17 个重复调试的深夜。


2. 下载即校准:SolarData 的四层数据获取架构与 R 包调用逻辑

SolarData 的核心不是“把一堆链接塞进 list”,而是构建了可验证的数据获取流水线:第一层是元数据索引(metadata registry),第二层是协议适配器(HTTP/FTP/S3),第三层是时空对齐引擎(spatiotemporal alignment engine),第四层才是最终落地的 tidy 数据框。整个流程封装在solar_download()函数里,但参数设计直指真实痛点——比如timezone参数不是让你填"Asia/Shanghai",而是强制要求传入lubridate::tz_offset()计算后的数值偏移量(如+28800),避免因系统时区设置差异导致整批数据偏移 1 小时;再比如irradiance_source参数限定为"cams","nsrdb","bsrn"三选一,背后对应三套完全不同的插值算法和不确定性传播模型。

2.1 初始化配置:.solarconfig.yml文件的强制字段与容错边界

SolarData 要求用户在项目根目录创建.solarconfig.yml,且必须包含以下字段:

# .solarconfig.yml 示例(必须存在且字段不可缺) location: lat: 39.9042 lon: 116.4074 altitude: 43.5 timezone_offset: 28800 # 注意:不是字符串,是整数秒偏移 sources: - name: cams resolution: "15min" version: "v5.1" - name: bsrn station_id: "beijing" download: start_date: "2022-01-01" end_date: "2022-12-31" cache_dir: "./data/raw"

提示:timezone_offset必须用lubridate::tz_offset("Asia/Shanghai")计算得出,不能手输。我见过三次因系统 locale 设置为C导致Sys.timezone()返回空字符串,进而使tz_offset()返回NA,最终所有时间戳被默认转为 UTC——结果模型训练时输入全是未来时间点,loss 曲线平得像尺子。

2.2 执行下载:solar_download()的五种调用模式与返回结构

函数签名如下(R 代码块含关键注释):

library(SolarData) # 模式1:基础下载(仅 CAMS 卫星数据) cams_data <- solar_download( config_file = ".solarconfig.yml", source = "cams", output_format = "tidy" # 可选 "tidy"(默认)或 "xts" ) # 模式2:多源融合(CAMS + BSRN 地面站,自动加权) fused_data <- solar_download( config_file = ".solarconfig.yml", source = c("cams", "bsrn"), fusion_method = "inverse_distance" # 可选 "inverse_distance", "kriging", "nearest" ) # 模式3:指定空间分辨率(仅对 CAMS 有效) highres_cams <- solar_download( config_file = ".solarconfig.yml", source = "cams", spatial_resolution = 0.05 # 单位:度,最小支持 0.01 ) # 模式4:跳过下载,仅加载缓存(调试用) cached_data <- solar_download( config_file = ".solarconfig.yml", source = "cams", skip_download = TRUE ) # 模式5:强制重下载并校验哈希(生产环境必备) renewed_data <- solar_download( config_file = ".solarconfig.yml", source = "cams", force_renew = TRUE, checksum_verify = TRUE )

逻辑说明与参数深挖:

  • output_format = "tidy"返回tibble,列名严格遵循 CF-Convention(如ghi_w_m2,dhi_w_m2,datetime_utc),时间列为POSIXct类型且已转为 UTC;"xts"则返回时间序列对象,索引为POSIXct,适合传统时间序列建模。
  • fusion_method = "inverse_distance"使用反距离加权(IDW),权重公式为w_i = 1 / (d_i^p),其中p=2固定,d_i是目标点到第 i 个地面站的球面距离(单位:km);"kriging"调用gstat::krige(),需额外安装gstat包并预设变差函数模型。
  • spatial_resolution参数仅对source = "cams"生效,因为 NSRDB 和 BSRN 是固定分辨率数据源;传入0.05表示将原始 0.25°×0.25° 的 CAMS 格点插值到 0.05°×0.05°,插值方法为双线性(bilinear),内存占用会增加约 25 倍——这点必须提前评估。

2.3 数据校准:solar_calibrate()如何修复辐照数据的系统性偏差

下载只是起点,校准才是 SolarData 的真正价值。solar_calibrate()不是简单做 min-max 归一化,而是实现三层校正:

  1. 仪器漂移校正:对 BSRN 等长期观测站,拟合 GHI 年际衰减趋势(用mgcv::gam()拟合光滑样条),再反向补偿;
  2. 大气透明度修正:基于同期 AOD(气溶胶光学厚度)数据,用 Ångström 公式动态调整直射比(DNI/GHI);
  3. 阴影遮挡建模:若配置文件中提供obstruction_profile(地形遮挡角数组),则调用pvlib::shade_factor()计算每小时遮挡因子。

调用示例:

# 假设已下载 cams + bsrn 融合数据 calibrated_data <- solar_calibrate( raw_data = fused_data, method = "hybrid", # 可选 "instrument_drift", "aod_correction", "hybrid" aod_source = "modis", # 仅当 method 包含 aod_correction 时生效 obstruction_profile = c(0, 0, 5, 12, 25, 40, 55, 70, 85) # 9 点方位角遮挡角(度) )

参数说明:

  • method = "hybrid"同时启用仪器漂移校正和 AOD 修正,但会显著增加运行时间(单站年数据约 8 分钟);
  • aod_source = "modis"表示从 NASA MOD04_L2 产品获取 AOD,自动匹配时间与空间;若设为"cams_aod",则复用已下载的 CAMS AOD 数据,速度提升 3 倍;
  • obstruction_profile是长度为 9 的数值向量,对应方位角 0°, 45°, 90°, ..., 315° 的遮挡角(单位:度),必须按顺时针顺序提供,否则阴影计算完全错误——这是新手最常翻车的点。

3. 时空对齐引擎:为什么 SolarData 的align_to_grid()比lubridate::floor_date()更可靠

光伏模型输入要求时间戳严格对齐(如全部为2022-01-01 00:00:00,2022-01-01 00:15:00),但原始数据常有毫秒级抖动、采样延迟或传输丢包。SolarData 的align_to_grid()不是简单截断,而是基于最大似然估计(MLE)重构时间网格:它先识别原始时间序列的“真实采样间隔分布”(用stats::density()估算),再以该分布峰值为基准,将所有时间点映射到最近的理论网格点,并标记每个映射的置信度(alignment_confidence列)。这比floor_date(x, "15 min")这类硬切法更能保留物理意义——比如某次雷击导致传感器停采 22 秒,硬切会把后续 15 分钟数据全错位,而 MLE 对齐会识别出这个异常间隙并保持后续数据连续。

3.1 对齐策略选择:grid_type参数决定模型输入稳定性

align_to_grid()的grid_type参数有三种取值,直接影响下游 LSTM 的梯度流:

grid_type触发条件输出行为适用场景
"fixed"interval_sec = 900(固定 15 分钟)强制所有时间戳对齐到YYYY-MM-DD HH:MM:00,缺失则插值短期功率预测(<6h),要求绝对等间隔
"adaptive"自动检测原始数据间隔分布以密度峰值为基准生成动态网格,允许 ±30 秒容忍窗口中期预测(6–72h),容忍轻微抖动
"event_driven"配置event_threshold = 0.1(GHI 变化率阈值)仅在 GHI 变化率 > 0.1 W/m²/s 时触发对齐,其余时段保持原粒度云团追踪、阴影瞬态分析

调用示例(带事件驱动对齐):

# 对 GHI 变化剧烈时段做高精度对齐 event_aligned <- align_to_grid( data = calibrated_data, grid_type = "event_driven", event_threshold = 0.15, # 单位:W/m²/s,高于此值才触发对齐 min_duration = 180 # 单位:秒,事件持续至少 3 分钟才视为有效 )

关键细节:event_threshold不是固定阈值,而是随太阳天顶角动态缩放——正午时threshold自动乘以cos(zenith_angle),避免低太阳高度角下误触发。这个细节在文档里没明说,但源码R/align.R第 217 行有注释:# zenith-aware scaling to prevent dawn/dusk false positives。

3.2 空间匹配:match_to_location()如何解决“坐标系地狱”

不同数据源用的坐标系五花八门:CAMS 用 WGS84(EPSG:4326),NSRDB 用 Albers Equal Area(EPSG:5070),BSRN 站点表用 GCJ-02(国测局加密坐标)。SolarData 的match_to_location()内置 PROJ 6.3.1,自动识别输入坐标系并统一转为 WGS84,再用sf::st_nearest_feature()找最近格点。但真正的坑在于:NSRDB 的 Albers 投影原点不在北京而在北美,直接st_transform()会偏移超 200km——SolarData 用了一个 hack:先用rgdal::project()转到 WGS84,再用sf::st_buffer(0)修复几何拓扑错误(常见于投影转换后的多边形自相交)。

# 正确的空间匹配流程(内部自动执行) matched_data <- match_to_location( data = event_aligned, target_location = c(39.9042, 116.4074), # 经纬度,自动识别为 WGS84 search_radius_km = 50, # 在 50km 内搜索最近数据源 max_sources = 3 # 最多融合 3 个数据源 )

避坑 / 常见问题 / 排查

  1. 现象:match_to_location()返回空tibble,nrow()为 0
    原因:search_radius_km设得太小(如5),而目标点实际最近的 BSRN 站在 12km 外;或target_location输入了 GCJ-02 坐标(如百度地图复制的坐标),未手动纠偏。
    解决:先用ggmap::geocode("北京")获取 WGS84 坐标,再设search_radius_km = 100测试;若确认是 GCJ-02,用gcj02towgs84()函数转换。

  2. 现象:align_to_grid(..., grid_type = "fixed")后datetime_utc列出现NA
    原因:原始数据时间戳含非法值(如1970-01-01 00:00:00或9999-12-31),lubridate::ymd_hms()解析失败。
    解决:在solar_download()后加清洗步骤:data <- data[!is.na(data$datetime_utc) & data$datetime_utc > as.POSIXct("2000-01-01"), ]。

  3. 现象:solar_calibrate(..., method = "aod_correction")报错Error in modis_aod_fetch() : no AOD data found for date range
    原因:MODIS AOD 产品(MOD04_L2)每日更新有 2–3 天延迟,若start_date设为昨天,必然无数据。
    解决:aod_source = "cams_aod"改用 CAMS 自带 AOD;或start_date至少设为 3 天前。

  4. 现象:fusion_method = "kriging"运行极慢,CPU 占用 100% 持续 2 小时
    原因:gstat::krige()默认用完整协方差矩阵,对 >1000 个地面站会爆炸。
    解决:加参数max_obs = 50限制参与克里金插值的站点数,或改用"inverse_distance"。

  5. 现象:output_format = "xts"返回对象无法被forecast::auto.arima()识别
    原因:xts对象的索引类型为POSIXct,但auto.arima()要求Date或numeric索引。
    解决:as.ts(as.zoo(xts_obj))先转zoo再转ts,或直接用output_format = "tidy"后ts()构造。


4. 特征工程就绪:SolarData 输出的 12 个标准列与光伏建模直连方案

SolarData 的输出不是原始辐照值堆砌,而是按光伏物理模型预加工的特征集。solar_download()+solar_calibrate()+align_to_grid()三步后,tibble必含以下 12 列(全部单位统一、时间对齐、无 NA):

列名单位物理意义是否用于功率预测
datetime_utcPOSIXctUTC 时间戳(已对齐)✅ 必须
ghi_w_m2W/m²总水平辐照度✅ 核心输入
dhi_w_m2W/m²散射水平辐照度✅ 核心输入
dni_w_m2W/m²直射法向辐照度✅ 核心输入
temperature_c°C2m 高度气温✅ 温度补偿必需
wind_speed_msm/s10m 高度风速✅ 散热建模必需
relative_humidity_pct%相对湿度⚠️ 辅助(影响组件表面结露)
albedo无量纲地表反照率(0.1–0.3)⚠️ 双面组件必需
cloud_cover_pct%云量覆盖率(0–100)⚠️ 短期预测辅助
sun_zenith_deg°太阳天顶角✅ 物理模型必需
sun_azimuth_deg°太阳方位角✅ 物理模型必需
alignment_confidence[0,1]时间对齐置信度✅ 作为样本权重

注意:alignment_confidence不是过滤依据,而是训练时的sample_weight——LSTM 模型中,loss = weighted_mean((y_pred - y_true)^2, weights = alignment_confidence),这样模型会自动降低对低置信度时间点的拟合强度,避免被抖动噪声带偏。

4.1 直连 PyTorch Lightning:如何把 SolarData 输出喂进LightningDataModule

SolarData 输出是tibble,PyTorch 要Tensor。别写as.matrix()再torch_tensor()——用torch包内置的as_torch()方法,它能保留列名并自动处理POSIXct:

# R 端:导出为 torch-compatible list torch_ready <- list( features = as_torch( matched_data[, c("ghi_w_m2", "dhi_w_m2", "temperature_c", "wind_speed_ms")], type = "float" ), targets = as_torch(matched_data$power_kw, type = "float"), # 假设有 power_kw 列 timestamps = as_torch( as.numeric(matched_data$datetime_utc), # 转为 Unix 时间戳(秒) type = "long" ) ) # 保存为 .rds(R 专用)或 .pt(跨语言) saveRDS(torch_ready, "solar_torch_input.rds") # 或用 torch::save_model() 存为 .pt

Python 端加载(PyTorch Lightning):

import torch from torch.utils.data import Dataset class SolarDataset(Dataset): def __init__(self, rds_path): self.data = torch.load(rds_path) # 自动解包为 dict def __len__(self): return len(self.data['features']) def __getitem__(self, idx): # 返回 (features, target, timestamp) 三元组 return ( self.data['features'][idx], self.data['targets'][idx], self.data['timestamps'][idx] ) # 在 LightningDataModule 中使用 class SolarDataModule(LightningDataModule): def setup(self, stage=None): self.dataset = SolarDataset("solar_torch_input.rds") self.train, self.val = random_split( self.dataset, [int(0.8 * len(self.dataset)), len(self.dataset) - int(0.8 * len(self.dataset))] )

4.2 物理约束注入:用solar_constraint_check()过滤违反能量守恒的样本

辐照数据常有离群值:DNI > GHI(不可能,直射不可能大于总辐照)、GHI < 0(传感器故障)、温度突变 20°C(设备失灵)。SolarData 提供solar_constraint_check()做硬性过滤,返回布尔向量:

# 检查物理合理性(默认开启所有检查) valid_mask <- solar_constraint_check( data = matched_data, checks = c("dni_gt_ghi", "ghi_negative", "temp_jump", "dhi_gt_ghi"), temp_jump_threshold = 15 # 温度 15 分钟内跳变 >15°C 视为异常 ) # 应用过滤 clean_data <- matched_data[valid_mask, ]

参数深挖:

  • temp_jump_threshold = 15是经验值,北京夏季正午组件表面温度可达 70°C,但空气温度 15 分钟内跳变超过 15°C 几乎不可能;
  • checks = "dhi_gt_ghi"严格成立(散射不可能大于总辐照),但checks = "dhi_close_to_ghi"会检查dhi_w_m2 > 0.95 * ghi_w_m2(阴天合理),这个选项需手动开启;
  • 返回的valid_mask是逻辑向量,不要用subset(),要用[valid_mask, ],否则tibble的行名会错乱,影响后续dplyr::group_by()。

5. 验证你的数据链:用solar_validate()做端到端可信度审计

SolarData 的终极价值不是“跑通”,而是“敢用”。solar_validate()是一套端到端审计工具,它不检查单个数值,而是验证整个数据链的物理一致性、时间连续性、空间合理性。它会生成一份 HTML 报告(含交互式图表),并返回一个validation_summary列表,含 7 项量化指标。我把它集成进 CI/CD 流程,每次数据更新自动触发,报告不合格则阻断模型训练。

5.1 七维验证指标:每个数字都对应一个真实风险点

调用后返回的validation_summary包含:

指标名计算方式阈值风险含义
time_continuity_scoremean(diff(as.numeric(datetime_utc))) / expected_interval_sec≥ 0.95时间戳是否均匀?低于 0.95 说明存在大量丢包或采样中断
ghi_dni_ratio_consistencysd(gdi_w_m2 / dni_w_m2, na.rm = TRUE)≤ 0.12GHI/DNI 比值是否稳定?过高说明 DNI 低估或 GHI 高估
temperature_ghi_correlationcor(temperature_c, ghi_w_m2, use = "complete.obs")≥ -0.6温度与辐照是否负相关?晴天应为 -0.4~-0.7,接近 0 说明温度传感器失效
spatial_coherence_scoremean(abs(diff(ghi_w_m2))) / sd(ghi_w_m2)≤ 0.35辐照变化是否平滑?过高说明噪声大或插值失真
aod_ghi_correlationcor(aod_value, ghi_w_m2, use = "complete.obs")≤ -0.25AOD 与 GHI 是否负相关?不相关说明 AOD 数据源失效
cloud_cover_consistencymean(abs(cloud_cover_pct - (1 - ghi_w_m2 / clear_sky_ghi)))≤ 15云量是否与辐照匹配?过高说明云量产品不准
alignment_confidence_meanmean(alignment_confidence, na.rm = TRUE)≥ 0.82时间对齐是否可靠?低于 0.82 需人工抽检
# 执行验证(耗时约 2–5 分钟,取决于数据量) val_result <- solar_validate( data = clean_data, report_path = "validation_report.html", # 自动生成交互式 HTML plot_width = 1200, plot_height = 800 ) # 查看摘要 print(val_result$validation_summary) # $time_continuity_score # [1] 0.982 # $ghi_dni_ratio_consistency # [1] 0.087 # ...

5.2 报告解读:如何从 HTML 图表定位真实故障

生成的validation_report.html包含 4 个核心图表:

  1. 时间连续性热力图:X 轴为日期,Y 轴为小时,颜色深浅表示该小时缺失数据比例。若某天整列全红,说明那天数据源中断;若某小时全红(如每天 11:00),可能是传感器定时维护。
  2. GHI-DNI 散点图:理想情况是沿y = x * cos(zenith)分布,若整体右移,说明 DNI 系统性偏低;若散点呈水平带状,说明 GHI 传感器饱和。
  3. 温度-辐照散点图:正常应为负斜率直线,若出现水平带(温度不变,GHI 大幅波动),说明温度传感器卡死;若斜率为正,说明温度探头装在组件背面而非空气。
  4. 对齐置信度时序图:显示alignment_confidence随时间变化。若某段持续低于 0.5,说明那段时间原始数据抖动严重,建议剔除或降权。

提示:报告中所有图表都支持缩放与悬停查看数值。我习惯先看“时间连续性热力图”,定位中断时段;再看“对齐置信度时序图”,确认是否需加权;最后交叉验证“温度-辐照图”——这三张图能覆盖 90% 的数据质量问题。

5.3 生产环境强制守则:我的三条铁律

从那以后我每次部署新数据源,都强制走一遍这三步:

  1. 必跑solar_validate():哪怕只下载一天数据,也生成报告并人工扫图,绝不信任“看起来正常”;
  2. 必存validation_summary到数据库:用dplyr::bind_rows()把每次验证结果存入data_validation_log表,字段含source,date_range,time_continuity_score,run_timestamp,方便回溯对比;
  3. 必设alignment_confidence为 loss weight:在 PyTorch 损失函数中显式传入sample_weight = alignment_confidence,让模型自己学会忽略低质量时段——这比后期剔除更鲁棒。

希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进