dataforgoodfr / energetic-stress-production

Forecast the Energy production in France
https://greenforecast-squad.github.io/energetic-stress-production/
MIT License
1 stars 1 forks source link

Classification des jours Tempo #11

Closed antoinetavant closed 1 week ago

antoinetavant commented 5 months ago

L'objectif de cette issue est d'améliorer la classification des jours tempos (bleu, blancs et rouges).

Pour rappel, les classes tempo modifient la tarification de l'électricité : d'après la grille 2024, le tarif heure pleine bleu est à 16 cent/kWh, alors que le tarif rouge est à 75.6 cents/kWh !

Un premier essaie utilisant un classificateur sur la seule valeur de la production carbonée (consommation - production ENR) ne marche pas bien pour plusieurs raisons :

Pour ces deux raisons, il est impératif de modifier l'approche de classification des jours tempo.

La définition la plus explicite des règles est fournie par RTE ici. Cependant, il est possible que les seuils et règles effectivement en place évoluent.

Pour moi, il faut :

Remarque : On a vu que les classes tempo était mieux corrélée avec la prévision de consommation à J+1 plutôt que la consommation réelle. C'est normal, car les jours sont définis la veille pour le lendemain, mais il faut bien le prendre en compte dans la classification.

mattcln commented 4 months ago

Issue démarrée sur la branche 'tempo_pred'.

antoinetavant commented 3 months ago

@mattcln voici ce que j'ai fait à partir de la Doc RTE. Ça peut être une "baseline"

import pandas as pd
from energy_forecast import ROOT_DIR
data_file = ROOT_DIR / "data/rte_agg_daily_2014_2024.csv"
data_agg = pd.read_csv(data_file, index_col=0, parse_dates=True)

used_cols = ["Prévision_J-1", "Type_de_jour_TEMPO", "Eolien", "Solaire"]
period_start = "2015-09-01"  # included
period_end = "2016-08-31"  # included
data = data_agg.loc[period_start:period_end, used_cols]

data["production_nette"] = data["Prévision_J-1"] - (data["Eolien"] + data["Solaire"])  # here need to use Predicted production

q80 = data["production_nette"].quantile(0.8)
q40 = data["production_nette"].quantile(0.4)
qtemp30 = 8.5  # Rought estimation
gamma = -0.1176
kappa = 8.3042
data["production_norm"] = (data["production_nette"] - q40) / (   (q80 - q40) * np.exp(gamma * (kappa - qtemp30)) )  # there is a typo in the docs here : it is a - not a +

start_BLANC = 43
start_ROUGE = 22
start_blanc_rouge = start_BLANC + start_ROUGE
categories = pd.get_dummies(data["Type_de_jour_TEMPO"]).astype(int)
data["stock_rouge"] = start_ROUGE - categories["ROUGE"].cumsum()
data["stock_blanc"] = start_BLANC - categories["BLANC"].cumsum()
data["stock_blanc_rouge"] = data["stock_rouge"] + data["stock_blanc"]
data["jour_tempo"] = range(1, len(data) + 1)  # Not sur if start from 0 or 1 in docs

A_rouge = 3.15
B_rouge = -0.010
C_rouge = -0.031
A_blanc_rouge = 4
B_blanc_rouge = -0.015
C_blanc_rouge = -0.026

data["seuil_rouge"] = (
    A_rouge + B_rouge * data["jour_tempo"] + C_rouge * data["stock_rouge"]
)
data["seuil_blanc_rouge"] = (
    A_blanc_rouge
    + B_blanc_rouge * data["jour_tempo"]
    + C_blanc_rouge * data["stock_blanc_rouge"]
)

prediction_rouge = data["production_norm"] > data["seuil_rouge"]
start_ROUGE_allowed = "2015-10-31"
end_ROUGE_allowed = "2016-04-01"
prediction_rouge[:start_ROUGE_allowed] = False
prediction_rouge[end_ROUGE_allowed:] = False
prediction_rouge[data["stock_rouge"] < 0] = False

sundays = data.index.weekday == 6
saturdays = data.index.weekday == 5
prediction_rouge[sundays] = False
prediction_rouge[saturdays] = False

prediction_blanc_rouge = data["production_norm"] > data["seuil_blanc_rouge"]
prediction_blanc = prediction_blanc_rouge & ~prediction_rouge
prediction_blanc[sundays] = False

prediction_blanc[data["stock_blanc"] < 0] = False

J'ai eu cette réflexion pendant que j'implémentais cette logique : On doit considérer la classification uniquement des N (entre 2 et 4) prochain jours. Donc les valeurs par exemple de stock restant peuvent utiliser les valeurs réelles fournies par RTE.

