Skip to content

TURPE en standalone

Ce guide explique comment utiliser les fonctions de calcul TURPE indépendamment du pipeline principal, pour des analyses ad-hoc, des tests, ou des intégrations personnalisées.

Table des matières

  1. Vue d'ensemble
  2. Fonctions Pipeline (Usage Recommandé)
  3. Expressions individuelles (Usage Avancé)
  4. Cas d'usage pratiques
  5. Bonnes pratiques

Vue d'ensemble

Le module TURPE fournit 2 fonctions principales pour un usage externe :

  • ajouter_turpe_fixe(periodes, regles=None) - Calcul du TURPE fixe
  • ajouter_turpe_variable(periodes, regles=None) - Calcul du TURPE variable

Ces fonctions encapsulent toute la logique métier (jointure règles, filtrage temporel, détection C4/C5) et sont prêtes à l'emploi.

Import et configuration

import polars as pl
from datetime import datetime
from zoneinfo import ZoneInfo
from electricore.core.pipelines.turpe import (
    # Fonctions pipeline (usage recommandé)
    ajouter_turpe_fixe,
    ajouter_turpe_variable,

    # Utilitaires
    load_turpe_rules,
)

Les périodes attendent une colonne debut tz-aware (Europe/Paris) — les règles turpe_rules.csv sont jointes puis filtrées temporellement dessus (expr_filtrer_regles_temporelles).


Fonctions Pipeline (Usage Recommandé)

ajouter_turpe_fixe() - TURPE Fixe

Signature :

def ajouter_turpe_fixe(
    periodes: pl.LazyFrame,
    regles: Optional[pl.LazyFrame] = None
) -> pl.LazyFrame

Fonctionnalités : - ✅ Joint automatiquement les règles TURPE (chargées si regles=None) - ✅ Filtre les règles sur les dates d'application - ✅ Détecte C4 vs C5 automatiquement - ✅ Calcule le TURPE fixe pour la période - ✅ Conserve toutes les colonnes originales

Prérequis des données en entrée :

periodes = pl.LazyFrame({
    "pdl": [...],                                # Identifiant point de livraison
    "formule_tarifaire_acheminement": [...],     # FTA (ex: "BTINFMUDT", "BTSUPCU")
    "puissance_souscrite_kva": [...],            # Puissance C5 (kVA)
    "debut": [...],                              # Date début période (tz-aware Europe/Paris)
    "nb_jours": [...],                           # Nombre de jours de la période

    # Optionnel pour C4 (BT > 36 kVA) :
    "puissance_souscrite_hph_kva": [...],        # 4 puissances souscrites
    "puissance_souscrite_hch_kva": [...],
    "puissance_souscrite_hpb_kva": [...],
    "puissance_souscrite_hcb_kva": [...],
})

Colonnes ajoutées : - turpe_fixe_eur (€) - Montant TURPE fixe pour la période

Exemple 1 : Calcul TURPE fixe C5 simple

# Périodes d'abonnement résidentiel (BT ≤ 36 kVA), 90 jours (2025-01-01 → 2025-03-31)
periodes = pl.LazyFrame({
    "pdl": ["PDL001", "PDL002"],
    "formule_tarifaire_acheminement": ["BTINFCU4", "BTINFCUST"],
    "puissance_souscrite_kva": [9.0, 6.0],
    "debut": [datetime(2025, 1, 1, tzinfo=ZoneInfo("Europe/Paris"))] * 2,
    "nb_jours": [90, 90],
})

# Calcul automatique avec règles officielles
result = ajouter_turpe_fixe(periodes).collect()

print(result.select(["pdl", "puissance_souscrite_kva", "turpe_fixe_eur"]))

Résultat (calculé contre turpe_rules.csv courant) :

┌────────┬──────────────────────────┬─────────────────┐
│ pdl    │ puissance_souscrite_kva  │ turpe_fixe_eur  │
│ str    │ f64                      │ f64             │
├────────┼──────────────────────────┼─────────────────┤
│ PDL001 │ 9.0                      │ 29.91           │
│ PDL002 │ 6.0                      │ 24.59           │
└────────┴──────────────────────────┴─────────────────┘

Exemple 2 : Calcul TURPE fixe C4 avec modulation saisonnière

