Saltar al contenido

Demanda eléctrica en España durante el confinamiento COVID-19 visto con Python

Este artículo es, en parte, una actualización de este otro artículo: “Evolución de la demanda eléctrica en los primeros días del confinamiento en España (visto con Python)“.

En ese artículo le dimos un vistazo rápido a cómo había sido la demanda de electricidad en los primeros días del confinamiento. En este nuevo artículo vamos a ver dos meses completos de datos de demanda eléctrica.

Hace poco vimos como hacer gifs animados:

Ahora, además, vamos a crear una animación, un gif animado, usando una nueva opción. Esta vez voy a usar una biblioteca que se integra perfectamente con Matplotlib y que se llama gif.

La primera parte del código está relacionada con la descarga de los datos de la API de red eléctrica. A continuación pongo lo que vamos a importar y la parte de la descarga:

from urllib.request import urlopen
import json
from time import sleep
import datetime as dt
from typing import Optional, List
 
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
import gif # https://github.com/maxhumber/gif
plt.style.use("bmh")

# Download data during the period 2020/02/15 - 2020/04/30
df20 = pd.DataFrame(
    index=pd.date_range(
        "2020-02-14T23:00:00",
        "2020-04-30T23:50:00",
        freq="10T",
        tz="utc",
    ),
    columns=[
        "Demanda real", 
        "Demanda programada", 
        "Demanda prevista",
    ],
)
t = dt.datetime(2020,2,15,0,0)
_dt = dt.timedelta(hours=23, minutes=50)
tf = dt.datetime(2020,5,1,0,0)
while t < tf:
    print(t)
    try:
        url = (
            'https://apidatos.ree.es/es/datos/demanda/demanda-tiempo-real?'
            f'start_date={t.strftime("%Y-%m-%dT%H:%M")}'
            f'&end_date={(t + _dt).strftime("%Y-%m-%dT%H:%M")}'
            '&time_trunc=hour'
        )
        data = json.loads(urlopen(url).read())
        for entry in data["included"]:
            tmp = pd.DataFrame(entry["attributes"]["values"])
            tmp.set_index(
                pd.to_datetime(tmp["datetime"], utc=True), 
                inplace=True,
            )
            col = entry["attributes"]["title"]
            df20.loc[tmp.index, col] = tmp["value"]
        t += dt.timedelta(days=1)
    except:
        sleep(3)
# Download data during the period 2019/02/15 - 2019/04/30
df19 = pd.DataFrame(
    index=pd.date_range(
        "2019-02-14T23:00:00",
        "2019-04-30T23:50:00",
        freq="10T",
        tz="utc",
    ),
    columns=[
        "Demanda real", 
        "Demanda programada", 
        "Demanda prevista",
    ],
)
t = dt.datetime(2019,2,15,0,0)
_dt = dt.timedelta(hours=23, minutes=50)
tf = dt.datetime(2019,5,1,0,0)
while t < tf:
    print(t)
    try:
        url = (
            'https://apidatos.ree.es/es/datos/demanda/demanda-tiempo-real?'
            f'start_date={t.strftime("%Y-%m-%dT%H:%M")}'
            f'&end_date={(t + _dt).strftime("%Y-%m-%dT%H:%M")}'
            '&time_trunc=hour'
        )
        data = json.loads(urlopen(url).read())
        for entry in data["included"]:
            tmp = pd.DataFrame(entry["attributes"]["values"])
            tmp.set_index(
                pd.to_datetime(tmp["datetime"], utc=True), 
                inplace=True,
            )
            col = entry["attributes"]["title"]
            df19.loc[tmp.index, col] = tmp["value"]
        t += dt.timedelta(days=1)
    except:
        sleep(3)

Creamos dos DataFrames, uno con los datos de marzo y abril de 2020, df20, y otro con los datos de marzo y abril de 2019, df19.

Ahora comento la parte que involucra a la biblioteca gif.

@gif.frame
def create_frame(
    x: np.array, 
    y1: np.array, 
    y2: np.array, 
    week1: bool, 
    week2: bool, 
    label1: Optional[str] = "2020", 
    label2: Optional[str] = "2019",
) -> None:
    #fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(15, 8))
    fig = plt.figure(figsize=(15, 7.5))
    gs = GridSpec(1, 4, figure=fig)
    ax1 = fig.add_subplot(gs[0, 0:3])
    ax2 = fig.add_subplot(gs[0, -1])
    if week1:
        symbol_code1 = "ko"
        hatch1 = None
    else:
        symbol_code1 = "ks"
        hatch1 = "/"
        plt.rcParams.update({'hatch.color': 'r'})
    if week2:
        symbol_code2 = "ro"
        hatch2 = None
    else:
        symbol_code2 = "rs"
        hatch2 = "/"
        plt.rcParams.update({'hatch.color': 'k'})
    ax1.plot(x, y1, color="k", label=label1)
    ax1.plot(x, y2, color="r", label=label2)
    ax1.plot(x[-1], y1[-1], symbol_code1)
    ax1.plot(x[-1], y2[-1], symbol_code2)
    ax1.legend()
    ax2.bar([0], [np.sum(y1)], color="k", hatch=hatch1)
    ax2.bar([1], [np.sum(y2)], color="r", hatch=hatch2)
    ax2.set_xticks([0, 1])
    ax2.set_xticklabels([2020, 2019])
    plt.rcParams.update({'hatch.color': 'k'})
    return None