De plus, en situation réelles on pourra utiliser les données de cette API pour utiliser les productions ENR prévues par RTE afin de mettre a jours les differents parameters de classifications tempo https://data.rte-france.com/catalog/-/api/generation/Generation-Forecast/v2.1

mattcln commented 3 months ago

En fin de période, une vérification de l’écoulement du stock est effectuée pour assurer que l’intégralité du stock soit bien placée. En cas de besoin, l’algorithme peut placer des jours même si les seuils requis ne sont pas atteints

En 2019-2020 ils avaient utilisés 18/22 jours rouges (sous covid) et en 2021-2022 un jour blanc n'a pas été placé. Pour les autres années, tout a bien été écoulé.

Si on veut tester le modèle, j'ai une partie de code qui recalcule le stock au jour J pour tout l'historique.

stock_jours_annuels = {
    "rouge": 22,
    "blanc": 43,
    "bleu": 300
}
stock_jours_annuels_b = {
    "rouge": 22,
    "blanc": 43,
    "bleu": 301
}

def get_base_stock(year:int) -> dict:
    """
    Vérifie si l'année est bissextiles ou non
    Renvoie le stock initial de jour tempo.

    :param year: _description_
    :return: _description_
    """
    if isleap(year):
        return stock_jours_annuels_b.copy()
    else:
        return stock_jours_annuels.copy()

def update_stock(previous_tempo:str, stock_actuel:dict) -> dict:
    if previous_tempo:
        if previous_tempo == "ROUGE":
            stock_actuel["rouge"] -= 1
        elif previous_tempo == "BLANC":
            stock_actuel["blanc"] -= 1
        elif previous_tempo == "BLEU":
            stock_actuel["bleu"] -= 1
        return stock_actuel
    else:
        return None

def check_stock_empty(stock_actuel:dict, year:int):
    """
    Cette fonction permet de vérifier si les stocks sont bien calculés :
    Tout les stocks doivent à 0, si ce n'est pas le cas c'est que RTE
    n'a pas utilisé tout les jours en stock, l'écart devrait alors se 
    répercuter négativement sur les jours bleus (la somme du dictionnaire doit être de 0 forcément)

    :param stock_actuel: _description_
    :param year: _description_
    """
    if stock_actuel:
        if stock_actuel["rouge"] == stock_actuel["bleu"] == stock_actuel["blanc"] == 0:
            pass
        else:
            print(f"Les stocks de jours n'était pas à 0 le 1er septembre {year} ! :(")
            print(f"Voici l'état du stock : {stock_actuel}")
            if stock_actuel["rouge"] + stock_actuel["bleu"] + stock_actuel["blanc"] != 0:
                raise ValueError("La somme des stocks ne fait pas 0 à la fin de l'année.")

def get_cumulative_stock(data):
    """
    On calcule les stocks restants pour chaque jour tempo à une date X.

    Les valeurs représentent la situation à J-1 car la prédiction
    pour une date D se fait avec le stocks restants à J-1.

    Ex : 
    Une row avec 
        - stock_j_rouge = 0
        - stock_j_blanc = 1
        - stock_j_bleu = 14
        - type_tempo = "BLANC"
    => Au moment de prédire le jour "BLANC", je n'avais plus de jour rouge
    mais il me restait un dernier jour blanc en stock

    :param data: _description_
    :return: _description_
    """
    stock_rouge, stock_bleu, stock_blanc = [], [], []
    previous_tempo = stock_actuel = None
    for i, row in data.iterrows():
        # Remise à zéro les 1er Septembre
        # sinon on update le stock en fonction du type tempo de la veille 
        if row["Date"].month == 9 and row["Date"].day == 1:
            update_stock(previous_tempo, stock_actuel)
            check_stock_empty(stock_actuel, row["Date"].year)
            stock_actuel = get_base_stock(row["Date"].year + 1)
        else:
            update_stock(previous_tempo, stock_actuel)

        stock_rouge.append(stock_actuel["rouge"])
        stock_blanc.append(stock_actuel["blanc"])
        stock_bleu.append(stock_actuel["bleu"])

        # On enregistre le type tempo
        previous_tempo = row["type_tempo"]

    data["stock_j_rouge"] = stock_rouge
    data["stock_j_blanc"] = stock_blanc
    data["stock_j_bleu"] = stock_bleu
    return data

data = get_cumulative_stock(data)
antoinetavant commented 3 months ago

Super ! J'avais pas vu ça effectivement !

antoinetavant commented 1 week ago

Cette issue est résolue par #22 On pourra améliorer la performance, mais au moins, on a une baseline !