☰
GEFCOM2014负荷预测实战:R语言数据清洗与分位数回归指南
2026/10/10 14:25:05 网站建设 项目流程

简介:GEFCOM2014-EPFL能源负荷预测数据包聚焦电力系统短期负荷预测,面向数据科学研究者、R语言用户及能源电力从业人员。包体约111.53MB,提供多地区小时级负荷记录,可作为时间序列分析、特征工程与机器学习建模的实践数据集。已有1424人学习使用。借助R中的ts对象、forecast包、caret包及ggplot2,可完成趋势分解、ARIMA或状态空间模型构建、随机森林/SVM/神经网络对比、交叉验证与误差评估。资源包含从数据导入、可视化到模型优化的完整探索路径,适合教学练习及赛题复现;结合温度、节假日等外生变量还能进一步验证特征工程对精度的提升。对希望掌握负荷预测全流程的读者而言,是一份可直接上手的数据基础与实操指引。

1. 能源负荷预测第一道坎:GEFCOM2014 数据对齐与这个 EPFL 资源包

GEFCOM2014 是能源负荷预测圈绕不开的公开数据集,但真正动手时你会发现,最耗时间的不是调模型,而是把时间戳、温度和节假日对齐到同一条时间线上。gefcom2014-epfl这个资源包把 EPFL 整理的负荷数据和 R 脚本绑在一起,省掉了数据清洗环节的重复劳动,适合刚入行的 R 用户和需要快速建立 baseline 的工程师。这篇文章会从数据读取、特征构造、基线模型到概率评价完整过一遍,最后给出我在这个数据集上踩过的五个坑。整篇的代码都是 base R 加少量时间序列包,不依赖特定 IDE,复制到 RStudio 就能跑。

2. 资源包拆解:先看清数据文件,再谈建模

2.1 目录结构与 R 依赖准备

这类资源包通常长这样:data/下面是负荷和温度的 CSV,scripts/下面是数据清洗和建模的 R 脚本,还会有一个README说明字段含义。下载解压后第一件事不是打开 RStudio,而是打开 README 看字段定义,因为 GEFCOM2014 的数据在历次复现中被改动过多次,有些版本的load列名是小写,有些是LOAD,还有的将缺失值写成字符串"NA"而不是真正的NA。

list.files("gefcom2014-epfl", recursive = TRUE)

这条命令会递归列出所有文件,让你先掌握整体结构。如果发现scripts/下已经有别人写好的clean_data.R,我建议先跑一遍再自己重写,因为官方整理的数据格式很可能与你下载到的版本有细微差异,直接重写反而容易漏掉细节。R 依赖建议先装lubridate、zoo、quantreg、xgboost。这里有个小细节:quantreg依赖的SparseM包在某些 Linux 系统上有编译问题,如果安装失败,先单独安装SparseM再装quantreg。

install.packages(c("lubridate", "zoo", "quantreg", "xgboost"))

安装时不要一次装全部,因为xgboost在 Windows 上通常有预编译包,但在老版本 R 上可能链接失败。装完以后用packageVersion("xgboost")确认版本,我遇到过 1.6 和 1.7 之间quantile目标函数的参数名不兼容,导致同样的代码在不同电脑上结果不同。

2.2 数据分区与变量名识别

GEFCOM2014 的负荷预测数据集,一般会按训练和测试分成两个文件。训练文件包含历史负荷和对应的温度观测,测试文件只给时间点与温度预报,不给你真实负荷,需要你自己预测。常见列名如下表所示,但具体以你下载到的 README 为准:

文件常见列说明
load_train.csvtimestamp,load,temp训练期的小时级负荷与温度观测
load_test.csvtimestamp,temp测试期只有时间与温度预报
temp_obs.csvtimestamp,temp_obs温度观测值,可能与负荷文件分开提供
temp_forc.csvtimestamp,temp_forc温度预报值,模拟真实业务场景
train_raw <- read.csv("data/load_train.csv", header = TRUE, stringsAsFactors = FALSE) str(train_raw) head(train_raw)

