Cálculo del escenario base de demanda para impacto de la COVID-19

Analizar el impacto de la COVID-19 en la demanda eléctrica parte de la base de que sabemos con qué vamos a comparar dicho impacto, dicho de otro modo: que existe un escenario base de referencia de demanda apto para ser comparado.

Sin embargo, en la mayoría de los casos, los deltas que uno lee en prensa son generalmente siempre referidos a años anteriores. No se pretende que no sea así, pero dichas comparaciones no son todo lo ortodoxas que cabría, al estar comparando circunstancias diferentes de laboralidad y temperatura, y la propia tendencia de desacoplamiento entre PIB y demanda que llevamos años observando. En este ejercicio, lo que pretendo es formular un escenario base de referencia sobre el que poder comparar la demanda real peninsular de 2020 y ver si de este modo se acerca a lo visto en el post anterior. Vamos a ello:

Demanda en b.c. histórica por Comunidad Autónoma

En primer lugar, para ponderar la temperatura por la demanda, necesitaré datos desagregados geográficamente de demanda. Lo más parecido son los balances de energía por Comunidad Autónoma que publica REE en su página de series estadísticas. Aquí hay fundamentalmente dos opciones: descargarse manualmente los datos (de hasta máximo 24 meses con cada consulta) región por región, o hacer uso de su API, bastante más sencilla que la de la página ESIOS. La API resulta cómoda, pero el output en JSON es horrible para tratar, y he decidido descargar las 15 regiones peninsulares en .csv en su lugar.

Una vez descargados, hay que tratarlos, ya que parece que no han estructurado los datos en una tabla, sino que parece un crop de la propia visualización de la web:

library(ggplot2)
library(riem)
library(tidyverse)
library(RCurl)
library(data.table)
library(lubridate)
library(timetk)
library(gt)
library(weathermetrics)
library(tsibble)
library(fable)
library(feasts)
library(fasster)

# Lista demanda mensual por CCAA
list_demanda_CCAA <- list.files("/Users/pherreraariza/Documents/energychisquared/post_inputs/12_post/input_demanda_CCAA", pattern = "balance", full.names = TRUE)

# Leo archivo en una única tabla y limpio datos
demanda_CCAA <- rbindlist(lapply(list_demanda_CCAA, function(file) {
                  demanda_CCAA_file <- read_delim(file, delim = ",", locale = locale( encoding = "latin1"))
                  CCAA_name <- demanda_CCAA_file[[1,2]]
                  demanda_CCAA_header <- c("variable", as.character(demanda_CCAA_file[4,2:25]))
                  demanda_CCAA_table <- demanda_CCAA_file %>% filter(`Título` == "Demanda en b.c.") %>% mutate_all( ~ str_replace(., ",", "."))
                  colnames(demanda_CCAA_table) <- demanda_CCAA_header
                  demanda_CCAA_table <- demanda_CCAA_table %>% mutate(across(!variable, as.numeric),
                                                                      CCAA = CCAA_name) %>%
                                                               filter(variable == "Demanda en b.c.")
                  demanda_CCAA_table
                }))