La función anterior se encargará de dibujar un fotograma del gif animado. Esa función está decorada con el decorador gif.frame. En cada fotograma creamos dos gráficos, uno con la evolución de la demanda eléctrica cada 3 horas y el otro con la suma acumulada de los GW/h acumulados. Cuando las barras de la derecha presentan un patrón (líneas oblicuas) indica que ese día corresponde a fin de semana. Los distingo así porque los días de fin de semana la demanda es bastante más baja. En el 2019 hubo 18 días de fin de semana entre marzo y abril mientras que en 2020 hubo 17 días de fin de semana. Esto también afectará al resultado final.

frames: List = []
steps1 = pd.date_range("2020-03-02", "2020-04-30", freq="D", tz="utc")
steps2 = pd.date_range("2019-02-16", "2019-04-30", freq="D", tz="utc")
for step1, step2 in zip(df20.index[::18], df19.index[::18]):
    print(step1, step2)
    tmp20 = df20.loc[df20.index <= step1, "Demanda real"]
    tmp19 = df19.loc[df19.index <= step2, "Demanda real"]
    x = tmp20.index.values
    y1 = tmp20.values
    y2 = tmp19.values
    week1 = True if tmp20.index.weekday[-1] < 5 else False
    week2 = True if tmp19.index.weekday[-1] < 5 else False
    frame = create_frame(x, y1, y2, week1, week2)
    frames.append(frame)
# I repeat the last frame so the end lasts a little bit longer
for i in range(20):
    frames.append(frame)
gif.save(frames, "ree_evolution.gif", duration=100)

Una vez que sabemos como crear cada fotograma hacemos un bucle donde vamos leyendo datos de los DataFrames con los datos del 2019 y los datos del 2020 y esos datos los metemos como entrada a la función. Cada paso del bucle lee 3 horas más de datos.

Añado un nuevo bucle para repetir el último fotograma 20 veces más y que así dé tiempo de ver el resultado final.

El gif animado que me queda ocupa alrededor de 100Mb y no lo voy a subir aquí. Lo que he subido aquí es un vídeo obtenido a partir del gif animado usando ffmpeg. Desde la línea de comandos puedes hacer lo siguiente para convertir el gif animado a mp4:

>>> ffmpeg -i ree_evolution.gif -movflags faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" ree_evolution.mp4

El resultado final de la animación lo puedes ver aquí:

Por último, si quieres ver el valor numérico de la demanda acumulada podrías usar el siguiente código:

print(
    df20.dropna().values.sum(), 
    df19.dropna().values.sum(), 
    df20.dropna().values.sum() / df19.dropna().values.sum(),
)

El resultado es que la demanda ha bajado alrededor del 10% con respecto a los mismos meses del año pasado. Esto es solo para hacerse una idea ya que influyen muchos factores que no voy a comentar (como el número de días de fin de semana incluidos).

El código completo lo tenéis aquí:

from urllib.request import urlopen
import json
from time import sleep
import datetime as dt
from typing import Optional, List
 
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
import gif # https://github.com/maxhumber/gif
plt.style.use("bmh")

# Download data during the period 2020/02/15 - 2020/04/30
df20 = pd.DataFrame(
    index=pd.date_range(
        "2020-02-14T23:00:00",
        "2020-04-30T23:50:00",
        freq="10T",
        tz="utc",
    ),
    columns=[
        "Demanda real", 
        "Demanda programada", 
        "Demanda prevista",
    ],
)
t = dt.datetime(2020,2,15,0,0)
_dt = dt.timedelta(hours=23, minutes=50)
tf = dt.datetime(2020,5,1,0,0)
while t < tf:
    print(t)
    try:
        url = (
            'https://apidatos.ree.es/es/datos/demanda/demanda-tiempo-real?'
            f'start_date={t.strftime("%Y-%m-%dT%H:%M")}'
            f'&end_date={(t + _dt).strftime("%Y-%m-%dT%H:%M")}'
            '&time_trunc=hour'
        )
        data = json.loads(urlopen(url).read())
        for entry in data["included"]:
            tmp = pd.DataFrame(entry["attributes"]["values"])
            tmp.set_index(
                pd.to_datetime(tmp["datetime"], utc=True), 
                inplace=True,
            )
            col = entry["attributes"]["title"]
            df20.loc[tmp.index, col] = tmp["value"]
        t += dt.timedelta(days=1)
    except:
        sleep(3)