str()能直接暴露时间列有没有被读成字符串、负荷列是不是numeric;head()则让你快速扫一眼日期格式。很多新手在这一步跳过,结果后面as.POSIXct()全错。我一般会再跑一条summary(train_raw$load),确认没有负数或极端尖峰,负荷数据出现负数十有八九是计量单位或者 NA 被填成了 0。如果load列有负值,用which(train_raw$load < 0)定位到具体行,别急着删,先看那个时间点前后有没有记录异常。

2.3 时间戳重对齐:时区、夏令时与小时序列

GEFCOM2014 的数据是按小时记录的,但北美地区有夏令时切换,3 月的某天会少一小时,11 月的某天会多一小时。如果直接as.POSIXct却没处理时区,会出现连续两个相同的小时戳,或者缺少某个小时。R 里最常用的做法是统一用 UTC 解析,再按业务时间轴对齐。

library(lubridate) train_raw$datetime <- as.POSIXct(train_raw$timestamp, tz = "UTC") train_raw$hour <- hour(train_raw$datetime) train_raw$weekday <- wday(train_raw$datetime, label = TRUE) # 检查重复时间戳 dup_idx <- duplicated(train_raw$datetime) sum(dup_idx)

如果sum(dup_idx)大于 0,说明原始数据里混入了夏令时未校准的时间,或者同一小时有两个记录。处理办法是先把重复项打出来,看是两个样本重合还是时间跳变。若是夏令时切换导致的缺失,用seq.POSIXt生成完整小时序列,再left_join回原数据,把缺的负荷行留空,让后续的缺失值处理统一接管。

这里有个玄学问题:你永远不知道发布者当初用的什么时区。所以不要盲目相信代码里的tz参数,每次解析后都手动打印head(train_raw$datetime, 5)和tail(train_raw$datetime, 5),与原始字符串逐一比对。血泪经验是:我曾在某次复现中因为时区差了 8 小时,导致周末哑变量全部错位,模型在凌晨时段残差异常放大,排查了两天才发现是as.POSIXct默认用了本地时区。

2.4 温度序列的合并方式

负荷预测里温度是最强外因,但温度观测和负荷往往不在同一张表里。资源包若单独给了temp_obs.csv和temp_forc.csv,需要按datetime精确连接。连接时要用左连接,保证负荷表每一行都保留。

temp_obs <- read.csv("data/temp_obs.csv", header = TRUE) temp_obs$datetime <- as.POSIXct(temp_obs$timestamp, tz = "UTC") df <- merge(train_raw, temp_obs[, c("datetime", "temp_obs")], by = "datetime", all.x = TRUE)

merge的by参数必须明确写清楚。如果不写,R 会按所有同名列做笛卡尔积,数据量稍大就直接内存爆炸。all.x = TRUE是保留负荷侧所有行,配合后面的缺失值处理,比inner_join更能暴露数据断点。我一般还会检查合并后行数有没有变:

stopifnot(nrow(df) == nrow(train_raw))

stopifnot是 R 里一个廉价断言,行数不一致就立刻报错,避免你带着隐患继续往下跑。

温度观测和温度预报要分开列。测试集上只有预报值,模型如果混合使用观测值和预报值,上线时会因为温度误差而被带偏。我在做项目时会把temp_obs和temp_forc都放进特征矩阵,训练期用观测值,测试期用预报值,让模型自己学温度不确定性的影响,效果比只塞一个温度列更稳。

2.5 缺失值的版图:先看缺在哪,再决定怎么填

数据清洗里最盲目的做法是全局na.omit(),直接删掉一整行。GEFCOM2014 的负荷和温度缺失往往集中在特定时段,比如某些天的凌晨或极端天气前后。删除前先画缺失版图。

