Aller au contenu

Détails des calculs - PT2QE

Vue d'ensemble du pipeline de calcul

PT2QE implémente un système de recommandations de prix en 5 phases séquentielles :

  1. Extraction des offres : Récupération des prix client × article depuis SYS_TARIF_SIMULATION
  2. Enrichissement historique : Calcul des métriques transactionnelles sur 4 trimestres fiscaux
  3. Matching avec corridors PT1CE : Jointure avec les corridors optimaux (MASTER ou NATIONAL)
  4. Calcul des recommandations : Application de l'arbre de décision à 3 chemins avec cascade de capping
  5. Génération des analyses : Production des exports analytiques

Toutes les phases utilisent Oracle SQL pour le traitement en masse, avec des tables temporaires compressées (COMPRESS NOLOGGING) pour optimiser les performances.

Phase 1 : Extraction des offres de prix actuelles

Périmètre d'extraction

Fichier source : extract_price_offers.py

Tables Oracle consultées : - SYS_MD_CLIENT : Référentiel clients - SYS_MD_CONDITION : Conditions tarifaires - SYS_MD_ARTICLE : Référentiel articles - SYS_TARIF_SIMULATION : Offres de prix actuelles - SYS_FACTURE_LIGNE : Historique transactionnel - PT0CE_TYPE_CLIENT_MAPPING : Mapping TYPE_CLIENT - PT0CE_TYPE_RESTAURANT_MAPPING : Mapping TYPE_RESTAURANT

Filtres appliqués :

-- Conditions tarifaires
cnd.ID_ACC IS NULL                           -- Pas d'accord
AND cnd.FG_CND_VLD = 'X'                     -- Condition valide
AND cnd.DT_FIN > SYSDATE                     -- Encore valide
AND cnd.ID_TYP_CND IN ('ZFAP', 'ZFSP', 'ZIAP', 'ZISP')
AND cnd.ID_SEQ IN ('A305', 'A565')           -- Séquences tarifaires actives

-- Clients
UPPER(TRIM(c.ID_TC_CG)) <> 'AUTRES'          -- Exclusion AUTRES
AND (
    (c.ID_STA_CLN IN ('10', '20', '30', '50') AND cnd.LC_SEQ_CLN = 'CLIENT')
    OR (c.ID_AGC <> 'A000' AND cnd.LC_SEQ_CLN IN ('HIERARCHIE CLIENT N4', 'HIERARCHIE CLIENT N5', 'HIERARCHIE CLIENT N6'))
)

-- Articles
a.ID_GMM <> 'PSN'                            -- Exclusion PSN
AND a.LC_HI1 <> 'Qualificatif article inconnu'
AND a.LC_HIC_SYSCO_N2 NOT IN ('Divers', 'Freezer')

-- ZOOM1 uniquement (filtre final)
WHERE ou.UNIVERS = 'ZOOM1'

⚠️ PÉRIMÈTRE STRICT : PT2QE ne traite QUE l'UNIVERS ZOOM1. Les offres ZOOM2 et ZOOM3 sont exclues du traitement.

Calcul de l'UNIVERS

L'UNIVERS est déterminé par des règles métier complexes basées sur : - ID_TC_CG / ID_TC_CIBLE : Type de client - FG_HM : Flag hors mercuriale (récupéré depuis l'historique transactionnel) - ID_MERC_HM : Identifiant mercuriale - ID_KAM : Présence d'un KAM

Règles de calcul :

UNIVERS = CASE
    -- ZOOM1 : RCI PI avec ou sans KAM
    WHEN ID_TC_CG = 'RCI' AND ID_TC_CIBLE = 'PINDEP' THEN 'ZOOM1'

    -- ZOOM1 : RCI GI SANS KAM
    WHEN ID_TC_CG = 'RCI' AND ID_TC_CIBLE = 'GINDEP'
        AND ID_KAM = 'NO_KAM' THEN 'ZOOM1'

    -- ZOOM2 : RCI GI AVEC KAM
    WHEN ID_TC_CG = 'RCI' AND ID_TC_CIBLE = 'GINDEP'
        AND ID_KAM <> 'NO_KAM' THEN 'ZOOM2'

    -- ZOOM1 : FG_HM = '1' (hors mercuriale) - HORS RSC
    WHEN FG_HM = '1' AND (
        (ID_TC_CG = 'RSI' AND ID_TC_CIBLE IN ('MP_AO_', 'GR_A_G', 'MPMAPA', 'SRCREG', 'CA_REG', 'CR_REG'))
        OR (ID_TC_CG = 'RCI' AND ID_TC_CIBLE IN ('PERSYS', 'PERCLI', 'AS_AUT'))
    ) THEN 'ZOOM1'

    -- ZOOM1 : RSC avec ID_MERC_HM = 'HM'
    WHEN ID_TC_CG = 'RSC' AND ID_TC_CIBLE IN ('CR_NAT', 'SRCNAT', 'MP_NAT', 'CA_NAT')
        AND TRIM(ID_MERC_HM) = 'HM' THEN 'ZOOM1'

    -- ZOOM2 : FG_HM = '0' (mercuriale)
    WHEN FG_HM = '0' AND (
        (ID_TC_CG = 'RSI' AND ID_TC_CIBLE IN ('MP_AO_', 'SRCREG', 'MPMAPA', 'CA_REG', 'CR_REG'))
        OR (ID_TC_CG = 'RCI' AND ID_TC_CIBLE = 'GC_REG')
    ) THEN 'ZOOM2'

    -- ZOOM2 : RCI GC_REG avec FG_HM = '1'
    WHEN ID_TC_CG = 'RCI' AND ID_TC_CIBLE = 'GC_REG'
        AND FG_HM = '1' THEN 'ZOOM2'

    -- ZOOM3 : RSC avec ID_MERC_HM <> 'HM'
    WHEN ID_TC_CG = 'RSC' AND ID_TC_CIBLE IN ('SRCNAT', 'CR_NAT', 'MP_NAT', 'CA_NAT')
        AND TRIM(ID_MERC_HM) <> 'HM' THEN 'ZOOM3'

    -- ZOOM3 : RCC
    WHEN ID_TC_CG = 'RCC' AND ID_TC_CIBLE IN ('RCHNAT', 'GCN3PL', 'GCNDIR') THEN 'ZOOM3'

    ELSE 'HORS PERIMETRE'
END

Récupération du FG_HM

FG_HM n'est PAS présent dans les tables de référentiel. Il est récupéré depuis l'historique transactionnel :

WITH LAST_FG_HM AS (
    SELECT 
        ID_CLN_KEY,
        ID_CLN,
        ID_ART,
        FG_HM as LAST_FG_HM,
        ID_MERC_HM,
        ROW_NUMBER() OVER (PARTITION BY ID_CLN, ID_ART ORDER BY DT_CDE DESC, ID_FAC DESC) as RN
    FROM SYS_FACTURE_LIGNE
    WHERE DT_CDE BETWEEN :start_date AND :end_date
        -- [filtres transactionnels]
)
SELECT * FROM LAST_FG_HM WHERE RN = 1

Valeurs : - '1' : Hors mercuriale - '0' : Mercuriale - NULL : Pas d'historique → classification impossible

Phase 2 : Enrichissement avec l'historique transactionnel

Détermination de la période d'analyse

Fichier source : utils/period_manager.py

Table Oracle consultée : SYS_MD_CALENDRIER_SYSCO

PT2QE utilise le calendrier fiscal Sysco (structure 4-4-5 semaines) pour déterminer automatiquement les 4 derniers trimestres fiscaux COMPLETS.

Algorithme de sélection :

def get_last_four_complete_fiscal_quarters(reference_date: date) -> Dict:
    """
    Retourne les 4 derniers trimestres fiscaux complets.

    Un trimestre est considéré COMPLET si sa dernière semaine est terminée
    (END_DATE < reference_date).
    """

Requête SQL :

WITH QUARTER_BOUNDS AS (
    SELECT
        ID_EXF,
        NO_TRF,
        ID_EXF || '_Q' || NO_TRF as QUARTER_CODE,
        MIN(ID_SEM) as START_DATE,
        MAX(ID_SEM) as END_DATE
    FROM SYS_MD_CALENDRIER_SYSCO
    WHERE ID_EXF BETWEEN '{current_year - 2}' AND '{current_year}'
    GROUP BY ID_EXF, NO_TRF
)
SELECT
    QUARTER_CODE,
    START_DATE,
    END_DATE
FROM QUARTER_BOUNDS
WHERE END_DATE < TO_DATE('{reference_date}', 'YYYY-MM-DD')
ORDER BY ID_EXF DESC, NO_TRF DESC
FETCH FIRST 4 ROWS ONLY

Exemple de résultat (si exécuté le 2025-11-03) :

Trimestres fiscaux : 2025_Q01, 2024_Q04, 2024_Q03, 2024_Q02
Période d'analyse  : 2024-02-04 → 2025-04-27

⚠️ AUTOMATIQUE : La période est recalculée à chaque exécution selon la date du jour. Aucune saisie manuelle n'est nécessaire.

Calcul des métriques transactionnelles 4Q

Filtres transactionnels appliqués :

FROM SYS_FACTURE_LIGNE f
WHERE f.DT_CDE BETWEEN :start_date AND :end_date
    AND f.ID_TYP_FAC = 'ZF2'                       -- Type de facture
    AND f.MT_GM4 IS NOT NULL                       -- Marge GM4 renseignée
    AND f.MT_CAB > 0                               -- CA positif
    AND f.QT_UF > 0                                -- Quantité positive
    AND COALESCE(TRIM(f.FG_PRESTA), '0') = '0'    -- Pas de prestation
    AND COALESCE(TRIM(f.FG_MARCHANDISE), ' ') IN ('X', '1')  -- Marchandise uniquement
    AND p.ID_PRP_KEY IS NOT NULL                   -- Prix référencé (exclusion promo via PRP_LABELS)

Métriques calculées par couple client × article :

-- Total global (hors Prix promo)
SUM(CASE WHEN TRIM(op.LC_PRP_N1) <> 'Prix promo' THEN f.MT_CAB ELSE 0 END) as MT_CAB_4Q,
SUM(CASE WHEN TRIM(op.LC_PRP_N1) <> 'Prix promo' THEN f.QT_UF ELSE 0 END) as QT_UF_4Q,
SUM(CASE WHEN TRIM(op.LC_PRP_N1) <> 'Prix promo' THEN f.QT_KG ELSE 0 END) as QT_KG_4Q,