glimpse(demanda_CCAA)
## Rows: 15
## Columns: 26
## $ variable <chr> "Demanda en b.c.", "Demanda en b.c.", "Demanda en b.c.", "De…
## $ `ene/18` <dbl> 950230.9, 454690.9, 1844479.5, 153187.5, 813698.1, 450314.0,…
## $ `feb/18` <dbl> 883557.7, 406832.6, 1717220.7, 143908.8, 741390.3, 429018.6,…
## $ `mar/18` <dbl> 953924.6, 427714.4, 1873335.3, 148671.1, 780145.3, 447419.7,…
## $ `abr/18` <dbl> 878451.3, 395382.1, 1695932.4, 134488.1, 736925.2, 403494.7,…
## $ `may/18` <dbl> 870108.9, 371670.8, 1645679.0, 136589.1, 761407.2, 429690.4,…
## $ `jun/18` <dbl> 836118.9, 386425.1, 1596787.9, 135197.2, 789298.3, 420678.8,…
## $ `jul/18` <dbl> 865999.6, 398805.8, 1607005.1, 147700.9, 902760.7, 409460.9,…
## $ `ago/18` <dbl> 855891.1, 517022.4, 1617145.5, 139379.1, 903149.0, 405988.0,…
## $ `sep/18` <dbl> 847528.7, 454134.4, 1580536.9, 138154.1, 790124.9, 423398.1,…
## $ `oct/18` <dbl> 873135.6, 408719.7, 1637148.8, 147459.7, 729111.6, 439097.8,…
## $ `nov/18` <dbl> 894410.5, 413793.2, 1723854.1, 145925.1, 726074.3, 434543.7,…
## $ `dic/18` <dbl> 920538.9, 436251.1, 1720942.2, 140688.5, 766338.6, 407554.2,…
## $ `ene/19` <dbl> 955695.7, 457953.3, 1831829.7, 157569.4, 841665.4, 463542.5,…
## $ `feb/19` <dbl> 800857.1, 394740.9, 1563729.7, 137243.5, 722984.4, 412888.1,…
## $ `mar/19` <dbl> 819770.8, 388246.9, 1570501.3, 144513.1, 769206.2, 439192.3,…
## $ `abr/19` <dbl> 775652.1, 392573.3, 1498887.1, 133753.6, 717397.6, 409325.0,…
## $ `may/19` <dbl> 775559.0, 371863.6, 1455568.9, 134904.5, 753933.4, 439674.5,…
## $ `jun/19` <dbl> 734051.8, 390596.4, 1403979.9, 133645.4, 781848.1, 431270.9,…
## $ `jul/19` <dbl> 771577.4, 431889.8, 1459713.9, 153082.7, 934973.7, 425913.1,…
## $ `ago/19` <dbl> 751690.8, 482287.4, 1458252.7, 134540.5, 891511.5, 403832.7,…
## $ `sep/19` <dbl> 733274.9, 429438.0, 1437838.6, 136656.0, 740195.6, 424695.6,…
## $ `oct/19` <dbl> 741663.0, 394986.1, 1518709.9, 148674.0, 748991.0, 448901.5,…
## $ `nov/19` <dbl> 762830.4, 414781.0, 1611162.9, 145490.2, 756543.7, 441644.4,…
## $ `dic/19` <dbl> 770472.5, 416962.3, 1639128.2, 141234.6, 777171.5, 408815.5,…
## $ CCAA     <chr> "PRINCIPADO DE ASTURIAS", "EXTREMADURA", "GALICIA", "LA RIOJ…

Lo importante de estos datos es ver si durante 2018 y 2019 las distribuciones de consumo, o cuotas entre CCAA, permanecen aproximadamente iguales:

cuota_CCAA <- demanda_CCAA %>% pivot_longer(cols = !c(CCAA, variable), names_to = "fecha", values_to = "MWh") %>%
                               group_by(fecha, CCAA) %>%
                               summarise(MWh = sum(MWh)) %>%
                               mutate(freq = MWh / sum(MWh)) %>%
                               separate(col = fecha, into = c("mes", "año"), sep = "/", remove = FALSE) %>%
                               mutate(año = as.numeric(año)+2000,
                                      mes = gsub("ene", "01", mes),
                                      mes = gsub("feb", "02", mes),
                                      mes = gsub("mar", "03", mes),
                                      mes = gsub("abr", "04", mes),
                                      mes = gsub("may", "05", mes),
                                      mes = gsub("jun", "06", mes),
                                      mes = gsub("jul", "07", mes),
                                      mes = gsub("ago", "08", mes),
                                      mes = gsub("sep", "09", mes),
                                      mes = gsub("oct", "10", mes),
                                      mes = gsub("nov", "11", mes),
                                      mes = gsub("dic", "12", mes),
                                      fecha = ymd(paste0(año, mes,"01"))) %>%
                               select(fecha, CCAA, freq) 

cuota_CCAA %>% group_by(CCAA) %>% plot_time_series(fecha, freq,
                                .color_var = year(fecha),
                                .facet_ncol = 4,
                                .facet_scales = "free",
                                .title = "Cuota de demanda en b.c.",
                                .interactive = FALSE,
                                .legend_show = FALSE)