df$date <- as.Date(df$datetime) daily_missing <- aggregate(is.na(df$temp_obs), by = list(df$date), FUN = mean) colnames(daily_missing) <- c("date", "temp_na_ratio") daily_missing[daily_missing$temp_na_ratio > 0.1, ]

这里的aggregate(..., FUN = mean)会把布尔向量转成缺失比例。如果缺失集中分布在连续几天,说明是数据源断档,后续填充要谨慎;如果是零星几点,线性插值就够用。不要一上来就用同一个填法覆盖全部时间范围。还要检查负荷本身的缺失:

load_na_idx <- which(is.na(df$load))

如果负荷列有缺失,把它当作待预测目标会让训练集和验证集的关系变复杂。最简单的做法是训练时先删掉这些行,但验证时要把缺失位置的预测单独留下来与后续真实值对比,否则你无法知道模型在缺失段上表现如何。

另外,温度缺失超过 3 天的连续时段,用插值等于编数据,不如直接用温度预报值做替代。负荷数据里有一句行业老话:温度是最大的外因,但温度缺失的地方往往就是极端天气发生的地方。你插值抹平的恰恰是模型最该学习的极端样本。

3. 特征工程与基线模型:先让残差站得住

3.1 负荷序列的周期性特征:小时、星期与双驼峰

电力负荷曲线在大多数地区呈现早晚双峰:早上起床后用电上升,傍晚下班后达到最高点,深夜跌到低谷。这个形状在 GEFCOM2014 数据里同样存在。因此小时是最基础的特征,但不能直接把小时数值 0-23 丢给线性模型,因为线性模型会认为 0 和 23 之间距离是 23,而实际上它们只隔一小时。用正弦余弦编码能把循环关系保留下来。

df_sorted <- df[order(df$datetime), ] df_sorted$hour_sin <- sin(2 * pi * df_sorted$hour / 24) df_sorted$hour_cos <- cos(2 * pi * df_sorted$hour / 24) df_sorted$weekday_sin <- sin(2 * pi * as.numeric(df_sorted$weekday) / 7) df_sorted$weekday_cos <- cos(2 * pi * as.numeric(df_sorted$weekday) / 7)

hour如果是从lubridate::hour()得到的,范围是 0-23,除以 24 后乘以2 * pi,正好一周循环。weekday是因子,先用as.numeric()转成 1-7,再同样做循环编码。这个技巧对线性回归和 GBM 都有用,尤其是线性回归,能够显著降低对小时边界的敏感度。如果不用 sin/cos,而直接把 0 到 23 的整数喂进lm(),模型会把 23 点视为离 0 点最远的点,但实际上它们是相邻的。

滞后特征也是负荷预测的标配。负荷在时间上高度自相关:今天上午 10 点的负荷,大概率与昨天上午 10 点接近,也接近上周同一天上午 10 点。所以滞后 24 小时和滞后 168 小时是必加的特征。

df_sorted$lag24 <- dplyr::lag(df_sorted$load, 24) df_sorted$lag168 <- dplyr::lag(df_sorted$load, 168) df_sorted$temp_ma7 <- zoo::rollmeanr(df_sorted$temp_obs, 7, fill = NA)

dplyr::lag()是按行位置错位,所以必须先order()保证时间升序。lag168后面的 168 个样本会是NA,导致它们进不了模型。这不是问题,只要保证你的训练集按时间顺序过滤掉了这些行。但如果先做随机重排再做lag(),滞后特征就全部错乱,这是最容易犯的隐蔽错误。

3.2 线性回归基线:系数符号与残差诊断

我必须在任何复杂模型之前强制自己跑一个线性回归,因为线性回归能暴露特征构造的合理性。比如温度对负荷的影响,理想情况下应该是正系数——温度升高,制冷负荷增加;但如果你的数据是从暖气为主的地方采集的,温度升高反而降低负荷,系数符号也会不同。看到系数符号与直觉不符时,先别调模型,回头查数据。