-- Prix fermes uniquement
SUM(CASE WHEN TRIM(op.LC_PRP_N1) = 'Prix fermes' THEN f.MT_CAB ELSE 0 END) as MT_CAB_4Q_FERMES,
SUM(CASE WHEN TRIM(op.LC_PRP_N1) = 'Prix fermes' THEN f.QT_UF ELSE 0 END) as QT_UF_4Q_FERMES,
SUM(CASE WHEN TRIM(op.LC_PRP_N1) = 'Prix fermes' THEN f.QT_KG ELSE 0 END) as QT_KG_4Q_FERMES,

-- Prix indexés uniquement
SUM(CASE WHEN TRIM(op.LC_PRP_N1) = 'Prix indexés' THEN f.MT_CAB ELSE 0 END) as MT_CAB_4Q_INDEXES,
SUM(CASE WHEN TRIM(op.LC_PRP_N1) = 'Prix indexés' THEN f.QT_UF ELSE 0 END) as QT_UF_4Q_INDEXES,
SUM(CASE WHEN TRIM(op.LC_PRP_N1) = 'Prix indexés' THEN f.QT_KG ELSE 0 END) as QT_KG_4Q_INDEXES

Utilité : Ces métriques permettent d'identifier les couples client × article à fort volume, qui seront prioritaires dans l'analyse d'impact CA.

Récupération de la dernière QT_UF

WITH LAST_QT_UF AS (
    SELECT 
        ID_CLN,
        ID_ART,
        QT_UF as LAST_QT_UF,
        ROW_NUMBER() OVER (PARTITION BY ID_CLN, ID_ART ORDER BY DT_CDE DESC, ID_FAC DESC) as RN
    FROM SYS_FACTURE_LIGNE
    WHERE DT_CDE BETWEEN :start_date AND :end_date
        AND QT_UF > 0
        -- [mêmes filtres transactionnels]
)
SELECT * FROM LAST_QT_UF WHERE RN = 1

Utilité : Sert de colisage de référence pour le calcul du prix unitaire (PRIX_UNITAIRE_ACTUEL = PRIX_TARIF_ACTUEL / LAST_QT_UF).

Valeur par défaut : Si aucune transaction trouvée, LAST_QT_UF = 1.

Phase 3 : Matching avec les corridors PT1CE

Prérequis obligatoire

⚠️ CRITIQUE : PT2QE nécessite impérativement que PT1CE ait été exécuté au préalable et que les tables suivantes existent :

  • PT1CE_OPTIMAL_ZOOM1
  • PT1CE_OPTIMAL_ZOOM2
  • PT1CE_OPTIMAL_ZOOM3

Sans ces tables, PT2QE ne peut pas fonctionner.

Vérification : Utiliser l'option 5 du menu PT2QE (PT2QE_Menu.bat → "Vérifier les prérequis").

Stratégie de matching en 2 niveaux

Fichier source : utils/corridor_matcher.py

Le matching s'effectue en 2 tentatives successives avec fallback :

Niveau 1 : Match MASTER (prioritaire)

SELECT 
    o.*,
    c.* 
FROM offers o
LEFT JOIN PT1CE_OPTIMAL_ZOOM1 c
    ON o.ID_ART = c.ID_ART
    AND o.TYPE_CLIENT = c.TYPE_CLIENT
    AND o.TYPE_RESTAURANT = c.TYPE_RESTAURANT
    AND NVL(o.GEO, 'NULL') = NVL(c.GEO, 'NULL')
    AND c.CUBE_TYPE = 'MASTER'
    AND c.STATUS = 'OPTIMAL'
WHERE c.ID_ART IS NOT NULL

Dimensions de matching : - ID_ART : Article exact - TYPE_CLIENT : Type de client mappé - TYPE_RESTAURANT : Type de restaurant mappé - GEO : Zone géographique (avec gestion des NULL)

Flag résultant : MATCH_TYPE = 'MASTER'

Niveau 2 : Match NATIONAL (fallback)

Si aucun corridor MASTER n'est trouvé :

SELECT 
    o.*,
    c.* 
FROM offers o
JOIN PT1CE_OPTIMAL_ZOOM1 c
    ON o.ID_ART = c.ID_ART
    AND c.TYPE_CLIENT = 'NATIONAL'
    AND c.TYPE_RESTAURANT = 'NATIONAL'
    AND (c.GEO = 'NATIONAL' OR c.GEO IS NULL)
    AND c.CUBE_TYPE = 'NATIONAL'
    AND c.STATUS = 'OPTIMAL'
WHERE NOT EXISTS (
    SELECT 1 FROM MATCH_MASTER m 
    WHERE m.ID_CLN = o.ID_CLN AND m.ID_ART = o.ID_ART
)

Particularité NATIONAL : Agrégation tous clients confondus pour l'article, utilisée comme valeur de secours.

Flag résultant : MATCH_TYPE = 'NATIONAL'

Niveau 3 : Aucun corridor (exclusion)

SELECT 
    o.*,
    NULL as [toutes les colonnes corridor]
FROM offers o
WHERE NOT EXISTS (SELECT 1 FROM MATCH_MASTER WHERE ...)
    AND NOT EXISTS (SELECT 1 FROM MATCH_NATIONAL WHERE ...)

Flag résultant : MATCH_TYPE = 'NO_MATCH' et HAS_CORRIDOR = 0

⚠️ EXCLUSION : Les offres sans corridor sont exclues du calcul des recommandations.

Données récupérées depuis PT1CE

Anciennes bornes (PT0CE - référence historique) :

PAS_ACTIF, PRB_ACTIF, PRB_RC_ACTIF, PRB_COLL_ACTIF, PRB_TO_USE,
BORNE_PL1_PL2, BORNE_PL2_PL3, BORNE_PL3_PL4,
BORNE_PL4_PL5, BORNE_PL5_PL6, BORNE_PL6_PLX

Nouvelles bornes (PT1CE - post-optimisation) :

NEW_PAS, NEW_PRB, NEW_PRB_RC, NEW_PRB_COLL,
NEW_BORNE_PL1_PL2, NEW_BORNE_PL2_PL3, NEW_BORNE_PL3_PL4,
NEW_BORNE_PL4_PL5, NEW_BORNE_PL5_PL6, NEW_BORNE_PL6_PLX

Métadonnées corridor :

PRICE_SENSITIVITY,  -- HIGH / MEDIUM / LOW
ECART_TYPE,         -- Écart-type des prix observés
CUBE_TYPE,          -- MASTER / NATIONAL
STATUS              -- OPTIMAL (seul statut retenu)

Calcul du PRB effectif

PRB_ACTIF = CASE 
    WHEN PRB_TO_USE = 1 THEN PRB_RC_ACTIF 
    ELSE PRB_COLL_ACTIF 
END

NEW_PRB = CASE 
    WHEN PRB_TO_USE = 1 THEN NEW_PRB_RC 
    ELSE NEW_PRB_COLL 
END

Logique : PRB_TO_USE détermine si on utilise le PRB RCI (PRB_RC) ou le PRB COLLECTIVITÉ (PRB_COLL).

Calcul du pourcentage de hausse PAS

PCT_HAUSSE_PAS = CASE 
    WHEN PAS_ACTIF > 0 AND NEW_PAS > 0 
    THEN (NEW_PAS - PAS_ACTIF) / PAS_ACTIF
    ELSE 0
END

Utilité : Cette métrique sert de base pour le calcul de RECO2 (hausse proportionnelle).

Phase 4 : Calcul des recommandations avec arbre de décision

Architecture de l'arbre de décision

PT2QE implémente 3 CHEMINS décisionnels distincts selon le contexte de l'offre :

                    DÉBUT
          ┌───────────────────────┐
          │  NEW_PAS < PAS_ACTIF ? │
          └───────────┬───────────┘
        ┌─────────────┴──────────────┐
        │ OUI                   NON  │
        ▼                            ▼
    CHEMIN 1                ┌──────────────────────────────┐
    GEL PRIX                │ Prix actuel dans PL1 ancien ? │
                            └──────────┬───────────────────┘
                         ┌─────────────┴─────────────┐
                         │ OUI                  NON  │
                         ▼                           ▼
                     CHEMIN 2                    CHEMIN 3
                     CONSERVATION                OPTIMISATION
                     PREMIUM                     STANDARD

CHEMIN 1 : PAS_BAISSE_GEL_PRIX

Condition de déclenchement :

IS_PAS_BAISSE = CASE 
    WHEN NEW_PAS < PAS_ACTIF THEN 1 
    ELSE 0 
END

DECISION_PATH = CASE 
    WHEN IS_PAS_BAISSE = 1 THEN 'PAS_BAISSE_GEL_PRIX'
    -- ...
END

Action :

PRIX_RECOMMANDE = PRIX_TARIF_ACTUEL  -- Gel total, aucune hausse
PCT_HAUSSE_FINALE = 0

Rationale métier : Ne pas répercuter les baisses de coût d'achat au client. Le prix reste inchangé même si le nouveau PAS est plus bas que l'ancien.

RECO_SELECTIONNEE : 'GEL_PRIX'

CAPPING_APPLIED : 'GEL_PAS' (priorité absolue)

Statistiques observables : Utiliser le fichier decision_path_analysis.csv pour voir le nombre d'offres concernées.

CHEMIN 2 : PL1_CONSERVATION_PREMIUM

Condition de déclenchement :

IS_PL1_ANCIENNES = CASE 
    WHEN PRIX_TARIF_ACTUEL <= PRB_ACTIF
        AND PRIX_TARIF_ACTUEL > BORNE_PL1_PL2
    THEN 1 
    ELSE 0 
END

DECISION_PATH = CASE 
    WHEN IS_PAS_BAISSE = 0 
        AND IS_PL1_ANCIENNES = 1 
    THEN 'PL1_CONSERVATION_PREMIUM'
    -- ...
END

Vérification : Le prix actuel doit être dans le palier PL1 des anciennes bornes PT0CE (avant optimisation PT1CE).

Action en 2 étapes :

-- Étape 1 : Conservation avec plancher de sécurité
PRIX_AVANT_CAPPING_PRB = GREATEST(PRIX_TARIF_ACTUEL, NEW_BORNE_PL2_PL3)

-- Étape 2 : Application capping PRB final
PRIX_RECOMMANDE = LEAST(PRIX_AVANT_CAPPING_PRB, NEW_PRB)

Rationale métier : Maintenir les clients premium en position haute, tout en appliquant un plancher de sécurité à la borne PL2_PL3 des nouvelles bornes pour éviter une compression excessive du corridor.

RECO_SELECTIONNEE : 'CONSERVATION_PREMIUM'

CAPPING_APPLIED : - 'PRB_FINAL' si le prix dépasse NEW_PRB (capping plafond) - 'PLANCHER_PL2_PL3' si le prix était sous NEW_BORNE_PL2_PL3 (capping plancher) - 'NONE' si aucun des deux cas