# Point industriel avec 4 puissances souscrites (BT > 36 kVA), année pleine sous BTSUPCU
periodes_c4 = pl.LazyFrame({
    "pdl": ["PDL_INDUSTRIE"],
    "formule_tarifaire_acheminement": ["BTSUPCU"],
    # Toujours requise (le schéma C4/C5 est évalué dans la même expression),
    # sans effet sur le résultat C4.
    "puissance_souscrite_kva": [60.0],

    # 4 puissances par cadran temporel (économies via modulation)
    "puissance_souscrite_hph_kva": [36.0],  # Hiver Pleines Heures (P₁)
    "puissance_souscrite_hch_kva": [36.0],  # Hiver Creuses Heures (P₂)
    "puissance_souscrite_hpb_kva": [60.0],  # Été Pleines Heures (P₃)
    "puissance_souscrite_hcb_kva": [60.0],  # Été Creuses Heures (P₄)

    "debut": [datetime(2025, 9, 1, tzinfo=ZoneInfo("Europe/Paris"))],
    "nb_jours": [365],
})

result = ajouter_turpe_fixe(periodes_c4).collect()

print(f"TURPE fixe annuel C4 : {result['turpe_fixe_eur'][0]:.2f} €")

Résultat (calculé contre turpe_rules.csv courant — détail en turpe-fixe-c4-btsup36kva.md) :

TURPE fixe annuel C4 : 1484.47 €

💡 Contrainte réglementaire C4 : P₁ ≤ P₂ ≤ P₃ ≤ P₄
💡 Économies : 73.20 €/an (~4.7 %) vs puissance constante 60 kVA (1557.67 €/an)

ajouter_turpe_variable() - TURPE Variable

Signature :

def ajouter_turpe_variable(
    periodes: pl.LazyFrame,
    regles: Optional[pl.LazyFrame] = None
) -> pl.LazyFrame

Fonctionnalités : - ✅ Joint automatiquement les règles TURPE - ✅ Filtre les règles sur les dates d'application - ✅ Calcule le TURPE variable par cadran horaire - ✅ Ajoute la composante dépassement (C4 uniquement) - ✅ Conserve toutes les colonnes originales

Prérequis des données en entrée :

periodes = pl.LazyFrame({
    "pdl": [...],
    "formule_tarifaire_acheminement": [...],
    "debut": [...],  # tz-aware Europe/Paris

    # Énergies par cadran (kWh) - selon FTA :
    # 4 cadrans (Tempo/EJP, C4 ou C5) :
    "energie_hph_kwh": [...],  # Hiver Pleines Heures
    "energie_hch_kwh": [...],  # Hiver Creuses Heures
    "energie_hpb_kwh": [...],  # Été Pleines Heures
    "energie_hcb_kwh": [...],  # Été Creuses Heures

    # OU HP/HC (2 cadrans) :
    "energie_hp_kwh": [...],   # Heures Pleines
    "energie_hc_kwh": [...],   # Heures Creuses

    # OU Base (1 cadran) :
    "energie_base_kwh": [...],

    # Optionnel pour C4 - Dépassements de puissance :
    "duree_depassement_h": [...],  # Durée totale dépassement (heures)
})

Colonnes ajoutées : - turpe_variable_eur (€) - Montant TURPE variable (cadrans + dépassement)

Exemple 3 : Calcul TURPE variable C4 avec 4 cadrans

# Période avec énergies par cadran horaire, sous BTSUPCU (en vigueur depuis 2025-08-01)
periodes = pl.LazyFrame({
    "pdl": ["PDL001"],
    "formule_tarifaire_acheminement": ["BTSUPCU"],
    "debut": [datetime(2025, 9, 1, tzinfo=ZoneInfo("Europe/Paris"))],

    # Énergies C4 (kWh)
    "energie_hph_kwh": [1000.0],  # Hiver Pleines Heures
    "energie_hch_kwh": [800.0],   # Hiver Creuses Heures
    "energie_hpb_kwh": [600.0],   # Été Pleines Heures
    "energie_hcb_kwh": [400.0],   # Été Creuses Heures

    # Dépassement (optionnel)
    "duree_depassement_h": [10.0],  # 10h de dépassement total
})

result = ajouter_turpe_variable(periodes).collect()

print(f"TURPE variable : {result['turpe_variable_eur'][0]:.2f} €")

Résultat :

TURPE variable : 245.74 €

Détail : 121.64 € (cadrans) + 124.10 € (dépassement)

Exemple 4 : Calcul TURPE variable HP/HC simple