Pese a que hay algunas diferencias, decido utilizar los datos de 2019 para el modelo. Y aquí hago una hipótesis burda, donde supongo que dicho reparto mensual se mantiene igual durante todas las horas del mes. No hay otra forma de disponer los datos horarios de demanda, por lo que no hay más remedio si quiero realizar algún tipo de ponderación de temperaturas. Como las estaciones están repartidas por todo la península, pero dispongo de la ubicación, hago una tabla auxiliar para relacionar dicha ubicación con la tabla de cuotas de demanda por CCAA.

# Trabajo con el año 2019
cuota_CCAA <- cuota_CCAA %>% filter(year(fecha) == 2019) %>% select(CCAA, freq)

Datos horarios de temperatura

# Listado de las estaciones de aeropuertos de España
spain_airports <- riem_stations(network = "ES__ASOS")

spain_airports_id <- spain_airports %>% select(id)
spain_airports %>% gt()

La lista es bastante extensa, aunque la integridad de los datos es lo crucial. Con la siguiente función, se puede descargar a la ruta que quiera en local los .RData con todos los datos de meteo que haya registrado en ASOS.

Una vez tengo la BBDD de temperaturas, sólo falta leer las estaciones para las cuales nos interesa el estudio (peninsulares) y relacionarlas con cada CCAA para realizar el promedio horario de las estaciones:

# Leo tabla auxilar
CCAA_stations <- read_csv2("/Users/pherreraariza/Documents/energychisquared/post_inputs/12_post/input_demanda_CCAA/CCAA_stations.csv", locale = locale(encoding = "latin1"))

glimpse(CCAA_stations)
## Rows: 45
## Columns: 3
## $ id   <chr> "LEAB", "LEAL", "LEAM", "LEAS", "LEBZ", "LEBL", "LEBB", "LEBG", …
## $ name <chr> "Albacete", "Alicante", "Almeria", "Aviles", "Badajoz", "Barcelo…
## $ CCAA <chr> "CASTILLA- LA MANCHA", "COMUNIDAD VALENCIANA", "ANDALUCÍA", "PRI…
# Leo temperaturas
list_temperaturas <- list.files("/Users/pherreraariza/Documents/Meteo/", pattern = ".rds", full.names = TRUE)

temperaturas <- rbindlist(lapply(list_temperaturas, function(file) {
                  temperaturas_file <- readRDS(file)
                  temperaturas_file
                }))

# Selecciono y asigno las CCAA de península
temp_peninsular <- temperaturas %>% mutate(tmpf = fahrenheit.to.celsius(tmpf),
                                           datetime = round_date(valid, unit = "hour")) %>%
                                    group_by(station, datetime) %>%
                                    distinct(datetime, .keep_all = TRUE) %>%
                                    select(datetime, station, tmpf) %>%
                                    right_join(CCAA_stations, by = c("station" = "id")) %>%
                                    as_tsibble(index = datetime, key = c(station, CCAA)) %>%
                                    fill_gaps(.full = TRUE) %>%
                                    as.data.frame() %>%
                                    group_by(datetime, CCAA) %>%
                                    summarise(tmpf = mean(tmpf, na.rm =TRUE)) %>%
                                    drop_na(CCAA) %>%
                                    mutate_all(~ifelse(is.nan(.), NA, .))