Exemple concret :

Prix actuel       : 25,00 €
PRB_ACTIF (ancien): 27,00 €  } → Prix dans PL1 anciennes bornes
BORNE_PL1_PL2     : 23,00 €  }

NEW_BORNE_PL2_PL3 : 24,00 €  (plancher)
NEW_PRB           : 28,00 €  (plafond)

Calcul :
  Étape 1 : MAX(25,00 ; 24,00) = 25,00 €
  Étape 2 : MIN(25,00 ; 28,00) = 25,00 €

Résultat : Prix inchangé à 25,00 € (déjà entre plancher et plafond)
CAPPING_APPLIED = 'NONE'

CHEMIN 3 : OPTIMISATION_STANDARD

Condition de déclenchement :

DECISION_PATH = CASE 
    WHEN IS_PAS_BAISSE = 0 
        AND IS_PL1_ANCIENNES = 0 
    THEN 'OPTIMISATION_STANDARD'
    -- ...
END

Utilisation : Tous les autres cas (majorité des offres).

Action : Calcul de RECO1 et RECO2

Ce chemin calcule 2 recommandations différentes puis sélectionne le maximum :

PRIX_AVANT_CAPPING_PRB = GREATEST(RECO1_AVEC_CAPPING, RECO2)
PRIX_RECOMMANDE = LEAST(PRIX_AVANT_CAPPING_PRB, NEW_PRB)

RECO1 : Repositionnement par paliers

Caractéristique : Logique PARAMÉTRABLE via le fichier de configuration.

Fichier de configuration : config/pt2qe_config.json

Structure du paramétrage :

{
  "recommendations": {
    "reco1_rules": [
      {
        "position": "ABOVE_PL1",
        "condition": "PRIX_TARIF_ACTUEL > NEW_BORNE_PL1_PL2",
        "action": "NO_CHANGE",
        "target": "PRIX_TARIF_ACTUEL",
        "comment": "Prix déjà en PL1, pas de changement"
      },
      {
        "position": "PL1_PL2",
        "condition": "PRIX_TARIF_ACTUEL > NEW_BORNE_PL2_PL3",
        "action": "TO_PL1",
        "target": "NEW_BORNE_PL1_PL2",
        "comment": "Remonter vers PL1"
      },
      {
        "position": "PL2_PL3",
        "condition": "PRIX_TARIF_ACTUEL > NEW_BORNE_PL3_PL4",
        "action": "TO_PL1",
        "target": "NEW_BORNE_PL1_PL2",
        "comment": "Remonter vers PL1"
      },
      {
        "position": "PL3_PL4",
        "condition": "PRIX_TARIF_ACTUEL > NEW_BORNE_PL4_PL5",
        "action": "TO_PL2",
        "target": "NEW_BORNE_PL2_PL3",
        "comment": "Remonter vers PL2"
      },
      {
        "position": "PL4_PL5",
        "condition": "PRIX_TARIF_ACTUEL > NEW_BORNE_PL5_PL6",
        "action": "TO_PL3",
        "target": "NEW_BORNE_PL3_PL4",
        "comment": "Remonter vers PL3"
      },
      {
        "position": "PL5_PL6",
        "condition": "PRIX_TARIF_ACTUEL > NEW_BORNE_PL6_PLX",
        "action": "TO_PL5",
        "target": "NEW_BORNE_PL5_PL6",
        "comment": "Remonter vers PL5"
      },
      {
        "position": "PL6_PLX",
        "condition": "PRIX_TARIF_ACTUEL >= NEW_PAS",
        "action": "TO_PL6",
        "target": "NEW_BORNE_PL6_PLX",
        "comment": "Remonter vers PL6"
      },
      {
        "position": "BELOW_PAS",
        "condition": "DEFAULT",
        "action": "TO_PAS",
        "target": "NEW_PAS",
        "comment": "Remonter au PAS minimum"
      }
    ]
  }
}

Traduction en SQL : Le module config/pt2qe_config.py génère automatiquement le CASE SQL :

def build_reco1_sql_case(table_alias: str = "ed") -> str:
    """Génère le CASE SQL pour RECO1_BASE à partir des règles configurées."""
    rules = self.get_reco1_rules()

    case_parts = ["CASE"]
    for rule in rules:
        condition = rule['condition']
        target = rule['target']
        comment = rule.get('comment', rule['action'])

        # Remplacer les noms de colonnes par l'alias
        condition_sql = condition.replace("PRIX_TARIF_ACTUEL", f"{table_alias}.PRIX_TARIF_ACTUEL")
        condition_sql = condition_sql.replace("NEW_", f"{table_alias}.NEW_")

        target_sql = target.replace("PRIX_TARIF_ACTUEL", f"{table_alias}.PRIX_TARIF_ACTUEL")
        target_sql = target_sql.replace("NEW_", f"{table_alias}.NEW_")

        if condition == "DEFAULT":
            case_parts.append(f"    ELSE {target_sql}  -- {comment}")
        else:
            case_parts.append(f"    WHEN {condition_sql} THEN {target_sql}  -- {comment}")

    case_parts.append("END")
    return "\n".join(case_parts)

Résultat SQL généré :

RECO1_BASE = CASE
    WHEN ed.PRIX_TARIF_ACTUEL > ed.NEW_BORNE_PL1_PL2 THEN ed.PRIX_TARIF_ACTUEL  -- Prix déjà en PL1, pas de changement
    WHEN ed.PRIX_TARIF_ACTUEL > ed.NEW_BORNE_PL2_PL3 THEN ed.NEW_BORNE_PL1_PL2  -- Remonter vers PL1
    WHEN ed.PRIX_TARIF_ACTUEL > ed.NEW_BORNE_PL3_PL4 THEN ed.NEW_BORNE_PL1_PL2  -- Remonter vers PL1
    WHEN ed.PRIX_TARIF_ACTUEL > ed.NEW_BORNE_PL4_PL5 THEN ed.NEW_BORNE_PL2_PL3  -- Remonter vers PL2
    WHEN ed.PRIX_TARIF_ACTUEL > ed.NEW_BORNE_PL5_PL6 THEN ed.NEW_BORNE_PL3_PL4  -- Remonter vers PL3
    WHEN ed.PRIX_TARIF_ACTUEL > ed.NEW_BORNE_PL6_PLX THEN ed.NEW_BORNE_PL5_PL6  -- Remonter vers PL5
    WHEN ed.PRIX_TARIF_ACTUEL >= ed.NEW_PAS THEN ed.NEW_BORNE_PL6_PLX  -- Remonter vers PL6
    ELSE ed.NEW_PAS  -- Remonter au PAS minimum
END

⚠️ MODIFICATION DES RÈGLES :

Pour modifier la logique de repositionnement RECO1 :

  1. Éditer le fichier config/pt2qe_config.json
  2. Modifier les règles dans la section recommendations.reco1_rules
  3. Relancer PT2QE (pas besoin de modifier le code Python/SQL)

Exemple de modification : Faire remonter les prix PL5_PL6 vers PL4 au lieu de PL5 :

{
  "position": "PL5_PL6",
  "condition": "PRIX_TARIF_ACTUEL > NEW_BORNE_PL6_PLX",
  "action": "TO_PL4",
  "target": "NEW_BORNE_PL4_PL5",  // ← Changé de PL5_PL6 à PL4_PL5
  "comment": "Remonter vers PL4"
}

RECO2 : Hausse proportionnelle au PAS

Formule :

RECO2 = PRIX_TARIF_ACTUEL * (1 + PCT_HAUSSE_PAS)

 PCT_HAUSSE_PAS = (NEW_PAS - PAS_ACTIF) / PAS_ACTIF

Rationale métier : Répercuter proportionnellement la hausse du coût d'achat (PAS) sur le prix de vente.

Exemple :

Prix actuel : 15,00 €
PAS ancien  : 10,00 €
PAS nouveau : 11,00 €  → Hausse de 10%

RECO2 = 15,00 × 1,10 = 16,50 €

Cas particuliers :

-- Si PAS nul ou négatif
WHEN PAS_ACTIF <= 0 OR NEW_PAS <= 0 THEN RECO2 = PRIX_TARIF_ACTUEL

-- Si PAS en baisse (ne devrait pas arriver dans CHEMIN 3)
WHEN NEW_PAS < PAS_ACTIF THEN RECO2 < PRIX_TARIF_ACTUEL

Cascade de capping (CHEMIN 3 uniquement)

PT2QE applique une cascade de 3 cappings successifs sur RECO1, dans l'ordre suivant :

RECO1_BASE
┌───────────────────────────────────┐
│ 1. CAPPING SENSIBILITÉ PRIX       │
│    (HIGH / MEDIUM / LOW)          │
└───────────┬───────────────────────┘
  RECO1_APRES_CAPPING_SENSIBILITE
┌───────────────────────────────────┐
│ 2. CAPPING PRODUITS BASIQUES      │
│    (+50% max)                     │
└───────────┬───────────────────────┘
     RECO1_AVEC_CAPPING
┌───────────────────────────────────┐
│ 3. SÉLECTION MAX(RECO1, RECO2)    │
└───────────┬───────────────────────┘
   PRIX_AVANT_CAPPING_PRB
┌───────────────────────────────────┐
│ 4. CAPPING PRB FINAL (TOUS)       │
│    (Plafond absolu)               │
└───────────┬───────────────────────┘
      PRIX_RECOMMANDE

Étape 1 : Capping sensibilité prix

Source des paramètres : Fichier inputs/capping_type_client.csv

Structure du fichier :

TYPE_CLIENT;CAPPING_HIGH;CAPPING_MEDIUM;CAPPING_LOW
RCI PI GI;0,025;0,05;0,075
RSI HM;0,025;0,05;0,075
RSC HM;0,025;0,05;0,075
NATIONAL;0,025;0,05;0,075

Format : - Séparateur : point-virgule (;) - Décimale : virgule (,) - Encodage : CP1252

Valeurs standard : - CAPPING_HIGH : 2,5% (produits très sensibles) - CAPPING_MEDIUM : 5,0% (sensibilité moyenne) - CAPPING_LOW : 7,5% (peu sensibles)

Application :