# Point résidentiel avec heures pleines/creuses (FTA BTINFMUDT — grille HP/HC)
periodes_hphc = pl.LazyFrame({
    "pdl": ["PDL002"],
    "formule_tarifaire_acheminement": ["BTINFMUDT"],
    "debut": [datetime(2025, 9, 1, tzinfo=ZoneInfo("Europe/Paris"))],

    # Énergies HP/HC (kWh)
    "energie_hp_kwh": [2000.0],
    "energie_hc_kwh": [1500.0],
})

result = ajouter_turpe_variable(periodes_hphc).collect()

print(result.select(["pdl", "energie_hp_kwh", "energie_hc_kwh", "turpe_variable_eur"]))

Résultat :

┌────────┬────────────────┬────────────────┬─────────────────────┐
│ pdl    │ energie_hp_kwh │ energie_hc_kwh │ turpe_variable_eur │
│ str    │ f64            │ f64            │ f64                 │
├────────┼────────────────┼────────────────┼─────────────────────┤
│ PDL002 │ 2000.0         │ 1500.0         │ 151.30              │
└────────┴────────────────┴────────────────┴─────────────────────┘

Expressions individuelles (Usage Avancé)

⚠️ Avertissement : Ces expressions sont conçues pour un usage interne au pipeline. Leur utilisation directe nécessite de gérer manuellement : - Le chargement et la jointure avec les règles TURPE - Le filtrage temporel des règles applicables - La validation des colonnes requises - La création/gestion de toutes les colonnes intermédiaires

Usage recommandé : Utilisez ajouter_turpe_fixe() et ajouter_turpe_variable() sauf besoin très spécifique.

Expressions TURPE Fixe

from electricore.core.pipelines.turpe import (
    expr_calculer_turpe_fixe_annuel,      # Calcul annuel C4/C5
    expr_calculer_turpe_fixe_journalier,  # Proratisation journalière
    expr_calculer_turpe_fixe_periode,     # Montant période
    expr_valider_puissances_croissantes_c4,  # Validation P₁≤P₂≤P₃≤P₄
)

Exemple minimaliste (non recommandé)

# ⚠️ Vous devez gérer manuellement les règles et colonnes
regles = load_turpe_rules()

df = pl.DataFrame({
    "formule_tarifaire_acheminement": ["BTINFCU4"],
    "puissance_souscrite_kva": [9.0],
    "debut": [datetime(2025, 1, 1, tzinfo=ZoneInfo("Europe/Paris"))],
    "nb_jours": [92],

    # Colonnes C4 obligatoires même pour C5 (l'expression évalue les deux branches) !
    "puissance_souscrite_hph_kva": [None],
    "puissance_souscrite_hch_kva": [None],
    "puissance_souscrite_hpb_kva": [None],
    "puissance_souscrite_hcb_kva": [None],
})

# Jointure et filtrage manuel — le nom de la règle jointe est "Formule_Tarifaire_Acheminement"
result = (
    df.join(regles, left_on="formule_tarifaire_acheminement",
            right_on="Formule_Tarifaire_Acheminement", how="left")
    .filter(pl.col("start") <= pl.col("debut"))
    .with_columns([
        expr_calculer_turpe_fixe_annuel().alias("turpe_annuel"),
        expr_calculer_turpe_fixe_periode().alias("turpe_periode"),
    ])
)

Expressions TURPE Variable

from electricore.core.pipelines.turpe import (
    expr_calculer_turpe_cadran,              # Calcul par cadran individuel
    expr_calculer_turpe_contributions_cadrans,  # Tous cadrans (dict)
    expr_sommer_turpe_cadrans,               # Somme des cadrans
    expr_calculer_composante_depassement,    # Pénalités dépassement (C4)
)

Exemple calcul par cadran individuel

# Calcul manuel pour un cadran spécifique
df = pl.DataFrame({
    "energie_hph_kwh": [1000.0],
    "c_hph": [6.91],  # c€/kWh (doit être récupéré des règles)
})

result = df.with_columns(
    expr_calculer_turpe_cadran("hph").alias("turpe_hph")
)

print(result["turpe_hph"][0])  # → 69.10 €

Composante dépassement (C4 uniquement)

from electricore.core.pipelines.turpe import expr_calculer_composante_depassement

⚠️ Important : Cette expression attend : - duree_depassement_h (float) - Durée totale de dépassement en heures (tous cadrans confondus) - cmdps (float) - Tarif €/h issu des règles TURPE

Le calcul des dépassements est à la charge de l'appelant (mesure Linky/analyseur).

Exemple réaliste