model_lm <- lm(load ~ hour_sin + hour_cos + weekday_sin + weekday_cos + temp_obs + lag24 + lag168, data = df_sorted, na.action = na.exclude) summary(model_lm)

na.action = na.exclude比默认na.omit更讲究:它让predict()返回和原数据等长的向量,NA 位置保持 NA,方便你计算误差时自动跳过。summary()里重点看两个地方:temp_obs的系数符号,以及lag168的显著性。如果lag168不显著,说明跨周信息没有用,反而要小心模型过拟合。残差诊断我一般会画一张预测值与真实值的散点图,重点关注高负荷段是否系统性偏低,那通常是高峰特征没有构造好。

3.3 梯度提升:参数选择与早停

当线性回归的残差已经不再有明显的时间模式后,再上梯度提升。在 R 里调xgboost时,小时级数据会把类别特征直接做成 0/1 哑变量。GEFCOM2014 这个量级的数据,lightgbm 的直方图算法优势不明显,xgboost反而更容易控制过拟合。我一般先用一组固定参数跑 baseline,不做搜索,目的是看特征能不能复用。

library(xgboost) feature_cols <- c("hour_sin", "hour_cos", "weekday_sin", "weekday_cos", "temp_obs", "lag24", "lag168") train_df <- df_sorted[complete.cases(df_sorted[, feature_cols]), ] dtrain <- xgb.DMatrix( data = as.matrix(train_df[, feature_cols]), label = train_df$load ) params <- list( objective = "reg:squarederror", eta = 0.05, max_depth = 6, subsample = 0.8, colsample_bytree = 0.8, min_child_weight = 3 ) model_xgb <- xgb.train(params, dtrain, nrounds = 300, verbose = 0)

objective = "reg:squarederror"对应常规 RMSE 损失。如果你想输出分位数,可以改成"reg:quantileerror",并设置quantile_alpha参数,但这里有个坑:不同版本 xgboost 的 quantile 参数名不一样,有的叫quantile_alpha,有的直接叫alpha,运行前用xgb.parameters确认。eta = 0.05和nrounds = 300是保守组合,适合先验证特征。min_child_weight = 3是防止 GBM 在负荷尖峰上过分拟合,因为负荷数据里极端高温日的样本量很少,树很容易把单个异常样本学死。

3.4 分位数预测:从单点输出到区间

如果业务只要求一个预测值,上面的模型已经够了。但 GEFCOM2014 这类比赛以及电力系统实际调度,都需要知道预测的不确定性。R 自带的quantreg包是最稳妥的分位数回归实现,我一般会先跑几个关键分位数作为概率预测基线。

library(quantreg) model_qr <- rq(load ~ hour_sin + hour_cos + weekday_sin + weekday_cos + temp_obs + lag24 + lag168, tau = c(0.05, 0.5, 0.95), data = df_sorted, na.action = na.exclude) pred_qr <- predict(model_qr, newdata = df_sorted)

tau是分位数向量,rq()会对每个分位数独立拟合。这种做法的好处是稳定、可解释,缺点是当变量间关系非线性时偏差可能较大。实际应用中,我经常先用quantreg做一个概率输出基线,再让 GBM 去追中间值,两边对照评估。注意predict()出来的矩阵列顺序与tau一致,第一列是 0.05,第二列是 0.5,第三列是 0.95,后续计算损失时不要搞反。

4. 评价指标与验证脚本:用分位损失替代单一 RMSE

4.1 Pinball Loss 的公式与 R 实现

RMSE 只衡量点预测的平均误差,但分位数预测需要用分位损失来评估。分位损失也叫 Pinball Loss,它对预测值落在真实值上方和下方惩罚不对称,系数由分位数tau决定。当tau = 0.5时损失退化为 MAE,当tau = 0.05时,它主要惩罚预测值高于真实值的情况,因为 5% 分位数的意思是真实值有 5% 的概率低于这个值。