RECO1_APRES_CAPPING_SENSIBILITE = CASE
    WHEN PRICE_SENSITIVITY = 'HIGH' AND CAPPING_HIGH IS NOT NULL THEN
        LEAST(RECO1_BASE, PRIX_TARIF_ACTUEL * (1 + CAPPING_HIGH))
    WHEN PRICE_SENSITIVITY = 'MEDIUM' AND CAPPING_MEDIUM IS NOT NULL THEN
        LEAST(RECO1_BASE, PRIX_TARIF_ACTUEL * (1 + CAPPING_MEDIUM))
    WHEN PRICE_SENSITIVITY = 'LOW' AND CAPPING_LOW IS NOT NULL THEN
        LEAST(RECO1_BASE, PRIX_TARIF_ACTUEL * (1 + CAPPING_LOW))
    ELSE RECO1_BASE
END

Détection du capping :

CAPPING_SENSIBILITE_APPLIED = CASE
    WHEN PRICE_SENSITIVITY = 'HIGH' AND CAPPING_HIGH IS NOT NULL 
        AND RECO1_BASE > PRIX_TARIF_ACTUEL * (1 + CAPPING_HIGH) 
    THEN 1
    -- ... (idem pour MEDIUM et LOW)
    ELSE 0
END

Exemple :

Prix actuel       : 20,00 €
RECO1_BASE        : 23,00 €  (repositionnement vers PL2)
PRICE_SENSITIVITY : HIGH
CAPPING_HIGH      : 0,025 (2,5%)

Prix max autorisé : 20,00 × 1,025 = 20,50 €
RECO1_APRES_CAPPING_SENSIBILITE = MIN(23,00 ; 20,50) = 20,50 €

→ Capping appliqué : hausse limitée à +2,5%

Étape 2 : Capping produits basiques

Condition : LC_ATTRIBUT = 'Basiques'

Règle : Hausse maximale de 50% par rapport au prix actuel, quel que soit le capping sensibilité.

Application :

RECO1_AVEC_CAPPING = CASE
    WHEN LC_ATTRIBUT = 'Basiques' THEN
        LEAST(RECO1_APRES_CAPPING_SENSIBILITE, PRIX_TARIF_ACTUEL * 1.50)
    ELSE RECO1_APRES_CAPPING_SENSIBILITE
END

Détection du capping :

CAPPING_BASIQUES_APPLIED = CASE
    WHEN LC_ATTRIBUT = 'Basiques' 
        AND RECO1_APRES_CAPPING_SENSIBILITE > PRIX_TARIF_ACTUEL * 1.50
    THEN 1
    ELSE 0
END

⚠️ ÉCRASE LE CAPPING SENSIBILITÉ : Le capping basiques s'applique après le capping sensibilité et peut le remplacer s'il est plus restrictif.

Exemple :

Prix actuel                        : 10,00 €
RECO1_BASE                         : 25,00 €
RECO1_APRES_CAPPING_SENSIBILITE    : 15,00 €  (après capping LOW à 50%)
LC_ATTRIBUT                        : 'Basiques'

Prix max basiques : 10,00 × 1,50 = 15,00 €
RECO1_AVEC_CAPPING = MIN(15,00 ; 15,00) = 15,00 €

→ Dans ce cas, le capping basiques n'ajoute pas de restriction supplémentaire

Exemple où le capping basiques est plus restrictif :

Prix actuel                        : 10,00 €
RECO1_BASE                         : 25,00 €
RECO1_APRES_CAPPING_SENSIBILITE    : 20,00 €  (après capping LOW à 100%)
LC_ATTRIBUT                        : 'Basiques'

Prix max basiques : 10,00 × 1,50 = 15,00 €
RECO1_AVEC_CAPPING = MIN(20,00 ; 15,00) = 15,00 €

→ Le capping basiques remplace le capping sensibilité (plus restrictif)

Étape 3 : Sélection MAX(RECO1, RECO2)

PRIX_AVANT_CAPPING_PRB = GREATEST(RECO1_AVEC_CAPPING, RECO2)

RECO_TYPE = CASE
    WHEN RECO1_AVEC_CAPPING >= RECO2 THEN 'REPOSITIONNEMENT_PALIERS'
    ELSE 'HAUSSE_PROPORTIONNELLE_PAS'
END

RECO_SELECTIONNEE = CASE
    WHEN RECO1_AVEC_CAPPING >= RECO2 THEN 'RECO1_REPOSITIONNEMENT_PALIERS'
    ELSE 'RECO2_HAUSSE_PROPORTIONNELLE_PAS'
END

Logique : Sélection de la recommandation la plus favorable (= la plus haute) pour maximiser le repositionnement tarifaire.

Exemple comparatif :

Scénario A : RECO1 gagne
----------------------------
RECO1_AVEC_CAPPING : 18,50 €
RECO2              : 16,20 €
→ Prix sélectionné : 18,50 € (RECO1_REPOSITIONNEMENT_PALIERS)

Scénario B : RECO2 gagne
----------------------------
RECO1_AVEC_CAPPING : 14,00 €
RECO2              : 15,80 €
→ Prix sélectionné : 15,80 € (RECO2_HAUSSE_PROPORTIONNELLE_PAS)

Étape 4 : Capping PRB final (TOUS LES CHEMINS)

Application universelle : Ce capping s'applique à tous les chemins décisionnels (CHEMIN 1, 2 et 3).

PRIX_RECOMMANDE = LEAST(PRIX_AVANT_CAPPING_PRB, NEW_PRB)

CAPPING_PRB_APPLIED = CASE
    WHEN PRIX_AVANT_CAPPING_PRB > NEW_PRB THEN 1
    ELSE 0
END

Rationale métier : Le PRB (Prix de Référence Brut) est un plafond absolu infranchissable. Aucun prix recommandé ne peut dépasser le NEW_PRB, sous peine de compression du corridor.

Exemple :

PRIX_AVANT_CAPPING_PRB : 32,00 €
NEW_PRB                : 30,00 €

PRIX_RECOMMANDE = MIN(32,00 ; 30,00) = 30,00 €
CAPPING_PRB_APPLIED = 1

→ Hausse plafonnée au PRB

Synthèse du capping appliqué

Le champ CAPPING_APPLIED indique le capping le plus contraignant qui a été appliqué, selon l'ordre de priorité suivant :

CAPPING_APPLIED = CASE
    -- Priorité 1 : Gel total (aucun mouvement)
    WHEN DECISION_PATH = 'PAS_BAISSE_GEL_PRIX' 
    THEN 'GEL_PAS'

    -- Priorité 2 : Capping PRB final (plafond absolu)
    WHEN CAPPING_PRB_APPLIED = 1 
    THEN 'PRB_FINAL'

    -- Priorité 3 : Plancher de sécurité (CHEMIN 2)
    WHEN PLANCHER_PL2_APPLIED = 1 
    THEN 'PLANCHER_PL2_PL3'

    -- Priorité 4 : Capping basiques (CHEMIN 3)
    WHEN CAPPING_BASIQUES_APPLIED = 1
    THEN 'BASIQUES_50PCT'

    -- Priorité 5 : Capping sensibilité (CHEMIN 3)
    WHEN CAPPING_SENSIBILITE_APPLIED = 1 
    THEN 'SENSIBILITE'

    -- Aucun capping appliqué
    ELSE 'NONE'
END

Valeurs possibles : - GEL_PAS : Gel total (CHEMIN 1) - PRB_FINAL : Plafond PRB (tous chemins) - PLANCHER_PL2_PL3 : Plancher de sécurité (CHEMIN 2) - BASIQUES_50PCT : Capping produits basiques (CHEMIN 3) - SENSIBILITE : Capping sensibilité prix (CHEMIN 3) - NONE : Aucun capping

Utilité : Permet d'analyser statistiquement l'impact de chaque type de capping via le fichier capping_distribution.csv.

Calcul du pourcentage de hausse finale

PCT_HAUSSE_FINALE = CASE 
    WHEN PRIX_TARIF_ACTUEL > 0 THEN
        (PRIX_RECOMMANDE - PRIX_TARIF_ACTUEL) / PRIX_TARIF_ACTUEL
    ELSE 0
END

Format de stockage : Décimal (ex: 0.05 pour 5%)

Format d'export : Pourcentage avec 2 décimales (ex: 5,00%)

Cas particuliers : - Prix actuel nul : 0% - CHEMIN 1 (gel) : 0% - Prix recommandé identique : 0%

Les 3 positions dans le corridor

PT2QE calcule 3 positions distinctes pour tracer l'évolution du prix dans le corridor :

Position 1 : Prix actuel dans ANCIENNES bornes (PT0CE)

POSITION_TARIF_ACTUEL_DANS_ANCIENNES_BORNES = CASE
    WHEN PRIX_TARIF_ACTUEL > PRB_ACTIF THEN 'ABOVE_PRB'
    WHEN PRIX_TARIF_ACTUEL >= BORNE_PL1_PL2 THEN 'PL1'
    WHEN PRIX_TARIF_ACTUEL >= BORNE_PL2_PL3 THEN 'PL2'
    WHEN PRIX_TARIF_ACTUEL >= BORNE_PL3_PL4 THEN 'PL3'
    WHEN PRIX_TARIF_ACTUEL >= BORNE_PL4_PL5 THEN 'PL4'
    WHEN PRIX_TARIF_ACTUEL >= BORNE_PL5_PL6 THEN 'PL5'
    WHEN PRIX_TARIF_ACTUEL >= BORNE_PL6_PLX THEN 'PL6'
    WHEN PRIX_TARIF_ACTUEL >= PAS_ACTIF THEN 'PLX'
    ELSE 'BELOW_PAS'
END

Utilité : Montre la position de départ avant toute intervention (référence historique PT0CE).

Position 2 : Prix actuel dans NOUVELLES bornes (PT1CE)

PALIER_TARIF_ACTUEL_VS_NOUVELLES_BORNES = CASE
    WHEN PRIX_TARIF_ACTUEL > NEW_PRB THEN 'ABOVE_PRB'
    WHEN PRIX_TARIF_ACTUEL >= NEW_BORNE_PL1_PL2 THEN 'PL1'
    WHEN PRIX_TARIF_ACTUEL >= NEW_BORNE_PL2_PL3 THEN 'PL2'
    WHEN PRIX_TARIF_ACTUEL >= NEW_BORNE_PL3_PL4 THEN 'PL3'
    WHEN PRIX_TARIF_ACTUEL >= NEW_BORNE_PL4_PL5 THEN 'PL4'
    WHEN PRIX_TARIF_ACTUEL >= NEW_BORNE_PL5_PL6 THEN 'PL5'
    WHEN PRIX_TARIF_ACTUEL >= NEW_BORNE_PL6_PLX THEN 'PL6'
    WHEN PRIX_TARIF_ACTUEL >= NEW_PAS THEN 'PLX'
    ELSE 'BELOW_PAS'
END

Utilité : Montre l'impact du recalibrage PT1CE (les bornes ont changé, donc la position peut avoir bougé même si le prix est identique).