# Visualizo huecos
temp_peninsular %>% group_by(CCAA, año = year(datetime)) %>% summarise(NAs = sum(is.na(tmpf))) %>% filter(NAs != 0) %>% gt()
año NAs
ANDALUCÍA
2014 12
2015 4
2016 2
2017 1
2018 3
2019 3
2020 3
2021 104
ARAGÓN
2014 22
2015 12
2016 18
2017 2
2018 10
2019 7
2020 8
2021 104
CANTABRIA
2014 2148
2015 2122
2016 1009
2017 7
2018 19
2019 16
2020 14
2021 104
CASTILLA Y LEÓN
2014 2903
2015 2624
2016 2863
2017 2895
2018 2875
2019 2372
2020 34
2021 104
CASTILLA- LA MANCHA
2014 8363
2015 8376
2016 8353
2017 8413
2018 8363
2019 4751
2020 4320
2021 188
CATALUÑA
2014 18
2015 6
2016 4
2017 1
2018 9
2019 5
2020 6
2021 104
COMUNIDAD DE MADRID
2014 16
2015 5
2016 4
2017 2
2018 7
2019 6
2020 7
2021 104
COMUNIDAD DE NAVARRA
2014 1828
2015 1628
2016 1038
2017 15
2018 42
2019 22
2020 23
2021 108
COMUNIDAD VALENCIANA
2014 18
2015 9
2016 4
2017 2
2018 9
2019 6
2020 7
2021 104
EXTREMADURA
2014 8448
2015 8501
2016 8520
2017 8599
2018 8390
2019 8385
2020 8525
2021 267
GALICIA
2014 16
2015 6
2016 4
2018 8
2019 4
2020 6
2021 104
LA RIOJA
2014 8726
2015 8696
2016 8730
2017 8706
2018 8699
2019 8721
2020 8732
2021 272
PAÍS VASCO
2014 19
2015 11
2016 4
2017 1
2018 8
2019 4
2020 6
2021 104
PRINCIPADO DE ASTURIAS
2014 1837
2015 1465
2016 828
2017 36
2018 24
2019 13
2020 17
2021 104
REGIÓN DE MURCIA
2014 2786
2015 2551
2016 2793
2017 2824
2018 1335
2019 35
2020 10
2021 105

La datos muestran muchos huecos, para algunas CCAA de casi la totalidad del año. Para corregirlo, se pueden agrupar aquellas CCAA con más huecos y hacer una media horaria con la esperanza de que completen entre todas dichos huecos. Sin embargo, dado que en los años más antiguos casi no se disponen de datos, es más que probable que no se vayan a completar, por lo que introducir en este grupo una CCAA con la mayoría de los datos sea la única solución. Por ello, introduciré Andalucía junto al resto de CCAA sin apenas datos:

# Listado CCAA agrupadas
CCAA_incompletas <- c("CANTABRIA", "CASTILLA Y LEÓN", "CASTILLA- LA MANCHA", "COMUNIDAD DE NAVARRA", "EXTREMADURA", "LA RIOJA", "PRINCIPADO DE ASTURIAS", "REGIÓN DE MURCIA", "ANDALUCÍA")

# Creo grupo_1 con las CCAA agrupadas
temp_peninsular_corregido <- temp_peninsular %>% mutate(CCAA_agrupadas = case_when(
                                                                    CCAA %in% CCAA_incompletas ~ "grupo_1",
                                                                    TRUE                      ~ CCAA))

# Detecto huecos a nivel anual
temp_peninsular_anual <- temp_peninsular_corregido %>% group_by(datetime, CCAA_agrupadas) %>%
                                                  summarise(tmpf = mean(tmpf, na.rm =TRUE)) %>%
                                                  drop_na(CCAA_agrupadas) %>%
                                                  mutate_all(~ifelse(is.nan(.), NA, .)) %>%
                                                  group_by(CCAA_agrupadas, año = year(datetime)) %>% 
                                                  summarise(NAs = sum(is.na(tmpf))) %>% 
                                                  filter(NAs != 0)

# Visualizo huecos
gt(temp_peninsular_anual)
año NAs
ARAGÓN
2014 22
2015 12
2016 18
2017 2
2018 10
2019 7
2020 8
2021 104
CATALUÑA
2014 18
2015 6
2016 4
2017 1
2018 9
2019 5
2020 6
2021 104
COMUNIDAD DE MADRID
2014 16
2015 5
2016 4
2017 2
2018 7
2019 6
2020 7
2021 104
COMUNIDAD VALENCIANA
2014 18
2015 9
2016 4
2017 2
2018 9
2019 6
2020 7
2021 104
GALICIA
2014 16
2015 6
2016 4
2018 8
2019 4
2020 6
2021 104
grupo_1
2014 12
2015 4
2016 2
2017 1
2018 3
2019 3
2020 2
2021 101
PAÍS VASCO
2014 19
2015 11
2016 4
2017 1
2018 8
2019 4
2020 6
2021 104

