El hueco térmico: una caracterización vía kmeans

El hueco térmico es una variable aleatoria que representa la necesidad de utilizar energía térmica tradicional y no renovable para abastecer el mercado eléctrico. Tiene dos fuentes principales de variabilidad:

  • La variabilidad de la demanda.
  • La variabilidad de las fuentes de energía renovable.

[Una pequeña digresión: cuando $Y = X_1 + X_2$, la varianza de $Y$ depende de las de $X_i$ y de su correlación. Si son independientes, es la suma de las dos; si están negativamente correladas, la de $Y$ es inferior a la suma; etc. Este humilde opinador sostiene que a medio plazo no hay otro remedio para el sistema eléctrico que forzar una correlación negativa entre $X_1$ y $X_2$, lo cual, en plata, significa cortes más o menos selectivos de suministro.]

La forma y tamaño del hueco térmico tiene una consecuencia menor pero muy aburrida:

  • Cuando es pequeño, los tirios de Twitter muestran capturas de pantalla de la aplicación de REE y la acompañan de comentarios del tipo: “¡La eólica es el futuro! ¡Ahora mismo está produciendo el nosecuantosmil por ciento del nosequé!”
  • Cuando es grande, los troyanos hacen lo propio, aunque cambiando el sentido del pie de la foto: “¡Los molinillos son un fracaso! Ahora mismo, ¡mirad! “La transición ecológica es un timo!”

Etc.

En lo que me he entretenido es en caracterizar el tamaño y forma diaria del hueco eléctrico con datos reales del último año (largo) y buscar en él nueve patrones básicos. Es decir, he extraído las formas funcionales del hueco térmico para cada día y las he agrupado en nueve tipos básicos. El resultado ha sido este:

El tamaño relativo (en %) de los clústers es

   1    2    3    4    5    6    7    8    9
 6.3  6.1 10.9 17.0 19.9  6.8 14.5  5.5 13.1

Finalmente, por si alguno quiere abundar en el análisis anterior, dejo acá el código —muy sucio, lo reconozco— que he usado para construir todo lo anterior:

import pandas as pd
import requests
import json

cookies = {
    'visid_incap_257780': 'jc28dyHpQzWndVySBH3efkKVn2IAAAAAQUIPAAAAAABbIZnfEcl/jLCTtncIWNvS',
    'visid_incap_1863806': 'C/tZlrhlSRyu5T/zgqoAhhejyWIAAAAAQUIPAAAAAACfU/4FREH4fkd1Sm8YSVZw',
    'incap_ses_1484_257780': 'xeeYIMzTQBv48xV8QTqYFGIh1GIAAAAAn2q8n2HZPmQdj2EwL7Yv4A==',
    'incap_ses_1484_1863806': '4oF1YhQzN1j6/BV8QTqYFGsh1GIAAAAAUqEZjUhiRZGSe22Zbr8INQ==',
}

headers = {
    'authority': 'demanda.ree.es',
    'accept': '*/*',
    'accept-language': 'en-US,en;q=0.9,es-ES;q=0.8,es;q=0.7',
    # Requests sorts cookies= alphabetically
    # 'cookie': 'visid_incap_257780=jc28dyHpQzWndVySBH3efkKVn2IAAAAAQUIPAAAAAABbIZnfEcl/jLCTtncIWNvS; visid_incap_1863806=C/tZlrhlSRyu5T/zgqoAhhejyWIAAAAAQUIPAAAAAACfU/4FREH4fkd1Sm8YSVZw; incap_ses_1484_257780=xeeYIMzTQBv48xV8QTqYFGIh1GIAAAAAn2q8n2HZPmQdj2EwL7Yv4A==; incap_ses_1484_1863806=4oF1YhQzN1j6/BV8QTqYFGsh1GIAAAAAUqEZjUhiRZGSe22Zbr8INQ==',
    'sec-fetch-dest': 'script',
    'sec-fetch-mode': 'no-cors',
    'sec-fetch-site': 'same-origin',
    'sec-gpc': '1',
    'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36',
}

params = {
    'callback': 'angular.callbacks._7',
    'curva': 'DEMANDAQH',
    'fecha': '2022-07-16',
}

response = requests.get('https://demanda.ree.es/WSvisionaMovilesPeninsulaRest/resources/demandaGeneracionPeninsula', params=params, cookies=cookies, headers=headers)


my_dates = [x.strftime("%Y-%m-%d") for x in pd.date_range(start = "2021-01-01", end = "2022-07-16")]


def get_date(my_date):
    params['fecha'] = my_date
    response = requests.get('https://demanda.ree.es/WSvisionaMovilesPeninsulaRest/resources/demandaGeneracionPeninsula', params=params, cookies=cookies, headers=headers)
    return response.text

my_data = [get_date(x) for x in my_dates]

def process_data(x):
    tmp = x
    tmp = tmp[21:-2]
    tmp = json.loads(tmp)
    tmp = tmp['valoresHorariosGeneracion']
    return pd.DataFrame(tmp)

tmp = [process_data(x) for x in my_data]

res = pd.concat(tmp)
res = res.groupby('ts').mean().reset_index()
res.to_csv("hueco-termico.csv", index=False)
library(data.table)
library(ggplot2)
library(lubridate)
library(scales)

minute_to_ts <- function(x){
  tmp <- paste(Sys.Date(), x)
  ymd_hm(tmp)
}

x <- fread("hueco-termico.csv")

x$hueco_termico <- x$gf + x$car + x$cc
x <- x[, c("ts", "hueco_termico")]

x$day <- sapply(strsplit(x$ts, " "), function(x) x[[1]])
x$minute <- sapply(strsplit(x$ts, " "), function(x) x[[2]])

x <- x[, n := .N, by = "day"]
x <- x[x$n == 288,]
x$n <- NULL
x$ts <- NULL

tmp <- dcast(x, day ~ minute, value.var = "hueco_termico")
days <- tmp$day
tmp <- as.matrix(tmp[, -1])

res <- kmeans(tmp, centers = 9)

centers <- data.table(res$centers)
centers$cluster <- as.numeric(row.names(centers))
centers <- melt(centers, id.vars = "cluster")
centers$ts <- minute_to_ts(centers$variable)

orig <- data.table(tmp)
orig$cluster <- res$cluster
orig$day <- days
orig <- melt(orig, id.vars = c("day", "cluster"))
orig$ts = minute_to_ts(orig$variable)

ggplot(centers, aes(x = ts, y = value)) +
  geom_line(data = orig, aes(group = day), alpha = 0.2) +
  geom_line(col = "red") +
  ylab("hueco térmico") +
  xlab("") +
  facet_wrap(~cluster) +
  scale_x_datetime(breaks = breaks_width("6 hour"),labels=date_format("%H:%M")) +
  theme_bw()