Exemple d'effet de recalibrage :

Prix actuel : 15,00 €

Anciennes bornes (PT0CE) :
  PL2_PL3 : 14,00 €
  PL3_PL4 : 17,00 €
→ Position 1 : PL3 (entre 14 et 17)

Nouvelles bornes (PT1CE) :
  PL2_PL3 : 16,00 €
  PL3_PL4 : 19,00 €
→ Position 2 : PL2 (sous 16)

Le prix n'a pas changé, mais sa position dans le corridor si !

Position 3 : Prix recommandé dans NOUVELLES bornes (PT2QE)

POSITION_NOUVEAU_PRIX_DANS_NOUVELLES_BORNES = CASE
    WHEN PRIX_RECOMMANDE > NEW_PRB THEN 'ABOVE_PRB'
    WHEN PRIX_RECOMMANDE >= NEW_BORNE_PL1_PL2 THEN 'PL1'
    WHEN PRIX_RECOMMANDE >= NEW_BORNE_PL2_PL3 THEN 'PL2'
    WHEN PRIX_RECOMMANDE >= NEW_BORNE_PL3_PL4 THEN 'PL3'
    WHEN PRIX_RECOMMANDE >= NEW_BORNE_PL4_PL5 THEN 'PL4'
    WHEN PRIX_RECOMMANDE >= NEW_BORNE_PL5_PL6 THEN 'PL5'
    WHEN PRIX_RECOMMANDE >= NEW_BORNE_PL6_PLX THEN 'PL6'
    WHEN PRIX_RECOMMANDE >= NEW_PAS THEN 'PLX'
    ELSE 'BELOW_PAS'
END

Utilité : Montre l'objectif atteint après application des recommandations PT2QE.

Exemple de trajectoire complète

Article X - Client Y

Position 1 (PT0CE)  : PL4  (anciennes bornes)
Position 2 (PT1CE)  : PL3  (nouvelles bornes, sans changement de prix)
Position 3 (PT2QE)  : PL2  (après recommandation)

Interprétation :
- Le prix était en PL4 dans l'ancien corridor
- Le recalibrage PT1CE l'a fait monter en PL3 (bornes plus basses)
- PT2QE recommande de le faire monter encore en PL2 (repositionnement)

Utilité analytique : Comparer les 3 positions permet de visualiser l'effet combiné de PT1CE + PT2QE.

Gestion avancée des cappings par cube

Architecture des cappings

PT2QE utilise un système de cappings par cube multidimensionnel :

CUBE = UNIVERS × TYPE_CLIENT × TYPE_RESTAURANT × GEO

Fichier source : utils/capping_manager.py

Génération initiale des cappings

Phase 1 : Chargement des cappings TYPE_CLIENT

Fichier : inputs/capping_type_client.csv

def load_type_client_capping(capping_file: Path):
    df = pd.read_csv(capping_file, sep=';', decimal=',', encoding='cp1252')

    for _, row in df.iterrows():
        self.type_client_cappings[row['TYPE_CLIENT']] = {
            'high': row['CAPPING_HIGH'],
            'medium': row['CAPPING_MEDIUM'],
            'low': row['CAPPING_LOW']
        }

Phase 2 : Génération des cubes

CREATE TABLE PT2QE_CAPPING_CUBES AS
-- Cubes MASTER
SELECT DISTINCT
    UNIVERS,
    TYPE_CLIENT,
    TYPE_RESTAURANT,
    GEO,
    'MASTER' as CUBE_TYPE,
    0.05 as CAPPING_HIGH,   -- Valeur par défaut
    0.15 as CAPPING_MEDIUM,
    0.20 as CAPPING_LOW
FROM enriched_offers
WHERE HAS_CORRIDOR = 1
    AND MATCH_TYPE = 'MASTER'
    AND UNIVERS = 'ZOOM1'

UNION ALL

-- Cubes NATIONAL
SELECT DISTINCT
    UNIVERS,
    'NATIONAL' as TYPE_CLIENT,
    'NATIONAL' as TYPE_RESTAURANT,
    'NATIONAL' as GEO,
    'NATIONAL' as CUBE_TYPE,
    0.03 as CAPPING_HIGH,   -- Plus conservateur
    0.10 as CAPPING_MEDIUM,
    0.15 as CAPPING_LOW
FROM enriched_offers
WHERE HAS_CORRIDOR = 1
    AND MATCH_TYPE = 'NATIONAL'
    AND UNIVERS = 'ZOOM1'

Phase 3 : Application des cappings du fichier CSV

-- Mise à jour des cubes MASTER
UPDATE PT2QE_CAPPING_CUBES
SET CAPPING_HIGH = :high,
    CAPPING_MEDIUM = :medium,
    CAPPING_LOW = :low
WHERE TYPE_CLIENT = :type_client
    AND CUBE_TYPE = 'MASTER'

Phase 4 : Export initial

Fichier généré : outputs/run_YYYYMMDD_HHMMSS/capping_cubes_generated.csv

UNIVERS;TYPE_CLIENT;TYPE_RESTAURANT;GEO;CAPPING_HIGH;CAPPING_MEDIUM;CAPPING_LOW
ZOOM1;RCI PI GI;RESTAURATION TRADITIONNELLE;ILE DE FRANCE;0,025;0,05;0,075
ZOOM1;RCI PI GI;RESTAURATION TRADITIONNELLE;NORD-EST;0,025;0,05;0,075
ZOOM1;RSI HM;RESTAURATION RAPIDE;SUD-OUEST;0,025;0,05;0,075
...

Ajustement manuel des cappings (itératif)

Objectif : Permettre d'affiner les cappings par cube spécifique après analyse des premiers résultats.

Workflow :

  1. Analyse des résultats initiaux

Consulter les fichiers : - recommendations_detail.csv : Hausses par offre - capping_distribution.csv : Impact des cappings - decision_path_analysis.csv : Statistiques par chemin

  1. Identification des cubes à ajuster

Exemple : Trop de hausses > 10% pour le cube ZOOM1 × RCI PI GI × RESTAURATION TRADITIONNELLE × ILE DE FRANCE

  1. Copie du fichier de cappings
cd outputs\run_20251103_143022
copy capping_cubes_generated.csv ..\..\corrections\capping_cubes_corrections.csv
  1. Modification du fichier de corrections

Éditer corrections\capping_cubes_corrections.csv :

UNIVERS;TYPE_CLIENT;TYPE_RESTAURANT;GEO;CAPPING_HIGH;CAPPING_MEDIUM;CAPPING_LOW
ZOOM1;RCI PI GI;RESTAURATION TRADITIONNELLE;ILE DE FRANCE;0,020;0,040;0,060

⚠️ IMPORTANT : - Conserver toutes les colonnes (y compris les dimensions) - Ne modifier que les lignes des cubes à ajuster - Les autres lignes peuvent être supprimées (seules les lignes présentes seront mises à jour)

  1. Application des corrections

Lancer le batch : 2_ajuster_cappings.bat ou Menu option 2

Action interne :

def apply_capping_corrections(recommendations_table: str, corrections_folder: Path):
    corrections_df = pd.read_csv(corrections_folder / 'capping_cubes_corrections.csv')

    # Upload dans table temporaire
    temp_table = "TEMP_CAPPING_CORRECTIONS"
    _upload_corrections(corrections_df, temp_table)

    # MERGE pour mise à jour
    _update_recommendations(recommendations_table, temp_table)

Requête MERGE :

MERGE INTO PT2QE_RECOMMENDATIONS r
USING TEMP_CAPPING_CORRECTIONS c
ON (
    r.UNIVERS = c.UNIVERS
    AND r.TYPE_CLIENT = c.TYPE_CLIENT
    AND r.TYPE_RESTAURANT = c.TYPE_RESTAURANT
    AND NVL(r.GEO, 'NULL') = NVL(c.GEO, 'NULL')
)
WHEN MATCHED THEN UPDATE SET
    -- Recalcul RECO1_APRES_CAPPING_SENSIBILITE
    r.RECO1_APRES_CAPPING_SENSIBILITE = CASE
        WHEN r.PRICE_SENSITIVITY = 'HIGH' AND c.CAPPING_HIGH IS NOT NULL THEN
            LEAST(r.RECO1_BASE, r.PRIX_TARIF_ACTUEL * (1 + c.CAPPING_HIGH))
        -- ... (idem pour MEDIUM et LOW)
    END,

    -- Recalcul RECO1_AVEC_CAPPING (cascade avec basiques)
    r.RECO1_AVEC_CAPPING = CASE
        WHEN r.LC_ATTRIBUT = 'Basiques' THEN
            LEAST([nouvelle RECO1_APRES_CAPPING_SENSIBILITE], r.PRIX_TARIF_ACTUEL * 1.50)
        ELSE [nouvelle RECO1_APRES_CAPPING_SENSIBILITE]
    END,

    -- Recalcul PRIX_RECOMMANDE (selon DECISION_PATH)
    r.PRIX_RECOMMANDE = CASE
        WHEN r.DECISION_PATH = 'PAS_BAISSE_GEL_PRIX' 
        THEN r.PRIX_TARIF_ACTUEL

        WHEN r.DECISION_PATH = 'PL1_CONSERVATION_PREMIUM'
        THEN LEAST(GREATEST(r.PRIX_TARIF_ACTUEL, r.NEW_BORNE_PL2_PL3), r.NEW_PRB)

        WHEN r.DECISION_PATH = 'OPTIMISATION_STANDARD'
        THEN LEAST(GREATEST([nouvelle RECO1_AVEC_CAPPING], r.RECO2), r.NEW_PRB)
    END,

    -- Recalcul PCT_HAUSSE_FINALE
    r.PCT_HAUSSE_FINALE = (r.PRIX_RECOMMANDE - r.PRIX_TARIF_ACTUEL) / r.PRIX_TARIF_ACTUEL,

    -- Mise à jour flags de capping
    r.CAPPING_APPLIED = [recalcul complet],
    r.RECO_SELECTIONNEE = [recalcul],

    r.CALCULATION_DATE = SYSDATE

⚠️ RECALCUL COMPLET : L'application des corrections recalcule intégralement les recommandations pour les cubes concernés, en suivant exactement la même logique que le calcul initial.

  1. Nouvelle analyse