Ahora el número de huecos es mucho más manejable, y puedo interpolar con algún modelo para rellenar cada una de las series:

# Tabla con temperatura en formato tsibble
temp_peninsular_tsibble <- temp_peninsular_corregido %>% 
                                select(datetime, tmpf, CCAA_agrupadas) %>% 
                                group_by(datetime, CCAA_agrupadas) %>%
                                summarise(tmpf = mean(tmpf, na.rm = TRUE)) %>%
                                mutate(tmpf = ifelse(is.nan(tmpf), NA, tmpf)) %>%
                                tsibble(key = CCAA_agrupadas, index = datetime)

# Interpolación de huecos
temp_peninsular_interpolado <- temp_peninsular_tsibble %>% 
                                    model(naive = ARIMA(tmpf ~ -1 + pdq(0,1,0) + PDQ(0,0,0))) %>%
                                    interpolate(temp_peninsular_tsibble)

# Visualización de temperaturas
temp_peninsular_interpolado %>% plot_time_series(.date_var = datetime, .value = tmpf, .facet_vars = CCAA_agrupadas, .facet_ncol = 2, .interactive = FALSE)

Ahora, con las series completas, puedo ponderarlo con las cuotas de las CCAA agrupadas que tengo, y de esta manera tener una aproximación a la temperatura media histórica peninsular:

# Agrupación frecuencias para grupo_1
cuota_CCAA_agrupadas <- cuota_CCAA %>% mutate(CCAA_agrupadas = case_when(
                                                                    CCAA %in% CCAA_incompletas ~ "grupo_1",
                                                                    TRUE                      ~ CCAA)) %>%
                                            group_by(CCAA_agrupadas) %>%
                                            summarise(freq = sum(freq))

# Obtención tabla temperatura peninsular
temp_peninsular_ponderado <- temp_peninsular_interpolado %>% left_join(cuota_CCAA_agrupadas, by = "CCAA_agrupadas") %>%
                             index_by(datetime) %>%
                             summarise(tmp_ponderada = weighted.mean(tmpf, freq)) %>%
                             index_by(fecha = date(datetime)) %>%
                             summarise(tmp_ponderada = max(tmp_ponderada))

# Visualización temperatura
temp_peninsular_ponderado %>% plot_time_series(.date_var = fecha, .value = tmp_ponderada, .interactive = FALSE)

Hay un pico de temperaturas mínimas en 2021 (que afortunadamente no utilizaré), pero que manifiestan el problema de las medias de las regiones con menos datos: son al mismo tiempo regiones con climatología continental y con mínimos de temperatura que arrastran la distribución de los valores. Se podría corregir asignando un peso por población, por ejemplo, pero no he querido llegar mucho más lejos para el ejemplo.

Modelo

Para la tabla de input de datos final a escala diaria, ya solo falta la demanda (descargable desde la página de ESIOS) y una lista de fechas con los festivos nacionales para crear una variable dummy que discrmine entre laborables y festivos (incluyendo fines de semana):

# Lectura demanda
demanda_real <- read_csv2("/Users/pherreraariza/Documents/energychisquared/post_inputs/11_post/demanda_real/demanda_real.csv") %>% select(datetime, value) %>% rename(MWh = value)

# Lectura festivos
festivos <- read_csv2("/Users/pherreraariza/Documents/energychisquared/post_inputs/11_post/festivos.csv", col_types = "cc") %>% mutate(fecha = dmy(fecha))

# Join con tabla final para modelizar
demanda <- demanda_real %>% group_by(fecha = date(datetime)) %>%
                            summarise(MWh = sum(MWh, na.rm = TRUE)) %>%
                            mutate(weekday = wday(fecha, label = TRUE)) %>% 
                            left_join(festivos, by = "fecha") %>% 
                            mutate(tipo = factor(if_else(is.na(tipo) & weekday %in% c("lun","mar", "mié", "jue", "vie"), "L", "F"))) %>%
                            left_join(temp_peninsular_ponderado, by = "fecha") %>%
                            select(fecha, tipo, MWh, tmp_ponderada)

