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 :
- Extraction des offres : Récupération des prix client × article depuis SYS_TARIF_SIMULATION
- Enrichissement historique : Calcul des métriques transactionnelles sur 4 trimestres fiscaux
- Matching avec corridors PT1CE : Jointure avec les corridors optimaux (MASTER ou NATIONAL)
- Calcul des recommandations : Application de l'arbre de décision à 3 chemins avec cascade de capping
- 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_ZOOM1PT1CE_OPTIMAL_ZOOM2PT1CE_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 :
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 :
- Éditer le fichier
config/pt2qe_config.json - Modifier les règles dans la section
recommendations.reco1_rules - 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)
où 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 :
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 :
- 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
- Identification des cubes à ajuster
Exemple : Trop de hausses > 10% pour le cube ZOOM1 × RCI PI GI × RESTAURATION TRADITIONNELLE × ILE DE FRANCE
- Copie du fichier de cappings
cd outputs\run_20251103_143022
copy capping_cubes_generated.csv ..\..\corrections\capping_cubes_corrections.csv
- 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)
- 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.
- 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
⚠️ 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
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 :
- 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%)
- Option B : Ajouter au fichier de configuration (évolution future)
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 :
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 :
Action RECO1 :
Exemple :
⚠️ 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 :
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 :
Effet sur RECO2 :
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) :
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 :
⚠️ 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.