Consulter les nouveaux fichiers dans outputs/corrections_YYYYMMDD_HHMMSS/ : - Comparer avec la version précédente - Itérer si nécessaire (retour à l'étape 3)

Limitation : Nombre d'itérations illimité, mais chaque correction génère un nouveau dossier de sortie.

Phase 5 : Génération des analyses et exports

Exports générés automatiquement

Fichier source : analyze_recommendations.py

PT2QE génère 6 exports analytiques dans le dossier de sortie :

1. recommendations_detail.csv

Contenu : Export ligne à ligne de TOUTES les recommandations.

Colonnes principales :

ID_CLN;LC_CLN;ID_ART;LC_ART;ID_CND;DT_DEB_CONDITION;DT_FIN_CONDITION;
CATEGORIE_N1;...;CATEGORIE_N6;MARQUE;LC_ATTRIBUT;
TYPE_CLIENT;TYPE_RESTAURANT;GEO;UNIVERS;MATCH_TYPE;
MT_CAB_4Q;QT_UF_4Q;QT_KG_4Q;...;
PRIX_TARIF_ACTUEL;POSITION_TARIF_ACTUEL_DANS_ANCIENNES_BORNES;
PALIER_TARIF_ACTUEL_VS_NOUVELLES_BORNES;
RECO1_BASE;RECO1_APRES_CAPPING_SENSIBILITE;RECO1_AVEC_CAPPING;RECO2;
DECISION_PATH;RECO_TYPE;RECO_SELECTIONNEE;CAPPING_APPLIED;
PRIX_RECOMMANDE;PCT_HAUSSE_FINALE;POSITION_NOUVEAU_PRIX_DANS_NOUVELLES_BORNES

Tri : PCT_HAUSSE_FINALE DESC (hausses les plus fortes en premier)

Utilité : - Analyse détaillée ligne à ligne - Traçabilité complète de la cascade de calcul - Base pour analyses pivots - Investigation des cas limites

Format : - Prix : 3 décimales - Hausses : % avec 2 décimales - Séparateur : ; - Décimale : , - Encodage : CP1252

2. statistics_by_dimension.csv

Contenu : Agrégations par dimension clé (TYPE_CLIENT, TYPE_RESTAURANT, UNIVERS).

Requête SQL :

-- Par TYPE_CLIENT
SELECT
    'TYPE_CLIENT' as DIMENSION,
    TYPE_CLIENT as VALEUR,
    COUNT(*) as NB_OFFRES,
    COUNT(DISTINCT ID_CLN) as NB_CLIENTS,
    COUNT(DISTINCT ID_ART) as NB_ARTICLES,
    AVG(PRIX_TARIF_ACTUEL) as PRIX_MOY_ACTUEL,
    AVG(PRIX_RECOMMANDE) as PRIX_MOY_RECOMMANDE,
    AVG(PCT_HAUSSE_FINALE) * 100 as PCT_HAUSSE_MOY,
    MIN(PCT_HAUSSE_FINALE) * 100 as PCT_HAUSSE_MIN,
    MAX(PCT_HAUSSE_FINALE) * 100 as PCT_HAUSSE_MAX,
    STDDEV(PCT_HAUSSE_FINALE) * 100 as PCT_HAUSSE_STDDEV
FROM PT2QE_RECOMMENDATIONS
GROUP BY TYPE_CLIENT

-- Idem pour TYPE_RESTAURANT et UNIVERS

Utilité : - Comparaison inter-segments - Identification des segments à fort impact - Détection d'anomalies (ex: hausse moyenne aberrante)

3. impact_analysis.csv

Contenu : Analyse d'impact CA et distribution des hausses par TYPE_CLIENT × UNIVERS.

Requête SQL :

WITH IMPACT_CALC AS (
    SELECT
        TYPE_CLIENT,
        UNIVERS,
        COUNT(*) as NB_OFFRES,
        SUM(PRIX_TARIF_ACTUEL) as CA_ACTUEL,
        SUM(PRIX_RECOMMANDE) as CA_FUTUR,
        AVG(PCT_HAUSSE_FINALE) as HAUSSE_MOY,

        -- Distribution par tranche
        SUM(CASE WHEN PCT_HAUSSE_FINALE = 0 THEN 1 ELSE 0 END) as NB_SANS_HAUSSE,
        SUM(CASE WHEN PCT_HAUSSE_FINALE > 0 AND PCT_HAUSSE_FINALE <= 0.02 THEN 1 ELSE 0 END) as NB_0_2PCT,
        SUM(CASE WHEN PCT_HAUSSE_FINALE > 0.02 AND PCT_HAUSSE_FINALE <= 0.05 THEN 1 ELSE 0 END) as NB_2_5PCT,
        -- ... (autres tranches)

    FROM PT2QE_RECOMMENDATIONS
    GROUP BY TYPE_CLIENT, UNIVERS
)
SELECT
    TYPE_CLIENT,
    UNIVERS,
    NB_OFFRES,
    CA_ACTUEL,
    CA_FUTUR,
    (CA_FUTUR - CA_ACTUEL) as IMPACT_EUROS,
    ((CA_FUTUR - CA_ACTUEL) / CA_ACTUEL * 100) as IMPACT_PCT,
    HAUSSE_MOY * 100 as HAUSSE_MOY_PCT,

    -- Comptages
    NB_SANS_HAUSSE,
    NB_0_2PCT,
    NB_2_5PCT,
    -- ...

    -- Pourcentages
    (NB_SANS_HAUSSE * 100.0 / NB_OFFRES) as PCT_SANS_HAUSSE,
    (NB_0_2PCT * 100.0 / NB_OFFRES) as PCT_0_2,
    -- ...

FROM IMPACT_CALC

Colonnes : - CA_ACTUEL : Somme des prix actuels (approximation CA) - CA_FUTUR : Somme des prix recommandés - IMPACT_EUROS : Gain CA en € - IMPACT_PCT : Gain CA en % - NB_x_yPCT : Nombre d'offres dans chaque tranche de hausse - PCT_x_y : % d'offres dans chaque tranche

Utilité : - Estimation de l'impact CA potentiel - Identification des segments prioritaires - Validation de la cohérence des hausses

4. price_increase_distribution.csv

Contenu : Histogramme global des hausses.

Tranches définies :

TRANCHE_HAUSSE = CASE
    WHEN PCT_HAUSSE_FINALE = 0 THEN '00. Pas de hausse'
    WHEN PCT_HAUSSE_FINALE <= 0.02 THEN '01. 0-2%'
    WHEN PCT_HAUSSE_FINALE <= 0.05 THEN '02. 2-5%'
    WHEN PCT_HAUSSE_FINALE <= 0.07 THEN '03. 5-7%'
    WHEN PCT_HAUSSE_FINALE <= 0.10 THEN '04. 7-10%'
    WHEN PCT_HAUSSE_FINALE <= 0.12 THEN '05. 10-12%'
    WHEN PCT_HAUSSE_FINALE <= 0.15 THEN '06. 12-15%'
    WHEN PCT_HAUSSE_FINALE <= 0.17 THEN '07. 15-17%'
    WHEN PCT_HAUSSE_FINALE <= 0.20 THEN '08. 17-20%'
    ELSE '09. Plus de 20%'
END

Colonnes calculées :

SELECT
    TRANCHE_HAUSSE,
    COUNT(*) as NB_OFFRES,
    COUNT(DISTINCT ID_CLN) as NB_CLIENTS_UNIQUES,
    COUNT(DISTINCT ID_ART) as NB_ARTICLES_UNIQUES,
    AVG(PRIX_TARIF_ACTUEL) as PRIX_MOY_ACTUEL,
    AVG(PRIX_RECOMMANDE) as PRIX_MOY_RECOMMANDE,
    MIN(PCT_HAUSSE_FINALE) * 100 as HAUSSE_MIN_PCT,
    MAX(PCT_HAUSSE_FINALE) * 100 as HAUSSE_MAX_PCT,
    AVG(PCT_HAUSSE_FINALE) * 100 as HAUSSE_MOY_PCT,

    -- Pourcentages calculés en post-traitement
    (NB_OFFRES / SUM(NB_OFFRES) OVER() * 100) as PCT_OFFRES,
    SUM(NB_OFFRES) OVER(ORDER BY TRANCHE_HAUSSE) / SUM(NB_OFFRES) OVER() * 100 as PCT_CUMULE
FROM [...]
GROUP BY TRANCHE_HAUSSE
ORDER BY TRANCHE_HAUSSE

Utilité : - Vision synthétique de l'agressivité globale - Identification rapide des outliers (> 20%) - Validation des paramètres de capping

Lecture rapide :

Tranche NB_OFFRES PCT_OFFRES PCT_CUMULE Interprétation
00. Pas de hausse 500 5,0% 5,0% 5% de gel (CHEMIN 1 + égalité)
01. 0-2% 2000 20,0% 25,0% Hausses très faibles
02. 2-5% 4000 40,0% 65,0% Hausses modérées (majorité)
03. 5-7% 1500 15,0% 80,0% Hausses moyennes
04. 7-10% 1000 10,0% 90,0% 90% des offres ≤ 10%
... ... ... ... ...

5. decision_path_analysis.csv

Contenu : Statistiques par chemin de décision et type de recommandation.

Requête SQL :

SELECT
    -- Dimensions
    DECISION_PATH,
    RECO_SELECTIONNEE,

    -- Volumétrie
    COUNT(*) as NB_OFFRES,
    COUNT(DISTINCT ID_CLN) as NB_CLIENTS,
    COUNT(DISTINCT ID_ART) as NB_ARTICLES,

    -- Hausses
    AVG(PCT_HAUSSE_FINALE) * 100 as HAUSSE_MOY_PCT,
    MEDIAN(PCT_HAUSSE_FINALE) * 100 as HAUSSE_MEDIANE_PCT,

    -- Distribution des cappings
    SUM(CASE WHEN CAPPING_APPLIED = 'GEL_PAS' THEN 1 ELSE 0 END) as NB_CAP_GEL,
    SUM(CASE WHEN CAPPING_APPLIED = 'PRB_FINAL' THEN 1 ELSE 0 END) as NB_CAP_PRB,
    SUM(CASE WHEN CAPPING_APPLIED = 'PLANCHER_PL2_PL3' THEN 1 ELSE 0 END) as NB_CAP_PLANCHER,
    SUM(CASE WHEN CAPPING_APPLIED = 'BASIQUES_50PCT' THEN 1 ELSE 0 END) as NB_CAP_BASIQUES,
    SUM(CASE WHEN CAPPING_APPLIED = 'SENSIBILITE' THEN 1 ELSE 0 END) as NB_CAP_SENSIBILITE,
    SUM(CASE WHEN CAPPING_APPLIED = 'NONE' THEN 1 ELSE 0 END) as NB_SANS_CAPPING

FROM PT2QE_RECOMMENDATIONS
GROUP BY DECISION_PATH, RECO_SELECTIONNEE
ORDER BY DECISION_PATH, RECO_SELECTIONNEE

Utilité : - Comprendre la répartition entre les 3 chemins - Identifier quel chemin génère le plus de hausses - Analyser l'impact de chaque type de capping par chemin - Calibrer les seuils de déclenchement

Exemple d'insight :

DECISION_PATH          | RECO_SELECTIONNEE                | NB_OFFRES | HAUSSE_MOY_PCT
-----------------------|----------------------------------|-----------|---------------
PAS_BAISSE_GEL_PRIX    | GEL_PRIX                        | 500       | 0,00
PL1_CONSERVATION_PREMIUM | CONSERVATION_PREMIUM           | 1200      | 2,30
OPTIMISATION_STANDARD  | RECO1_REPOSITIONNEMENT_PALIERS  | 6000      | 6,50
OPTIMISATION_STANDARD  | RECO2_HAUSSE_PROPORTIONNELLE_PAS| 2300      | 5,20

Analyse : - 5% des offres gelées (CHEMIN 1) - 12% conservées en premium (CHEMIN 2) - 83% en optimisation standard (CHEMIN 3) - Parmi CHEMIN 3, RECO1 gagne dans 72% des cas (repositionnement plus agressif)

6. capping_distribution.csv

Contenu : Distribution détaillée des cappings appliqués.

Requête SQL :

SELECT
    CAPPING_APPLIED,
    DECISION_PATH,
    RECO_SELECTIONNEE,
    COUNT(*) as NB_OFFRES,
    AVG(PCT_HAUSSE_FINALE) * 100 as HAUSSE_MOY_PCT
FROM PT2QE_RECOMMENDATIONS
GROUP BY CAPPING_APPLIED, DECISION_PATH, RECO_SELECTIONNEE
ORDER BY CAPPING_APPLIED, DECISION_PATH, RECO_SELECTIONNEE

Utilité : - Mesurer l'impact de chaque type de capping - Identifier les cappings les plus contraignants - Calibrer les paramètres (ex: 50% pour basiques est-il adapté ?)

Exemple de lecture :

CAPPING_APPLIED  | DECISION_PATH         | RECO_SELECTIONNEE              | NB_OFFRES | HAUSSE_MOY_PCT
-----------------|-----------------------|--------------------------------|-----------|---------------
GEL_PAS          | PAS_BAISSE_GEL_PRIX   | GEL_PRIX                      | 500       | 0,00
PRB_FINAL        | OPTIMISATION_STANDARD | RECO1_REPOSITIONNEMENT_PALIERS| 800       | 8,50
PLANCHER_PL2_PL3 | PL1_CONSERVATION_PREMIUM | CONSERVATION_PREMIUM       | 300       | 3,20
BASIQUES_50PCT   | OPTIMISATION_STANDARD | RECO1_REPOSITIONNEMENT_PALIERS| 1200      | 35,00
SENSIBILITE      | OPTIMISATION_STANDARD | RECO1_REPOSITIONNEMENT_PALIERS| 2500      | 4,80
NONE             | OPTIMISATION_STANDARD | RECO2_HAUSSE_PROPORTIONNELLE_PAS| 1800    | 6,20

Analyse : - Le capping basiques (50%) s'applique à 1200 offres avec hausse moyenne de 35% → sans ce capping, les hausses auraient dépassé 50% (preuve de son utilité) - Le capping sensibilité s'applique à 2500 offres avec hausse moyenne de 4,8% → relativement peu contraignant - 800 offres plafonnées au PRB avec hausse moyenne de 8,5% → compression du corridor à surveiller

Export final des offres de prix

Génération du fichier final

Fichier source : generate_final_quotes.py

Déclenchement : - Option 3 du menu (3_generer_offres_finales.bat) - Argument : --generate-final-only

Prérequis : La table PT2QE_RECOMMENDATIONS doit exister (étapes 1 et 2 déjà effectuées).

Contenu du fichier final_price_offers.csv

Requête SQL :

SELECT
    -- Identifiants
    ID_CLN,
    LC_CLN,
    ID_ART,
    LC_ART,

    -- Dimensions
    TYPE_CLIENT,
    TYPE_RESTAURANT,
    GEO,
    UNIVERS,

    -- Infos condition
    ID_CND,
    TO_CHAR(DT_DEB_CONDITION, 'DD/MM/YYYY') as DT_DEB_CONDITION,
    TO_CHAR(DT_FIN_CONDITION, 'DD/MM/YYYY') as DT_FIN_CONDITION,

    -- Prix
    ROUND(PRIX_TARIF_ACTUEL, 3) as PRIX_TARIF_ACTUEL,
    ROUND(PRIX_RECOMMANDE, 3) as NOUVEAU_PRIX,

    -- Impact
    ROUND(PCT_HAUSSE_FINALE * 100, 2) as PCT_HAUSSE,
    ROUND(PRIX_RECOMMANDE - PRIX_TARIF_ACTUEL, 3) as HAUSSE_EUROS,

    -- Les 3 positions
    POSITION_TARIF_ACTUEL_DANS_ANCIENNES_BORNES,
    PALIER_TARIF_ACTUEL_VS_NOUVELLES_BORNES,
    POSITION_NOUVEAU_PRIX_DANS_NOUVELLES_BORNES,

    -- Contexte
    PRICE_SENSITIVITY,
    CASE
        WHEN PRIX_RECOMMANDE = RECO1_AVEC_CAPPING THEN 'REPOSITIONNEMENT'
        WHEN PRIX_RECOMMANDE = RECO2 THEN 'HAUSSE_PROPORTIONNELLE'
        ELSE 'AUTRE'
    END as TYPE_HAUSSE,

    -- Métadonnées
    TO_CHAR(CALCULATION_DATE, 'DD/MM/YYYY HH24:MI:SS') as DATE_CALCUL

FROM PT2QE_RECOMMENDATIONS
WHERE PRIX_RECOMMANDE IS NOT NULL
ORDER BY ID_CLN, ID_ART

Colonnes calculées en post-traitement :

# Statut de validation
df['VALIDATION_STATUS'] = 'VALIDE'
df.loc[df['PCT_HAUSSE'] > 20, 'VALIDATION_STATUS'] = 'A_VERIFIER'
df.loc[df['PCT_HAUSSE'] < 0, 'VALIDATION_STATUS'] = 'ANOMALIE'

Format : - Séparateur : ; - Décimale : , - Encodage : CP1252 - Prix : 3 décimales - Hausses : 2 décimales

Rapport de validation

Fichier généré : validation_report.csv

Contenu :

validation_stats = [
    {'CATEGORIE': 'GLOBAL', 'METRIQUE': 'Nombre total d\'offres', 'VALEUR': len(df)},
    {'CATEGORIE': 'GLOBAL', 'METRIQUE': 'Nombre de clients', 'VALEUR': df['ID_CLN'].nunique()},
    {'CATEGORIE': 'GLOBAL', 'METRIQUE': 'Nombre d\'articles', 'VALEUR': df['ID_ART'].nunique()},
    {'CATEGORIE': 'HAUSSES', 'METRIQUE': 'Hausse moyenne (%)', 'VALEUR': df['PCT_HAUSSE'].mean()},
    {'CATEGORIE': 'HAUSSES', 'METRIQUE': 'Hausse médiane (%)', 'VALEUR': df['PCT_HAUSSE'].median()},
    {'CATEGORIE': 'HAUSSES', 'METRIQUE': 'Hausse max (%)', 'VALEUR': df['PCT_HAUSSE'].max()},
    # ... (distribution TYPE_HAUSSE et VALIDATION_STATUS)
]

Résumé textuel

Fichier généré : export_summary.txt

Contenu :

RÉSUMÉ EXPORT FINAL PT2QE
==================================================

Date : 03/11/2025 14:30:22

VOLUMÉTRIE
--------------------
Offres exportées : 10.000
Clients uniques : 2.500
Articles uniques : 4.000

HAUSSES DE PRIX
--------------------
Hausse moyenne : 5,2%
Hausse médiane : 4,8%
Hausse min : 0,0%
Hausse max : 18,5%

VALIDATION
--------------------
VALIDE : 9.800 (98,0%)
A_VERIFIER : 180 (1,8%)
ANOMALIE : 20 (0,2%)

Optimisations et performances

Index créés automatiquement

Table des offres (PT2QE_PRICE_OFFERS) :

CREATE INDEX IX_PT2QE_PRICE_OFFERS_CLN ON PT2QE_PRICE_OFFERS(ID_CLN) COMPRESS;
CREATE INDEX IX_PT2QE_PRICE_OFFERS_ART ON PT2QE_PRICE_OFFERS(ID_ART) COMPRESS;
CREATE INDEX IX_PT2QE_PRICE_OFFERS_MAIN ON PT2QE_PRICE_OFFERS(ID_CLN, ID_ART) COMPRESS;
CREATE INDEX IX_PT2QE_PRICE_OFFERS_TC ON PT2QE_PRICE_OFFERS(ID_TC_CG, ID_TC_CIBLE, FG_HM) COMPRESS;

Utilité : - Accélération des jointures avec les corridors - Optimisation des requêtes d'analyse - Compression pour réduire l'espace disque

Parallélisation Oracle

Hint appliqué : /*+ PARALLEL(8) */

Localisation :

-- Extraction des offres
SELECT /*+ PARALLEL(8) */ DISTINCT ...

-- Enrichissement historique
SELECT /*+ PARALLEL(8) */ ...
FROM SYS_FACTURE_LIGNE f
...

Paramètre configurable : config/pt2qe_config.json

{
  "processing": {
    "parallel_degree": 8
  }
}

⚠️ AJUSTEMENT : Adapter selon les ressources serveur (CPU disponibles).

Compression des tables

Toutes les tables PT2QE utilisent COMPRESS NOLOGGING :

CREATE TABLE PT2QE_PRICE_OFFERS COMPRESS NOLOGGING AS
...

CREATE TABLE PT2QE_RECOMMENDATIONS COMPRESS NOLOGGING AS
...

Avantages : - Réduction de l'espace disque (jusqu'à 3-5×) - Amélioration des I/O (moins de données à lire) - Pas d'impact sur les performances de lecture