# Visualización
glimpse(demanda)                            
## Rows: 2,557
## Columns: 4
## $ fecha         <date> 2014-01-01, 2014-01-02, 2014-01-03, 2014-01-04, 2014-0…
## $ tipo          <fct> F, L, L, F, F, F, L, L, L, L, F, F, L, L, L, L, L, F, F…
## $ MWh           <dbl> 552505.8, 676598.3, 683164.3, 644651.8, 608006.3, 57373…
## $ tmp_ponderada <dbl> 13.679719, 14.722614, 15.789024, 13.014828, 13.590476, …

Para el modelo, el enfoque radica en tratar el dataset como una serie temporal, y trabajar con los términos que permita cada uno de los tres métodos que emplearé:

  • Término temperatura diaria: como variable exógena, cuya representación vs la demanda indica una relación cuadrática (Hyndman et al.), con un punto de inflexíón en torno a los 20 ºC.
  • Término para el tipo de día: booleano que indica si se trata de fin de semana o festivo vs laborable.
  • Término para la estacionalidad semanal: aplciable a través de un término de Fourier.
  • Término para la estacionalidad anual: aplciable a través de un término de Fourier.
# Relación cuadrátiva Temperatura vs Demanda
demanda %>% ggplot(aes(x = tmp_ponderada, y = MWh, col = tipo)) +
                geom_point(alpha = 0.6) +
                labs(x="Temperatura (ºC)", y = "Demanda (MWh)")

Se crean tres sets de datos: el de entrenamiento (5 años de datos), el de test (año 2019), que permitirá evaluar los errores y la bondad del modelo, y finalmente el de validación que será el año de estudio, 2020.

Para el set de entrenamiento pruebo con un método ARIMA, corrigiendo la varianza a través del logaritmo del outcome, incorporando la relación cuadrática de la temperatura, los términos de Fourier para la estacionalidad y el booleano con el tipo de día. Asímismo, fuerzo los términos PDQ a (0,0,0) para tratar de emular ruido blanco.

También pruebo con una regresión lineal (TSLM) y un método de redes neuronales (computacionalmente más costoso para el set de datos, aviso), y finalmente una combinación de los tres haciendo una media para cada valor:

# Set de entrenamiento
train_set <- demanda %>% filter(year(fecha) <= 2018) %>% 
                         as_tsibble(index = fecha)
                         
# Set de test
test_set <- demanda %>% filter(year(fecha) == 2019) %>% 
                         as_tsibble(index = fecha)

# Set de validación
validation_set <- demanda %>% filter(year(fecha) == 2020) %>% 
                         as_tsibble(index = fecha)

# Modelo
fit <-  train_set %>% model(ARIMA = ARIMA(log(MWh) ~ PDQ(0, 0, 0) + pdq(d=0) + tmp_ponderada + I(tmp_ponderada^2) + tipo + fourier(period = "week", K = 3) + fourier(period = "year", K = 3)),
                            TSLM = TSLM(log(MWh) ~  tmp_ponderada + I(tmp_ponderada^2) + tipo + fourier(period = "week", K = 3) + fourier(period = "year", K = 3)),
                            NNETAR = NNETAR(box_cox(MWh, 0.15) ~ AR() + tmp_ponderada + I(tmp_ponderada^2))) %>%
                        mutate(COMBINATION = (ARIMA + TSLM + NNETAR) / 3)

# KPI modelo
fit %>% accuracy()
## # A tibble: 4 x 9
##   .model      .type       ME   RMSE    MAE     MPE  MAPE  MASE    ACF1
##   <chr>       <chr>    <dbl>  <dbl>  <dbl>   <dbl> <dbl> <dbl>   <dbl>
## 1 ARIMA       Training  282. 14370.  9569. -0.0126  1.43 0.323  0.0140
## 2 TSLM        Training  503. 26185. 19489. -0.0756  2.87 0.657  0.719 
## 3 NNETAR      Training  183. 11132.  6800. -0.0186  1.02 0.229 -0.0158
## 4 COMBINATION Training  354. 13318.  9358. -0.0311  1.39 0.316  0.454
fit %>% select(COMBINATION) %>% gg_tsresiduals()