# Vous devez calculer vous-même les dépassements par cadran et les sommer
df = pl.DataFrame({
    "pdl": ["PDL_C4"],
    "cmdps": [12.41],  # €/h (règles TURPE pour C4)

    # Durée totale de dépassement calculée en amont
    # Exemple : somme des heures où P_réelle > P_souscrite_cadran
    "duree_depassement_h": [10.0],  # 10 heures de dépassement
})

result = df.with_columns(
    expr_calculer_composante_depassement().alias("penalite_depassement")
)

print(f"Pénalité : {result['penalite_depassement'][0]:.2f} €")

Résultat :

Pénalité : 124.10 €

Formule : duree_depassement_h × cmdps
        = 10 h × 12.41 €/h = 124.10 €

Cas d'usage pratiques

Cas 1 : Pipeline complet TURPE fixe + variable

from electricore.core.pipelines.turpe import ajouter_turpe_fixe, ajouter_turpe_variable

# Données d'entrée combinées — FTA BTINFMUDT (HP/HC), 31 jours (janvier 2025)
periodes = pl.LazyFrame({
    "pdl": ["PDL001"],
    "formule_tarifaire_acheminement": ["BTINFMUDT"],
    "puissance_souscrite_kva": [9.0],
    "debut": [datetime(2025, 1, 1, tzinfo=ZoneInfo("Europe/Paris"))],
    "nb_jours": [31],

    # Énergies HP/HC
    "energie_hp_kwh": [500.0],
    "energie_hc_kwh": [300.0],
})

# Chaînage des 2 fonctions
result = (
    periodes
    .pipe(ajouter_turpe_fixe)
    .pipe(ajouter_turpe_variable)
    .collect()
)

print(result.select(["pdl", "turpe_fixe_eur", "turpe_variable_eur"]))
# → turpe_fixe_eur: 12.87, turpe_variable_eur: 33.33

Cas 2 : Comparaison tarifaire multi-FTA

# Simuler plusieurs formules tarifaires pour un même profil, sur l'année 2025
# (le taux appliqué est celui en vigueur au DÉBUT de la période — pas de bascule
# de tarif mi-période dans ce calcul, cf. expr_filtrer_regles_temporelles)
profils = pl.LazyFrame({
    "scenario": ["Base 6kVA", "HP/HC 9kVA", "Tempo 12kVA"],
    "formule_tarifaire_acheminement": ["BTINFCUST", "BTINFMUDT", "BTINFMU4"],
    "puissance_souscrite_kva": [6.0, 9.0, 12.0],
    "debut": [datetime(2025, 1, 1, tzinfo=ZoneInfo("Europe/Paris"))] * 3,
    "nb_jours": [365] * 3,

    # Consommation propre à chaque grille tarifaire
    "energie_base_kwh": [3500.0, None, None],
    "energie_hp_kwh": [None, 2100.0, None],
    "energie_hc_kwh": [None, 1400.0, None],
    "energie_hph_kwh": [None, None, 800.0],
    "energie_hch_kwh": [None, None, 600.0],
    "energie_hpb_kwh": [None, None, 400.0],
    "energie_hcb_kwh": [None, None, 300.0],
})

# Calculs
result = (
    profils
    .pipe(ajouter_turpe_fixe)
    .pipe(ajouter_turpe_variable)
    .with_columns(
        (pl.col("turpe_fixe_eur") + pl.col("turpe_variable_eur")).alias("turpe_total_eur")
    )
    .collect()
)

print(result.select(["scenario", "turpe_fixe_eur", "turpe_variable_eur", "turpe_total_eur"]))
# → Base 6kVA        : 99.72  + 160.30 = 260.02
# → HP/HC 9kVA       : 151.56 + 144.62 = 296.18
# → Tempo 12kVA      : 169.56 + 86.27  = 255.83

Cas 3 : Analyse de sensibilité puissance souscrite

# Simuler l'impact de différentes puissances, FTA BTINFMUDT, année pleine
puissances = [6.0, 9.0, 12.0, 15.0, 18.0]

scenarios = pl.LazyFrame({
    "puissance_souscrite_kva": puissances,
    "formule_tarifaire_acheminement": ["BTINFMUDT"] * len(puissances),
    "debut": [datetime(2025, 1, 1, tzinfo=ZoneInfo("Europe/Paris"))] * len(puissances),
    "nb_jours": [365] * len(puissances),
})

result = (
    scenarios
    .pipe(ajouter_turpe_fixe)
    .with_columns(
        (pl.col("turpe_fixe_eur") / pl.col("puissance_souscrite_kva")).alias("cout_par_kva")
    )
    .collect()
)