pinball_loss <- function(y, q_pred, tau) { diff <- y - q_pred loss <- ifelse(diff >= 0, tau * diff, (tau - 1) * diff) mean(loss) }

这个函数和数学公式一一对应。测试时用一个简单向量验证:pinball_loss(c(10, 10), c(9, 11), tau = 0.5),两个样本的绝对值误差都是 1,平均损失也是 1。如果算出来不对,说明diff符号判断错了。我在第一版写这个函数时把tau - 1写成了1 - tau,结果所有分位损失都变成负数,后面全盘错乱。

对多分位数模型,最终指标是各分位损失的加权平均。GEFCOM2014 官方评价时通常考察多个分位数,GEFCOM2014 的负荷预测赛道在官方评价时使用的不是单一 RMSE,而是把多个分位点上的 Pinball Loss 聚合起来。所以你在复现时,不要只报 RMSE,否则无法和其他公开结果对比。

4.2 滚动窗口回测:框架与参数

负荷预测是时间序列,随机划分训练集和测试集很容易高估模型表现。正确的做法是滚动前推验证:用前 N 天训练,预测后 M 天,然后整个窗口往前滑。这样能模拟真实业务里"你只有过去的数据,未来永远是待预测"的情况。

train_days <- 30 test_days <- 3 horizon <- 24 * test_days n_rows <- nrow(df_sorted) results <- list() for (start in seq(1, n_rows, by = horizon)) { train_end <- start + train_days * 24 - 1 test_start <- train_end + 1 test_end <- test_start + horizon - 1 if (test_end > n_rows) break fit <- lm(load ~ hour_sin + hour_cos + weekday_sin + weekday_cos + temp_obs + lag24 + lag168, data = df_sorted[start:train_end, ]) pred <- predict(fit, newdata = df_sorted[test_start:test_end, ]) actual <- df_sorted$load[test_start:test_end] results[[length(results) + 1]] <- data.frame( time = df_sorted$datetime[test_start:test_end], actual = actual, pred = pred, mid = df_sorted$load[test_start:test_end] # 真实值,用于计算误差 ) }

seq(1, n_rows, by = horizon)让起点每隔 3 天推进一次。循环里train_days * 24 - 1是为了对齐到整点,test_end超过总行数就停止。这个框架能防住两件事:一是避免未来数据泄露,二是强制你检查模型在长周期上的稳定性。实际跑的时候,我会把每轮预测结果存到 list 里,而不是直接在循环里算指标,方便后面统一画误差曲线。

在真实项目里,train_days是模型训练窗口,太小会让模型学不到季节模式,太大又会拖慢计算速度。我在 GEFCOM2014 上常用的窗口是 30 天,因为负荷的季节模式在月度尺度上相对稳定,30 天足够覆盖一个完整的周期变化。如果你要预测的是寒潮或热浪事件,窗口需要拉长到 90 天,否则模型没见过同样强度的极端温度。

4.3 季节性折叠与冷启动检验

滚动窗口回测还有一个变体:季节性折叠验证。具体做法是把数据按月份分组,每个月作为一次验证集,其余月份作为训练集。这能直接暴露模型对季节漂移的敏感度。

df_sorted$month <- month(df_sorted$datetime) for (m in unique(df_sorted$month)) { train_part <- df_sorted[df_sorted$month != m, ] test_part <- df_sorted[df_sorted$month == m, ] fit <- lm(load ~ hour_sin + hour_cos + weekday_sin + weekday_cos + temp_obs + lag24 + lag168, data = train_part) pred <- predict(fit, newdata = test_part) rmse_month <- sqrt(mean((pred - test_part$load)^2, na.rm = TRUE)) print(paste("Month", m, "RMSE:", round(rmse_month, 3))) }

这个做法比随机 K 折更贴近电力业务的验证习惯。如果某个月的 RMSE 明显偏高,再去看那个月是否包含节假日集中时段或极端天气。我曾在某个项目里发现 2 月误差大幅上涨,查了一下是因为那年春节在 2 月,而训练集里没有覆盖春节的负荷模式——这就是冷启动问题。资源包的原始数据若不包含目标年份的节假日,你需要用外部节假日日历补充,而不是让模型自己学。

5. 常见问题与避坑:五个让模型翻车的细节

5.1 时间戳偏移一小时后,周末标签全部错位

现象:用as.POSIXct直接转换后,周末和节假日的哑变量在凌晨时段提前一小时触发,白天预测误差不明显,深夜误差突然放大。

原因:原始文件的时区元数据可能不是 UTC,系统默认时区自动把时间往前挪了八小时。R 的as.POSIXct在未指定tz时会用系统时区,如果你在中国,Sys.timezone()返回Asia/Shanghai,同一个字符串"2012-08-01 00:00:00"在 UTC 和 Asia/Shanghai 下对应实际时刻完全不同。问题不在于时间本身错,而是后面的wday()提取出的星期几错位。

解决:读取后第一件事是打印head(datetime)并核对业务时间。如果发现整体偏移,重新指定tz;更稳妥的做法是在read.csv之后先as.character固定字符串,再用lubridate::ymd_hms(tz = "UTC")转换,转换后立即与原字符串逐行对比。

5.2 全局填充温度 NA,把极端天气直接抹平

现象:模型在识别高温和寒潮时完全失效,残差在极端温度日突然放大。

原因:用均值或前值填充了缺失的温度,而缺失往往集中在传感器离线的那几天,恰好就是极端天气发生期间。GEFCOM2014 的温度缺失不是完全随机,很多时候是温度传感器在极冷或极热条件下故障。你用均值填充,等于把所有极端样本拉回正常水平,模型当然学不到极端情况。

解决:先按date分组统计缺失比例,对缺失连续超过 3 天的时段,改用相邻年份同期的气象站数据插值,或者干脆把该段从训练集中剔除。如果资源包自带温度预报,优先用预报值做输入,而不是历史观测均值。我在处理时还会加一个is_temp_missing布尔特征告诉模型这个样本的温度不可靠,模型有时能自动调整加权。

5.3 分位数结果不单调:95% 分位小于 5% 分位

现象:predict(rq(...))出来的 0.95 分位数在某几个小时突然低于 0.05 分位数,画出来分位数曲线交叉。

原因:分位数回归在样本量小或特征维度高时,个别分位点的解出现局部不稳定;更常见的是在newdata里有 NA,na.action处理不一致导致对齐错位。还有可能是样本里存在极端离群点,而rq()对离群点的处理比lm()更敏感。

解决:先过滤掉所有特征含 NA 的行,再进predict()。检查分位数矩阵每一行是否单调递增,用apply(pred_qr, 1, is.unsorted)找出交叉点。如果仍然交叉,检查数据里是否有极端离群点,对离群点做分位缩尾处理。分位数交叉在实际比赛中会被直接判为无效提交,所以这种问题要提前在验证阶段捕捉。

5.4 节假日列表写死,导致跨年份预测失败

现象:模型在节假日日的预测普遍偏低,误差呈明显的 7 天周期。

原因:训练集里用的节假日列表是某一年份的固定日期,比如把 2012 年的节假日硬编码成向量,测试集用到 2013 年甚至 2014 年时,节日日期完全不同。负荷数据里的节假日效应很强,春节、感恩节、圣诞节等节日当天和前后几天的负荷模式与平日差异很大。

解决:不要把节假日写死成向量。按年份动态生成每年对应的节假日,再通过merge关联到每个时间戳。R 里可以用timeDate包获取主要国家的节假日,或者手动维护一张年份-日期-节假日名映射表。资源包里的日期字段若带holiday标记,优先使用标记;若没有,就自己建表。还要注意节假日前后各一天往往也有负荷变化,我会把is_holiday、days_to_holiday、days_after_holiday一起作为特征。

5.5 滞后特征跨窗口泄漏,验证时好看上线就崩

现象:滚动窗口验证时模型表现极佳,RMSE 很低,但真实上线后误差翻倍。

原因:做滚动验证时,你提前把整个数据集的lag168一次性算好了。测试集中的第 15 天样本,它的lag168来自测试集前 7 天的真实负荷,而真实预测场景里你不可能知道未来 168 小时的负荷。看代码最直观:df_sorted$lag168 <- dplyr::lag(df_sorted$load, 168)这行在循环外执行,意味着测试集样本的滞后特征已经用到了它自身之后的真实值。

解决:在回测里重构特征时,必须保证每个样本进入模型前,所有滞后特征只依赖它自身时间点之前的值。我常用的方法是把特征构造写成一个带current_pos参数的函数:

lag_feature <- function(dt, pos, period = 168) { if (pos - period < 1) return(NA) dt$load[pos - period] }

在滚动循环内部,每预测到某一行就调用这个函数,而不是用预先算好的列。这样虽然慢一点,但能彻底阻断泄漏。从那以后我每次拿到新数据,都会强制跑一遍"人工挖掉 3 天真实值,再用模板预测"的验证,确认没有隐性的未来信息进入特征。

6. 进阶:把资源包变成你随时可复用的预测模板

到这个阶段,你手上已经有了能读数据、造特征、跑基线、算 Pinball Loss 的完整流程。我建议你再花半天做一件事:把前面的代码收拢成三个函数,load_gefcom()、build_features()、run_baseline(),然后保存成forecast_utils.R,以后任何新项目直接source()这些函数,只需要改文件路径和日期范围。

load_gefcom <- function(path_load, path_temp, tz = "UTC") { load_raw <- read.csv(path_load, stringsAsFactors = FALSE) temp_raw <- read.csv(path_temp, stringsAsFactors = FALSE) load_raw$datetime <- as.POSIXct(load_raw$timestamp, tz = tz) temp_raw$datetime <- as.POSIXct(temp_raw$timestamp, tz = tz) merge(load_raw, temp_raw[, c("datetime", "temp_obs")], by = "datetime", all.x = TRUE) } build_features <- function(dt) { dt$hour <- lubridate::hour(dt$datetime) dt$hour_sin <- sin(2 * pi * dt$hour / 24) dt$hour_cos <- cos(2 * pi * dt$hour / 24) dt$weekday <- lubridate::wday(dt$datetime, label = TRUE) dt$lag24 <- dplyr::lag(dt$load, 24) dt$lag168 <- dplyr::lag(dt$load, 168) dt$temp_ma7 <- zoo::rollmeanr(dt$temp_obs, 7, fill = NA) dt }

函数化最大的好处是验证和线上共用同一套特征逻辑,不会出现线上少一个lag24的情况。参数上,tz默认设为"UTC",但如果你的业务数据是本地时区,调用时直接传新的时区名,函数内部不需要改动。merge里用了all.x = TRUE,这能保证负荷行不丢,但后续必须自己处理温度缺失,避坑章节里强调过的点在这里依然适用。

函数写完以后,我还会做一个小验证:从资源包原始数据随机挑出 7 天,人为挖掉负荷值,然后用模板流程预测这 7 天,再用 Pinball Loss 比较预测值和被挖掉的值。这个测验的意义不在于指标多高,而在于确认整个流程从数据读取到指标计算没有任何隐藏的 NA 或对齐问题。如果某个时间点的预测值比真实值差了几百兆瓦,回头查那段时间的lag24是否因为缺失被错误填充。从那以后,每拿到一份新的负荷数据,我都会强制走一遍这个流程:先统一时区,再打印缺失版图,然后构造滞后特征,最后分位损失和区间稳定性一起看。这套习惯帮我避开过不少坑,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询