Los resultados son prometedores para la red neuronal, aunque la distribución de los errores parece indicar que aún queda mucha información en el remanente por ser explicada. Por otra parte, para tomar qué modelo utilizar para el añom 2020, hay que comprobar si sufre de over-fitting o no, y esto se podrá ver con el set de datos de test para el año 2019:

# Forecast test set
forecast_test <- fit %>% forecast(test_set, times = 10) 

# KPI sobre test
forecast_test %>% accuracy(test_set)
## # A tibble: 4 x 9
##   .model      .type     ME   RMSE    MAE    MPE  MAPE  MASE  ACF1
##   <chr>       <chr>  <dbl>  <dbl>  <dbl>  <dbl> <dbl> <dbl> <dbl>
## 1 ARIMA       Test  3090.  31710. 22426.  0.225  3.27   NaN 0.858
## 2 COMBINATION Test  2908.  28085. 20470.  0.175  3.04   NaN 0.604
## 3 NNETAR      Test  5332.  52541. 41572.  0.391  6.23   NaN 0.417
## 4 TSLM        Test   -22.5 26954. 18499. -0.138  2.73   NaN 0.688
forecast_test %>% autoplot(test_set, level = NULL) + labs(y = "Demanda (MWh) 2019")

Efectivamente la red neuronal padecía de un sobreajuste sobre el set de entrenamiento, al multiplicar su error por más de 8 en el set de test. El modelo que mejor se comporta es la regresión lineal con variables exógenas (TSLM), que será por tanto el que utilice para el análisis del año 2020.

# Forecast final sobre 2020
forecast_test_2020 <- fit %>% select(TSLM) %>% forecast(validation_set)

# Visualización
forecast_test_2020 %>% autoplot(validation_set, level = NULL) + labs(y = "Demanda (MWh) 2020")

La línea negra indica la demanda real observada y publicada por REE, mientras que la azul corresponde a la simulación de lo que considero sería el escenario base con el que contrastar el efecto de la pandemia. Claramente, se obversva el tremendo impacto a partir de mediados de marzo y cómo existe una cierta recuperación en verano, sin llegar a ser del 100%. Es únicamente en diciembre cuando parece que las líneas se llegan a solapar con mayor claridad, indicando una recuperación que sería indistinguible del propio error del modelo.

De forma análoga a como hice en el post previo de análisis de la demanda de la COVID-19 sobre la demanda eléctrica, se puede graficar el impacto mensual acumulado para ver si aproximadamente coincide o no:

# Impacto 2020 de COVID vs escenario base forecasteado
forecast_test_2020 %>% as_tibble() %>% 
                                select(fecha, .mean) %>% 
                                left_join(validation_set, by = "fecha") %>%
                                rename(base = .mean,
                                       real = MWh) %>%
                       group_by(month = yearmonth(fecha)) %>% 
                       summarise(base = sum(base, na.rm = TRUE),
                                 real = sum(real, na.rm = TRUE)) %>%
                       mutate(year = year(month),
                              month = month(month, label = TRUE)) %>% 
                       mutate(cum_base = cumsum(base),
                              cum_real = cumsum(real)) %>%
                       mutate(delta = ((cum_real/cum_base)-1)*100) %>%
                       ggplot(aes(x = month, y = delta, group = 1)) + geom_line(color = "blue") + 
                              geom_point(color = "blue") + geom_text(aes(label = round(delta, 1)), vjust = "inward", hjust = "inward", show.legend = FALSE) + 
                              ggtitle("Delta acumulado respecto al forecast base") + ylab("%") + xlab("Mes")  

Tal y como se muestra, el impacto total acumulado simulado es del -5.2%, cercano al -5.75% que mostraba en el anterior análisis, donde el escenario base era la media de la demanda desde 2017 a 2019, ambos incluidos. También es cierto que la simulación ya parte que para el mes de enero de 2020 se consumió un 1.1% más de los esperado, por lo que toda la altura de la curva acumulada habría que enrasarla al cero. Aún así, me parece un análisis más limpio que contar o comparar con medias de años anteriores.

 Share!

 
comments powered by Disqus