Inconvénient : NOLOGGING → pas de recovery possible en cas de crash (acceptable pour des tables temporaires).

Batch size

Paramètre : config/pt2qe_config.json

{
  "processing": {
    "batch_size": 100000
  }
}

Utilité : Si traitement par batch nécessaire (non utilisé actuellement, toutes les requêtes sont en bulk SQL).

Modification des calculs

Modifier les règles RECO1

Fichier : config/pt2qe_config.json

Exemple : Faire remonter les prix PL3_PL4 vers PL1 au lieu de PL2 :

{
  "recommendations": {
    "reco1_rules": [
      {
        "position": "PL3_PL4",
        "condition": "PRIX_TARIF_ACTUEL > NEW_BORNE_PL4_PL5",
        "action": "TO_PL1",
        "target": "NEW_BORNE_PL1_PL2",  // ← Changé de PL2_PL3 à PL1_PL2
        "comment": "Remonter vers PL1"
      }
    ]
  }
}

Application : Relancer PT2QE (option 1 du menu), les nouvelles règles seront automatiquement appliquées.

Modifier les cappings par défaut

Fichier : config/pt2qe_config.json

{
  "capping": {
    "default_high": 0.03,    // ← Changer de 0.05 à 0.03 (3%)
    "default_medium": 0.10,  // ← Changer de 0.15 à 0.10 (10%)
    "default_low": 0.15,     // ← Changer de 0.20 à 0.15 (15%)
    "basiques": 0.40         // ← Changer de 0.50 à 0.40 (40%)
  }
}