print(result.select(["puissance_souscrite_kva", "turpe_fixe_eur", "cout_par_kva"]))
# → 6 kVA  : 113.40 € (18.90 €/kVA)
# → 9 kVA  : 151.56 € (16.84 €/kVA)
# → 12 kVA : 189.72 € (15.81 €/kVA)
# → 15 kVA : 227.88 € (15.19 €/kVA)
# → 18 kVA : 266.04 € (14.78 €/kVA)

Cas 4 : Intégration avec données Odoo

from electricore.core.loaders import OdooReader

# Charger les abonnements depuis Odoo
with OdooReader(config) as odoo:
    abonnements = (
        odoo.query('x_pdl', domain=[('x_actif', '=', True)])
        .select(['x_name', 'x_fta', 'x_puissance_souscrite', 'x_date_debut'])
        .collect()
    )

# Renommer pour compatibilité pipeline TURPE
periodes = abonnements.rename({
    "x_name": "pdl",
    "x_fta": "formule_tarifaire_acheminement",
    "x_puissance_souscrite": "puissance_souscrite_kva",
    "x_date_debut": "debut",
}).with_columns(
    (pl.col("debut").dt.offset_by("1y") - pl.col("debut")).dt.total_days().alias("nb_jours")
)

# Calcul TURPE
result = periodes.lazy().pipe(ajouter_turpe_fixe).collect()

Bonnes pratiques

1. Toujours utiliser les fonctions pipeline

# ✅ BON - Gestion automatique des règles et validations
result = ajouter_turpe_fixe(periodes)

# ❌ MAUVAIS - Réinvention de la roue, risque d'erreurs
result = periodes.with_columns(expr_calculer_turpe_fixe_annuel())

2. Charger les règles TURPE une seule fois

# ✅ BON - Réutilisation des règles
regles = load_turpe_rules()
result1 = ajouter_turpe_fixe(periodes1, regles=regles)
result2 = ajouter_turpe_fixe(periodes2, regles=regles)

# ❌ MAUVAIS - Rechargement inutile
result1 = ajouter_turpe_fixe(periodes1)  # charge les règles
result2 = ajouter_turpe_fixe(periodes2)  # recharge les règles

3. Validation des données C4

from electricore.core.pipelines.turpe import expr_valider_puissances_croissantes_c4

# Toujours valider la contrainte P₁ ≤ P₂ ≤ P₃ ≤ P₄
periodes_c4 = periodes_c4.with_columns(
    expr_valider_puissances_croissantes_c4().alias("contrainte_ok")
)

# Filtrer les lignes invalides
invalides = periodes_c4.filter(~pl.col("contrainte_ok"))
if invalides.height > 0:
    print(f"⚠️ {invalides.height} points C4 ne respectent pas P₁≤P₂≤P₃≤P₄")

4. Gestion des colonnes optionnelles

# Les fonctions pipeline gèrent automatiquement les colonnes manquantes
# Inutile de créer manuellement les colonnes C4 pour un point C5

periodes_c5 = pl.LazyFrame({
    "pdl": ["PDL001"],
    "formule_tarifaire_acheminement": ["BTINFCU4"],
    "puissance_souscrite_kva": [9.0],
    "debut": [datetime(2025, 1, 1, tzinfo=ZoneInfo("Europe/Paris"))],
    "nb_jours": [90],
    # ✅ Pas besoin de créer puissance_souscrite_hph_kva/hch_kva/hpb_kva/hcb_kva
})

result = ajouter_turpe_fixe(periodes_c5)  # Fonctionne directement

5. Utiliser LazyFrame pour optimiser les performances

# ✅ BON - LazyFrame optimisé
periodes_lf = pl.scan_parquet("periodes.parquet")
result = ajouter_turpe_fixe(periodes_lf).collect()

# ⚠️ Moins optimal - DataFrame eager
periodes_df = pl.read_parquet("periodes.parquet")
result = ajouter_turpe_fixe(periodes_df.lazy()).collect()

Ressources complémentaires


Support

Pour toute question ou cas d'usage spécifique, consultez les tests unitaires qui couvrent 40 scénarios différents avec des exemples concrets et validés.

Les fonctions ajouter_turpe_fixe() et ajouter_turpe_variable() sont conçues pour être directement réutilisables sans modification. Si vous avez besoin d'une personnalisation, ouvrez une issue pour discussion.