# Download data during the period 2019/02/15 - 2019/04/30
df19 = pd.DataFrame(
    index=pd.date_range(
        "2019-02-14T23:00:00",
        "2019-04-30T23:50:00",
        freq="10T",
        tz="utc",
    ),
    columns=[
        "Demanda real", 
        "Demanda programada", 
        "Demanda prevista",
    ],
)
t = dt.datetime(2019,2,15,0,0)
_dt = dt.timedelta(hours=23, minutes=50)
tf = dt.datetime(2019,5,1,0,0)
while t < tf:
    print(t)
    try:
        url = (
            'https://apidatos.ree.es/es/datos/demanda/demanda-tiempo-real?'
            f'start_date={t.strftime("%Y-%m-%dT%H:%M")}'
            f'&end_date={(t + _dt).strftime("%Y-%m-%dT%H:%M")}'
            '&time_trunc=hour'
        )
        data = json.loads(urlopen(url).read())
        for entry in data["included"]:
            tmp = pd.DataFrame(entry["attributes"]["values"])
            tmp.set_index(
                pd.to_datetime(tmp["datetime"], utc=True), 
                inplace=True,
            )
            col = entry["attributes"]["title"]
            df19.loc[tmp.index, col] = tmp["value"]
        t += dt.timedelta(days=1)
    except:
        sleep(3)

@gif.frame
def create_frame(
    x: np.array, 
    y1: np.array, 
    y2: np.array, 
    week1: bool, 
    week2: bool, 
    label1: Optional[str] = "2020", 
    label2: Optional[str] = "2019",
) -> None:
    #fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(15, 8))
    fig = plt.figure(figsize=(15, 7.5))
    gs = GridSpec(1, 4, figure=fig)
    ax1 = fig.add_subplot(gs[0, 0:3])
    ax2 = fig.add_subplot(gs[0, -1])
    if week1:
        symbol_code1 = "ko"
        hatch1 = None
    else:
        symbol_code1 = "ks"
        hatch1 = "/"
        plt.rcParams.update({'hatch.color': 'r'})
    if week2:
        symbol_code2 = "ro"
        hatch2 = None
    else:
        symbol_code2 = "rs"
        hatch2 = "/"
        plt.rcParams.update({'hatch.color': 'k'})
    ax1.plot(x, y1, color="k", label=label1)
    ax1.plot(x, y2, color="r", label=label2)
    ax1.plot(x[-1], y1[-1], symbol_code1)
    ax1.plot(x[-1], y2[-1], symbol_code2)
    ax1.legend()
    ax2.bar([0], [np.sum(y1)], color="k", hatch=hatch1)
    ax2.bar([1], [np.sum(y2)], color="r", hatch=hatch2)
    ax2.set_xticks([0, 1])
    ax2.set_xticklabels([2020, 2019])
    plt.rcParams.update({'hatch.color': 'k'})
    return None

frames: List = []
steps1 = pd.date_range("2020-03-02", "2020-04-30", freq="D", tz="utc")
steps2 = pd.date_range("2019-02-16", "2019-04-30", freq="D", tz="utc")
for step1, step2 in zip(df20.index[::18], df19.index[::18]):
    print(step1, step2)
    tmp20 = df20.loc[df20.index <= step1, "Demanda real"]
    tmp19 = df19.loc[df19.index <= step2, "Demanda real"]
    x = tmp20.index.values
    y1 = tmp20.values
    y2 = tmp19.values
    week1 = True if tmp20.index.weekday[-1] < 5 else False
    week2 = True if tmp19.index.weekday[-1] < 5 else False
    frame = create_frame(x, y1, y2, week1, week2)
    frames.append(frame)
# I repeat the last frame so the end lasts a little bit longer
for i in range(20):
    frames.append(frame)
gif.save(frames, "ree_evolution.gif", duration=100)

print(
    df20.dropna().values.sum(), 
    df19.dropna().values.sum(), 
    df20.dropna().values.sum() / df19.dropna().values.sum(),
)

Y eso es todo. El COVID-19 nos está afectando de muchas formas. Hasta ahora no me ha interesado meterme en las más macabras así que aquí solo vemos como ha afectado a la demanda eléctrica y, además, tenéis a vuestra disposición una nueva opción para crear animaciones 🙂

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

+ two = six

Pybonacci