⚠️ ATTENTION : Ces valeurs sont des fallback utilisés uniquement si aucun capping spécifique n'est défini dans inputs/capping_type_client.csv.

Priorité : Capping CSV > Capping config > Capping hardcodé

Modifier le capping basiques

Actuellement : Hardcodé à 50% dans le SQL.

Localisation : calculate_recommandations.py, requête de création de PT2QE_RECOMMENDATIONS

-- Capping basiques à 50%
WHEN LC_ATTRIBUT = 'Basiques' THEN
    LEAST(RECO1_APRES_CAPPING_SENSIBILITE, PRIX_TARIF_ACTUEL * 1.50)

Pour modifier :

  1. Option A : Modifier directement dans le SQL (nécessite accès au code)

Remplacer 1.50 par la valeur souhaitée (ex: 1.40 pour 40%)

  1. Option B : Ajouter au fichier de configuration (évolution future)
{
  "capping": {
    "basiques": 0.40  // Capping basiques à 40%
  }
}

Puis modifier le code pour lire ce paramètre :

capping_basiques = self.app.config.get_capping_basiques()  # Retourne 0.40

# Injection dans le SQL
query = f"""
...
WHEN LC_ATTRIBUT = 'Basiques' THEN
    LEAST(RECO1_APRES_CAPPING_SENSIBILITE, PRIX_TARIF_ACTUEL * {1 + capping_basiques})
...
"""

Désactiver un chemin décisionnel

Exemple : Désactiver le CHEMIN 1 (gel des prix en baisse de PAS)

Localisation : calculate_recommandations.py, section DECISION_TREE

Modification :

-- AVANT
DECISION_PATH = CASE
    WHEN rwc.IS_PAS_BAISSE = 1 THEN 'PAS_BAISSE_GEL_PRIX'
    WHEN rwc.IS_PL1_ANCIENNES = 1 THEN 'PL1_CONSERVATION_PREMIUM'
    ELSE 'OPTIMISATION_STANDARD'
END

-- APRÈS (désactivation CHEMIN 1)
DECISION_PATH = CASE
    -- Commenté : WHEN rwc.IS_PAS_BAISSE = 1 THEN 'PAS_BAISSE_GEL_PRIX'
    WHEN rwc.IS_PL1_ANCIENNES = 1 THEN 'PL1_CONSERVATION_PREMIUM'
    ELSE 'OPTIMISATION_STANDARD'
END

Conséquence : Les offres avec NEW_PAS < PAS_ACTIF seront traitées en CHEMIN 2 ou 3 selon leur position.

Ajouter un 4ème chemin décisionnel

Exemple : Ajouter un chemin pour les articles à forte marge (GM4 > 40%)

Étape 1 : Modifier la CTE ENRICHED_DATA pour ajouter le flag :

WITH ENRICHED_DATA AS (
    SELECT 
        e.*,
        -- Flags existants
        CASE WHEN e.NEW_PAS < e.PAS_ACTIF THEN 1 ELSE 0 END as IS_PAS_BAISSE,
        CASE WHEN [...] THEN 1 ELSE 0 END as IS_PL1_ANCIENNES,

        -- Nouveau flag
        CASE WHEN e.TAUX_GM4 > 0.40 THEN 1 ELSE 0 END as IS_FORTE_MARGE,

        -- Cappings
        c.CAPPING_HIGH, c.CAPPING_MEDIUM, c.CAPPING_LOW

    FROM enriched_table e
    LEFT JOIN capping_table c ON [...]
)

Étape 2 : Modifier l'arbre de décision :

DECISION_PATH = CASE
    WHEN rwc.IS_PAS_BAISSE = 1 THEN 'PAS_BAISSE_GEL_PRIX'
    WHEN rwc.IS_PL1_ANCIENNES = 1 THEN 'PL1_CONSERVATION_PREMIUM'
    WHEN rwc.IS_FORTE_MARGE = 1 THEN 'FORTE_MARGE_CONSERVATEUR'  -- Nouveau chemin
    ELSE 'OPTIMISATION_STANDARD'
END

Étape 3 : Ajouter la logique de calcul pour le nouveau chemin :

PRIX_AVANT_CAPPING_PRB = CASE
    WHEN rwc.IS_PAS_BAISSE = 1 THEN rwc.PRIX_TARIF_ACTUEL
    WHEN rwc.IS_PL1_ANCIENNES = 1 THEN GREATEST(rwc.PRIX_TARIF_ACTUEL, rwc.NEW_BORNE_PL2_PL3)

    -- Nouveau chemin : hausse modérée +2%
    WHEN rwc.IS_FORTE_MARGE = 1 THEN rwc.PRIX_TARIF_ACTUEL * 1.02

    ELSE GREATEST(rwc.RECO1_AVEC_CAPPING, rwc.RECO2)
END

Étape 4 : Mettre à jour RECO_SELECTIONNEE :

RECO_SELECTIONNEE = CASE
    WHEN rwc.IS_PAS_BAISSE = 1 THEN 'GEL_PRIX'
    WHEN rwc.IS_PL1_ANCIENNES = 1 THEN 'CONSERVATION_PREMIUM'
    WHEN rwc.IS_FORTE_MARGE = 1 THEN 'CONSERVATION_FORTE_MARGE'  -- Nouveau
    WHEN rwc.RECO1_AVEC_CAPPING >= rwc.RECO2 THEN 'RECO1_REPOSITIONNEMENT_PALIERS'
    ELSE 'RECO2_HAUSSE_PROPORTIONNELLE_PAS'
END

⚠️ TEST : Valider sur un sous-ensemble de données avant application en production.

Cas particuliers et edge cases

Offres sans corridor (NO_MATCH)

Cause : Article sans corridor PT1CE correspondant (ni MASTER ni NATIONAL).

Action : Exclusion automatique du calcul des recommandations.

Requête :

WHERE HAS_CORRIDOR = 1  -- Filtre dans le calcul des recommandations

Statistiques : Consulter les logs d'exécution :

→ Résultats du matching :
  - MASTER : 8.500 offres, 2.000 clients, 3.500 articles
  - NATIONAL : 1.200 offres, 400 clients, 600 articles
  - NO_MATCH : 300 offres, 80 clients, 120 articles

→ Total : 9.700/10.000 offres avec corridor (97,0%)

Solution : Analyser les articles NO_MATCH et créer les corridors manquants dans PT1CE.

Prix actuel sous le PAS

Détection :

POSITION_TARIF_ACTUEL_DANS_NOUVELLES_BORNES = 'BELOW_PAS'

Action RECO1 :

RECO1_BASE = NEW_PAS  -- Remontée au PAS minimum

Exemple :

Prix actuel : 9,00 €
NEW_PAS     : 10,00 €

RECO1_BASE = 10,00 €
→ Hausse de 11,1%

⚠️ ATTENTION : Ce cas peut générer des hausses importantes si le prix était très en dessous du PAS.

Prix actuel au-dessus du PRB

Détection :

POSITION_TARIF_ACTUEL_DANS_NOUVELLES_BORNES = 'ABOVE_PRB'

Action : Prix reste inchangé (RECO1_BASE = PRIX_TARIF_ACTUEL)

Puis : Capping PRB final s'applique → Prix recommandé = NEW_PRB

Exemple :

Prix actuel : 32,00 €
NEW_PRB     : 30,00 €

RECO1_BASE = 32,00 € (pas de changement)
PRIX_RECOMMANDE = MIN(32,00 ; 30,00) = 30,00 €

→ Baisse de 6,25% (rare)

⚠️ ANOMALIE : Ce cas ne devrait pas arriver si PT1CE a été correctement calibré.

Absence de hausse du PAS

Condition :

NEW_PAS = PAS_ACTIF   PCT_HAUSSE_PAS = 0

Effet sur RECO2 :

RECO2 = PRIX_TARIF_ACTUEL * (1 + 0) = PRIX_TARIF_ACTUEL

Conséquence : RECO2 n'apporte aucune hausse → RECO1 gagnera toujours (sauf si également = Prix actuel).

Baisse du PAS dans CHEMIN 3

Théoriquement : Les offres avec NEW_PAS < PAS_ACTIF sont traitées en CHEMIN 1 (gel).

Si malgré tout traitées en CHEMIN 3 (bug de logique) :

RECO2 = PRIX_TARIF_ACTUEL * (1 + PCT_HAUSSE_PAS_NEGATIF)
 RECO2 < PRIX_TARIF_ACTUEL

Protection : La sélection MAX(RECO1, RECO2) garantit qu'une baisse ne sera pas appliquée (sauf si RECO1 également en baisse).

Sensibilité prix NULL

Cas : Corridor NATIONAL sans PRICE_SENSITIVITY renseigné.

Action :

RECO1_APRES_CAPPING_SENSIBILITE = RECO1_BASE  -- Pas de capping appliqué

⚠️ RISQUE : Hausses potentiellement très fortes sans limite.

Solution : S'assurer que tous les corridors (y compris NATIONAL) ont un PRICE_SENSITIVITY défini dans PT1CE.

Capping type client manquant

Cas : TYPE_CLIENT non présent dans inputs/capping_type_client.csv.

Action : Utilisation des valeurs par défaut hardcodées dans le code :

# Dans capping_manager.py
capping_rate = 0.0
if sensitivity == 'HIGH':
    capping_rate = cappings.get('high', 0.05)  # ← Fallback 5%
elif sensitivity == 'MEDIUM':
    capping_rate = cappings.get('medium', 0.15)  # ← Fallback 15%
elif sensitivity == 'LOW':
    capping_rate = cappings.get('low', 0.20)  # ← Fallback 20%

⚠️ RECOMMANDATION : Toujours définir explicitement tous les TYPE_CLIENT dans le fichier CSV pour éviter les surprises.