Aller au contenu

Architecture technique - PT2QE

Vue d'ensemble du système

PT2QE (Pricing Tier 2 Quote Engine) est un système de calcul de recommandations de prix personnalisées à la maille client × article. Il s'appuie sur les corridors optimaux générés par PT1CE (Tier 1) et applique une logique décisionnelle à 3 chemins avec cascade de cappings.

Périmètre strict : ZOOM1 uniquement (pas de ZOOM2 ni ZOOM3).

Prérequis obligatoires : - PT1CE doit avoir été exécuté (Option 1 + Option 2) pour créer les tables PT1CE_OPTIMAL_* - Tables de mapping PT0CE_TYPE_CLIENT_MAPPING et PT0CE_TYPE_RESTAURANT_MAPPING doivent exister - Fichier inputs/capping_type_client.csv doit être présent


Structure modulaire complète

1. Orchestrateur principal

Fichier : pt2qe_main.py

Responsabilités : - Parse les arguments ligne de commande - Initialise la configuration depuis config/pt2qe_config.json - Détermine les périodes d'analyse via PeriodManager - Orchestre l'exécution des 4 phases - Gère les modes d'exécution (nouveau calcul, corrections, export seul) - Crée les dossiers de sortie horodatés

Modes d'exécution :

  1. Mode complet (défaut) : Toutes les phases

    python pt2qe_main.py TARIFAIRE --capping-file inputs/capping_type_client.csv
    

  2. Mode corrections : Saute l'extraction, applique corrections

    python pt2qe_main.py TARIFAIRE --skip-extraction --apply-corrections --corrections-folder corrections/
    

  3. Mode export final : Génère uniquement l'export final

    python pt2qe_main.py TARIFAIRE --generate-final-only
    

Arguments disponibles : - database : Base Oracle (obligatoire) - --capping-file : Fichier CSV cappings (défaut : inputs/capping_type_client.csv) - --corrections-folder : Dossier avec corrections (défaut : corrections/) - --output-folder : Dossier de sortie (défaut : outputs/) - --apply-corrections : Applique les corrections de capping - --skip-extraction : Réutilise tables existantes - --generate-final-only : Export final uniquement - --dry-run : Affiche le plan d'exécution sans l'exécuter - -q / -qq : Niveaux de log (quiet / very quiet) - -vv : Mode verbeux (debug)

Structure de sortie :

outputs/
├── run_YYYYMMDD_HHMMSS/          # Nouveau calcul
├── corrections_YYYYMMDD_HHMMSS/   # Avec corrections
└── final_YYYYMMDD_HHMMSS/         # Export final

2. Module d'extraction

Fichier : extract_price_offers.py
Classe : PriceOffersExtractor

Responsabilités : - Extraction offres ZOOM1 depuis SYS_TARIF_SIMULATION - Enrichissement avec historique transactionnel 4 derniers trimestres - Récupération du dernier FG_HM connu (mercuriale/hors mercuriale) - Détermination de l'UNIVERS selon les Business Rules - Jointure avec mappings PT0CE_TYPE_CLIENT_MAPPING et PT0CE_TYPE_RESTAURANT_MAPPING - Calcul des métriques de performance : CA, volumes (UF, KG), répartition prix fermes/indexés

Scope temporel : 4 derniers trimestres fiscaux COMPLETS - Déterminé automatiquement par PeriodManager depuis SYS_MD_CALENDRIER_SYSCO - Un trimestre est complet si sa dernière semaine est passée - Période affichée au démarrage du traitement

Méthode principale :

def extract_current_offers_with_history(self, output_table: str, start_date: date, end_date: date)

Requête SQL produite : 1. PRP_LABELS : Exclusion des prix promo 2. CURRENT_OFFERS : Offres actuelles avec filtres 3. LAST_FG_HM : Dernier FG_HM par client × article 4. HISTORICAL_PERFORMANCE : Métriques 4Q (CA, volumes, fermes, indexés) 5. LAST_QT_UF : Dernière quantité connue 6. OFFERS_WITH_UNIVERS : Calcul UNIVERS selon Business Rules 7. Jointure finale : Enrichissement TYPE_CLIENT, TYPE_RESTAURANT

Filtres appliqués : - UNIVERS = 'ZOOM1' (filtre strict) - cnd.FG_CND_VLD = 'X' - cnd.DT_FIN > SYSDATE (conditions valides) - cnd.ID_TYP_CND IN ('ZFAP', 'ZFSP', 'ZIAP', 'ZISP') - cnd.ID_SEQ IN ('A305', 'A565') - ID_TC_CG <> 'AUTRES' - ID_GMM <> 'PSN' - LC_HIC_SYSCO_N2 NOT IN ('Divers', 'Freezer')

Table créée : PT2QE_PRICE_OFFERS
Compression : COMPRESS NOLOGGING
Index créés : - IX_PT2QE_PRICE_OFFERS_CLN sur ID_CLN - IX_PT2QE_PRICE_OFFERS_ART sur ID_ART - IX_PT2QE_PRICE_OFFERS_MAIN sur (ID_CLN, ID_ART) - IX_PT2QE_PRICE_OFFERS_TC sur (ID_TC_CG, ID_TC_CIBLE, FG_HM)

Statistiques affichées : - Nombre d'offres extraites - Nombre de clients/articles uniques - Taux d'enrichissement TYPE_CLIENT, TYPE_RESTAURANT, FG_HM

3. Module de calcul des recommandations

Fichier : calculate_recommandations.py
Classe : RecommendationCalculator

Composants utilisés : - CorridorMatcher : Matching offres × corridors PT1CE - RecommendationCalculator (utils) : Calculs RECO1/RECO2 - CappingManager : Gestion cappings par cube

Méthode principale :

def calculate_all_recommendations(self, offers_table: str, output_table: str, capping_file: Path = None)

Étapes d'exécution :

  1. Chargement cappings type client
  2. Lecture du fichier CSV capping_file
  3. Stockage des cappings HIGH/MEDIUM/LOW par TYPE_CLIENT

  4. Jointure offres × corridors PT1CE

  5. Appel de corridor_matcher.enrich_offers_with_corridors()
  6. Stratégie de matching en cascade :

    • Tentative 1 : Match MASTER (4 dimensions : ID_ART, TYPE_CLIENT, TYPE_RESTAURANT, GEO)
    • Tentative 2 : Fallback NATIONAL (ID_ART seul)
    • Marquage MATCH_TYPE : 'MASTER', 'NATIONAL', 'NO_MATCH'
  7. Génération cappings par cube

  8. Création table PT2QE_CAPPING_CUBES
  9. Génération des combinaisons UNIVERS × TYPE_CLIENT × TYPE_RESTAURANT × GEO
  10. Inclusion des cubes NATIONAL pour fallback
  11. Application des cappings depuis le fichier CSV

  12. Calcul des recommandations

  13. Requête SQL unique avec CTEs successives :
    • ENRICHED_DATA : Récupération données + cappings
    • RECO_CALC : Calcul RECO1_BASE (paramétrable) et RECO2
    • RECO_WITH_CAPPING_SENSITIVITY : Capping sensibilité sur RECO1
    • RECO_WITH_CAPPING_BASIQUES : Capping basiques 50% en cascade
    • DECISION_TREE : Arbre de décision à 3 chemins
    • FINAL_PRICES : Application capping PRB final
    • FINAL_PRICES_WITH_CAPPING : Détermination capping appliqué
  14. Table créée : PT2QE_RECOMMENDATIONS

  15. Export cappings générés

  16. Export de PT2QE_CAPPING_CUBES en CSV
  17. Fichier : capping_cubes_generated.csv

Arbre de décision (3 chemins) :

CHEMIN 1 : PAS_BAISSE_GEL_PRIX - Condition : NEW_PAS < PAS_ACTIF - Action : PRIX_RECOMMANDE = PRIX_TARIF_ACTUEL - Rationale : Ne pas répercuter les baisses de coût d'achat

CHEMIN 2 : PL1_CONSERVATION_PREMIUM - Condition : Prix actuel dans PL1 anciennes bornes (PRB_ACTIF ≥ Prix > BORNE_PL1_PL2) - Action : PRIX_RECOMMANDE = MAX(PRIX_TARIF_ACTUEL, NEW_BORNE_PL2_PL3) puis capping PRB - Rationale : Maintenir clients premium en position haute avec plancher sécurité

CHEMIN 3 : OPTIMISATION_STANDARD - Condition : Tous les autres cas - Action : PRIX_RECOMMANDE = MAX(RECO1_avec_cappings, RECO2) puis capping PRB - Cascade de cappings : 1. Capping sensibilité (HIGH/MEDIUM/LOW) 2. Capping basiques (50% max) 3. Capping PRB final

Règles RECO1 (paramétrables via config) :

Définies dans config/pt2qe_config.json :

{
  "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"
      }
      // ... autres règles
    ]
  }
}

Comment modifier les règles RECO1 : 1. Éditer config/pt2qe_config.json 2. Modifier le tableau reco1_rules 3. Pour chaque règle, spécifier : - position : Nom de la position - condition : Condition SQL (avec préfixe table "ed.") - action : Description de l'action - target : Colonne SQL cible - comment : Explication 4. La méthode build_reco1_sql_case() génère le CASE SQL dynamiquement

RECO2 (fixe) :

RECO2 = PRIX_TARIF_ACTUEL × (1 + (NEW_PAS - PAS_ACTIF) / PAS_ACTIF)

Cascade de cappings (CHEMIN 3) :

  1. Capping sensibilité (étape 1) :

    RECO1_APRES_CAPPING_SENSIBILITE = MIN(RECO1_BASE, PRIX_TARIF_ACTUEL × (1 + CAPPING_SENSITIVITY))
    
    CAPPING_SENSITIVITY = HIGH (2.5%), MEDIUM (5%), LOW (7.5%)

  2. Capping basiques (étape 2, en cascade) :

    SI LC_ATTRIBUT = 'Basiques':
        RECO1_AVEC_CAPPING = MIN(RECO1_APRES_CAPPING_SENSIBILITE, PRIX_TARIF_ACTUEL × 1.50)
    SINON:
        RECO1_AVEC_CAPPING = RECO1_APRES_CAPPING_SENSIBILITE
    

  3. Capping PRB final (tous chemins) :

    PRIX_RECOMMANDE = MIN(PRIX_AVANT_CAPPING_PRB, NEW_PRB)
    

Champs calculés : - DECISION_PATH : Chemin emprunté - RECO_TYPE : Type reco gagnante (description courte) - RECO_SELECTIONNEE : Nom explicite de la reco retenue - CAPPING_APPLIED : Type de capping le plus contraignant - PRIX_RECOMMANDE : Prix final recommandé - PCT_HAUSSE_FINALE : % de hausse - POSITION_TARIF_ACTUEL_DANS_ANCIENNES_BORNES : Position 1 - PALIER_TARIF_ACTUEL_VS_NOUVELLES_BORNES : Position 2 - POSITION_NOUVEAU_PRIX_DANS_NOUVELLES_BORNES : Position 3

Table créée : PT2QE_RECOMMENDATIONS
Compression : COMPRESS NOLOGGING

4. Module d'application des corrections

Fichier : apply_capping_rules.py
Classe : CappingApplicator

Responsabilités : - Lecture du fichier corrections/capping_cubes_corrections.csv - Application des nouveaux cappings sur les recommandations existantes - Recalcul complet selon l'arbre de décision avec nouveaux paramètres

Méthode principale :

def apply_capping_corrections(self, recommendations_table: str, corrections_folder: Path)

Processus d'exécution :

  1. Chargement corrections
  2. Lecture CSV capping_cubes_corrections.csv
  3. Validation colonnes : UNIVERS, TYPE_CLIENT, TYPE_RESTAURANT, GEO
  4. Validation présence cappings (CAPPING_HIGH, CAPPING_MEDIUM, CAPPING_LOW)

  5. Upload corrections

  6. Création table temporaire TEMP_CAPPING_CORRECTIONS
  7. Insertion des corrections

  8. Mise à jour recommandations

  9. Requête MERGE INTO sur PT2QE_RECOMMENDATIONS
  10. Jointure avec corrections par cube
  11. Recalcul de tous les champs :

    • RECO1_APRES_CAPPING_SENSIBILITE
    • RECO1_AVEC_CAPPING
    • PRIX_RECOMMANDE (selon DECISION_PATH existant)
    • PCT_HAUSSE_FINALE
    • POSITION_NOUVEAU_PRIX_DANS_NOUVELLES_BORNES
    • CAPPING_APPLIED
    • RECO_TYPE
    • RECO_SELECTIONNEE
    • CALCULATION_DATE
  12. Nettoyage

  13. Suppression table temporaire

Comment appliquer des corrections :

  1. Exécuter d'abord l'Option 1 (calcul initial)
  2. Analyser capping_cubes_generated.csv dans le dossier output
  3. Copier capping_cubes_generated.csv dans corrections/
  4. Renommer en capping_cubes_corrections.csv
  5. Éditer les valeurs CAPPING_HIGH, CAPPING_MEDIUM, CAPPING_LOW
  6. Exécuter l'Option 2 (ajustement cappings)
  7. Comparer les résultats avant/après

Format fichier corrections :

UNIVERS;TYPE_CLIENT;TYPE_RESTAURANT;GEO;CAPPING_HIGH;CAPPING_MEDIUM;CAPPING_LOW
ZOOM1;RCI PI GI;TRADITIONNEL;NORD;0,030;0,060;0,090
ZOOM1;RSI HM;RESTAURATION_RAPIDE;SUD;0,020;0,040;0,060

5. Module d'analyse

Fichier : analyze_recommendations.py
Classe : RecommendationAnalyzer

Responsabilités : - Génération de 6 exports analytiques - Statistiques par dimension - Distribution des hausses - Analyse des chemins de décision - Impact des cappings

Méthode principale :

def analyze_and_export(self, recommendations_table: str, output_folder: Path)

6 exports générés :

1. recommendations_detail.csv - Export ligne à ligne de TOUTES les recommandations - Contient cascade complète : RECO1_BASE → RECO1_APRES_CAPPING_SENSIBILITE → RECO1_AVEC_CAPPING - Traçabilité : DECISION_PATH, RECO_SELECTIONNEE, CAPPING_APPLIED - Tri : PCT_HAUSSE_FINALE DESC

2. statistics_by_dimension.csv - Agrégations par TYPE_CLIENT, TYPE_RESTAURANT, UNIVERS - Métriques : nb offres, clients, articles, prix moyens, hausses (min/max/moy/stddev)

3. impact_analysis.csv - Impact CA par TYPE_CLIENT × UNIVERS - Distribution hausses par tranches (0%, 0-2%, 2-5%, etc.) - Impact € et % sur le CA

4. price_increase_distribution.csv - Histogramme hausses par tranches (0-2%, 2-5%, 5-7%, etc.) - % offres et % cumulé par tranche - Statistiques par tranche (min/max/moy)

5. decision_path_analysis.csv - Statistiques par DECISION_PATH × RECO_SELECTIONNEE - Détail cappings appliqués par chemin - Volumétrie et hausses moyennes

6. capping_distribution.csv - Croisement CAPPING_APPLIED × DECISION_PATH × RECO_SELECTIONNEE - Vision détaillée impact de chaque type de capping

Scope des analyses : - ZOOM1 uniquement - Période : 4 derniers trimestres fiscaux complets (affichée dans summary.txt)

6. Module de génération finale

Fichier : generate_final_quotes.py
Classe : FinalQuoteGenerator

Responsabilités : - Génération fichier final client × article - Rapport de validation - Résumé textuel

Méthode principale :

def generate_final_quotes(self, recommendations_table: str, output_file: Path)

Fichiers générés :

1. final_price_offers.csv - Format : Client × Article avec prix recommandés - Colonnes : - Identifiants : ID_CLN, ID_ART, ID_CND - Dimensions : TYPE_CLIENT, TYPE_RESTAURANT, GEO, UNIVERS - Dates condition : DT_DEB_CONDITION, DT_FIN_CONDITION - Prix : PRIX_TARIF_ACTUEL, NOUVEAU_PRIX - Impact : PCT_HAUSSE, HAUSSE_EUROS - 3 positions : POSITION_TARIF_ACTUEL_DANS_ANCIENNES_BORNES, PALIER_TARIF_ACTUEL_VS_NOUVELLES_BORNES, POSITION_NOUVEAU_PRIX_DANS_NOUVELLES_BORNES - Contexte : PRICE_SENSITIVITY, TYPE_HAUSSE - Validation : VALIDATION_STATUS, DATE_CALCUL

2. validation_report.csv - Statistiques de validation - Catégories : GLOBAL, HAUSSES, TYPE_HAUSSE, VALIDATION

3. export_summary.txt - Résumé textuel avec volumétrie et hausses

Règles de validation : - VALIDE : Normal - A_VERIFIER : Hausse > 20% - ANOMALIE : Hausse < 0% (ne devrait pas arriver)

7. Utilitaires

Fichier : utils/corridor_matcher.py
Classe : CorridorMatcher

Responsabilités : - Matching offres × corridors PT1CE - Stratégie de fallback MASTER → NATIONAL - Calcul des positions dans corridors

Méthode principale :

def enrich_offers_with_corridors(self, offers_table: str, enriched_table: str) -> str

Stratégie de matching :

  1. Tentative MASTER (4 dimensions)

    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'
    

  2. Fallback NATIONAL (ID_ART seul)

    JOIN PT1CE_OPTIMAL_ZOOM1 c ON
        o.ID_ART = c.ID_ART
        AND c.TYPE_CLIENT = 'NATIONAL'
        AND c.TYPE_RESTAURANT = 'NATIONAL'
        AND c.CUBE_TYPE = 'NATIONAL'
    WHERE NOT EXISTS (SELECT 1 FROM MATCH_MASTER)
    

  3. NO_MATCH

  4. Offres sans corridor disponible
  5. HAS_CORRIDOR = 0

Statistiques affichées : - Nombre d'offres par MATCH_TYPE - Taux de couverture global

Fichier : utils/capping_manager.py
Classe : CappingManager

Responsabilités : - Chargement cappings depuis CSV - Génération table cappings par cube - Application corrections

Méthodes :

def load_type_client_capping(self, capping_file: Path)
def generate_cube_capping_table(self, enriched_table: str) -> str
def apply_cube_corrections(self, capping_table: str, corrections_file: Path)

Fichier : utils/period_manager.py
Classe : PeriodManager

Responsabilités : - Détermination périodes fiscales depuis SYS_MD_CALENDRIER_SYSCO - Calcul 4 derniers trimestres complets - Validation périodes

Méthodes principales :

def get_last_four_complete_fiscal_quarters(self, reference_date: date) -> Dict
def get_quarter_bounds(self, quarter_code: str) -> Dict
def get_fiscal_year_for_date(self, ref_date: date) -> str

Logique de détermination : 1. Récupère le trimestre en cours depuis FG_SEM_EN_COURS = 'X' 2. Remonte dans le temps pour trouver 4 trimestres COMPLETS 3. Un trimestre est complet si END_DATE < reference_date 4. Retourne start_date, end_date, et liste des trimestres

Fichier : utils/recommendation_calculator.py
Classe : RecommendationCalculator

Responsabilités : - Calcul RECO1 selon règles paramétrables - Calcul RECO2 (hausse proportionnelle) - Application cappings - Validation recommandations

Fichier : utils/impact_analyzer.py
Classe : ImpactAnalyzer

Responsabilités : - Analyse d'impact global - Analyse par dimension - Distribution des hausses - Top hausses

8. Configuration

Fichier : config/pt2qe_config.py
Classe : PT2QEConfig

Responsabilités : - Chargement configuration depuis JSON - Valeurs par défaut si fichier absent - Génération SQL dynamique pour RECO1

Méthodes :

def load()
def get_batch_size() -> int
def get_default_cappings() -> Dict[str, float]
def get_capping_basiques() -> float
def get_reco1_rules() -> List[Dict[str, str]]
def build_reco1_sql_case(table_alias: str = "ed") -> str

Comment modifier la configuration :

  1. Modifier les cappings par défaut :

    {
      "capping": {
        "default_high": 0.05,
        "default_medium": 0.15,
        "default_low": 0.20,
        "basiques": 0.50
      }
    }
    

  2. Modifier les règles RECO1 : Éditer le tableau reco1_rules (voir section Module de calcul)

  3. Modifier les paramètres de sortie :

    {
      "output": {
        "csv_separator": ";",
        "csv_encoding": "cp1252",
        "decimal_places": 3,
        "percentage_format": 2
      }
    }
    


Flux de données détaillé

Schéma général

SYS_TARIF_SIMULATION ──┐
SYS_FACTURE_LIGNE ─────┼──> PT2QE_PRICE_OFFERS
PT0CE_TYPE_*_MAPPING ──┘

PT2QE_PRICE_OFFERS ────┐
PT1CE_OPTIMAL_* ───────┼──> PT2QE_PRICE_OFFERS_ENRICHED
PT2QE_CAPPING_CUBES ───┘

PT2QE_PRICE_OFFERS_ENRICHED ──> PT2QE_RECOMMENDATIONS

PT2QE_RECOMMENDATIONS ──> 6 fichiers CSV + final_price_offers.csv

Phase 1 : Extraction (ZOOM1 uniquement)

Durée scope temporel : 4 derniers trimestres fiscaux complets

Sources : - SYS_TARIF_SIMULATION : Offres actuelles - SYS_FACTURE_LIGNE : Historique transactionnel - PT0CE_TYPE_CLIENT_MAPPING : Enrichissement TYPE_CLIENT - PT0CE_TYPE_RESTAURANT_MAPPING : Enrichissement TYPE_RESTAURANT - SYS_MD_CALENDRIER_SYSCO : Détermination périodes fiscales

CTEs dans la requête : 1. PRP_LABELS : Exclusion prix promo 2. ORIGINE_CDE_UNIQUE : Origine commande 3. CURRENT_OFFERS : Offres actuelles ZOOM1 4. LAST_FG_HM : Dernier FG_HM par client × article (période 4Q) 5. HISTORICAL_PERFORMANCE : Métriques 4Q (CA, volumes) 6. LAST_QT_UF : Dernière quantité (période 4Q) 7. OFFERS_WITH_UNIVERS : Calcul UNIVERS selon Business Rules 8. Jointure finale : Enrichissement dimensions

Transformation des données : - FG_HM : 'X' → '1', sinon '0' - ID_KAM : NULL ou vide → 'NO_KAM' - UNIVERS : Calcul selon règles ZOOM1 (voir doc CONCEPTS.md)

Sortie : - Table : PT2QE_PRICE_OFFERS - Colonnes : ~50 colonnes (identifiants, dimensions, prix, métriques 4Q)

Phase 2 : Enrichissement corridors

Sources : - PT2QE_PRICE_OFFERS - PT1CE_OPTIMAL_ZOOM1 (MASTER + NATIONAL)

CTEs dans la requête : 1. CORRIDORS_ZOOM1_ONLY : Filtrage ZOOM1 strict 2. MATCH_MASTER : Tentative match 4 dimensions 3. MATCH_NATIONAL : Fallback ID_ART seul 4. NO_MATCH : Offres sans corridor

Champs ajoutés : - Anciennes bornes : PAS_ACTIF, PRB_ACTIF, BORNE_PL1_PL2, etc. - Nouvelles bornes : NEW_PAS, NEW_PRB, NEW_BORNE_PL1_PL2, etc. - Métadonnées : PRICE_SENSITIVITY, ECART_TYPE, PCT_HAUSSE_PAS - Matching : HAS_CORRIDOR, MATCH_TYPE, SOURCE_ZOOM, CUBE_TYPE

Sortie : - Table : PT2QE_PRICE_OFFERS_ENRICHED

Phase 3 : Calcul recommandations

Sources : - PT2QE_PRICE_OFFERS_ENRICHED - PT2QE_CAPPING_CUBES - config/pt2qe_config.json (règles RECO1)

CTEs dans la requête : 1. ENRICHED_DATA : Jointure cappings + détection flags - IS_PAS_BAISSE : 1 si NEW_PAS < PAS_ACTIF - IS_PL1_ANCIENNES : 1 si prix dans PL1 anciennes bornes

  1. RECO_CALC : Calcul recommandations de base
  2. RECO1_BASE : Selon règles paramétrables
  3. RECO2 : Hausse proportionnelle PAS

  4. RECO_WITH_CAPPING_SENSITIVITY : Capping sensibilité

  5. RECO1_APRES_CAPPING_SENSIBILITE
  6. CAPPING_SENSIBILITE_APPLIED : Flag

  7. RECO_WITH_CAPPING_BASIQUES : Capping basiques en cascade

  8. RECO1_AVEC_CAPPING
  9. CAPPING_BASIQUES_APPLIED : Flag

  10. DECISION_TREE : Arbre de décision

  11. DECISION_PATH : Chemin emprunté
  12. PRIX_AVANT_CAPPING_PRB : Prix avant capping final
  13. PLANCHER_PL2_APPLIED : Flag (CHEMIN 2)
  14. RECO_TYPE : Type reco gagnante
  15. RECO_SELECTIONNEE : Nom explicite

  16. FINAL_PRICES : Application capping PRB

  17. PRIX_RECOMMANDE : Prix final
  18. CAPPING_PRB_APPLIED : Flag

  19. FINAL_PRICES_WITH_CAPPING : Synthèse capping

  20. CAPPING_APPLIED : Type le plus contraignant
  21. PCT_HAUSSE_FINALE : % hausse

  22. Sélection finale : Tous les champs + 3 positions

Sortie : - Table : PT2QE_RECOMMENDATIONS - Colonnes : ~70 colonnes (tout + cascade calculs + 3 positions)

Phase 4 : Analyses et export

Source : - PT2QE_RECOMMENDATIONS

Génération 6 fichiers : 1. recommendations_detail.csv : Export brut 2. statistics_by_dimension.csv : Agrégations 3. impact_analysis.csv : Impact CA 4. price_increase_distribution.csv : Histogramme 5. decision_path_analysis.csv : Stats par chemin 6. capping_distribution.csv : Impact cappings

Génération export final (Option 3) : - final_price_offers.csv - validation_report.csv - export_summary.txt


Gestion des tables Oracle

Tables créées automatiquement

1. PT2QE_PRICE_OFFERS - Scope : Offres ZOOM1 actuelles + historique 4Q - Compression : COMPRESS NOLOGGING - Index : 4 index automatiques - Durée de vie : Recréée à chaque run (Option 1) - Comment supprimer : DROP TABLE PT2QE_PRICE_OFFERS PURGE

2. PT2QE_PRICE_OFFERS_ENRICHED - Scope : Offres + corridors PT1CE - Compression : COMPRESS NOLOGGING - Index : Aucun - Durée de vie : Recréée à chaque calcul - Comment supprimer : DROP TABLE PT2QE_PRICE_OFFERS_ENRICHED PURGE

3. PT2QE_CAPPING_CUBES - Scope : Cappings par cube (MASTER + NATIONAL) - Compression : Non compressée - Index : Aucun - Durée de vie : Recréée à chaque calcul - Comment supprimer : DROP TABLE PT2QE_CAPPING_CUBES PURGE

4. PT2QE_RECOMMENDATIONS - Scope : Recommandations finales avec cascade complète - Compression : COMPRESS NOLOGGING - Index : Aucun - Durée de vie : Recréée à chaque calcul, mise à jour par corrections - Comment supprimer : DROP TABLE PT2QE_RECOMMENDATIONS PURGE

5. TEMP_CAPPING_CORRECTIONS (temporaire) - Scope : Corrections de cappings - Compression : Non compressée - Index : Aucun - Durée de vie : Supprimée après application corrections - Comment supprimer : Automatique

Stratégie DROP/CREATE

Tous les modules utilisent :

try:
    q = self.db.create_query(f"DROP TABLE {table_name} PURGE")
    q.execute()
    q.close()
except:
    pass

# Puis CREATE TABLE ...

Avantages : - Pas de problème si table existe déjà - PURGE libère l'espace immédiatement (pas de corbeille) - Clean entre les runs

Index créés

Sur 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;

Rationale : - Accélère les jointures client × article - Accélère les recherches par TYPE_CLIENT - COMPRESS réduit l'espace disque

Commandes utiles

Vérifier l'existence d'une table :

SELECT COUNT(*) 
FROM user_tables 
WHERE table_name = 'PT2QE_PRICE_OFFERS';

Compter les lignes :

SELECT COUNT(*) FROM PT2QE_RECOMMENDATIONS;

Voir les index :

SELECT index_name, table_name, column_name
FROM user_ind_columns
WHERE table_name LIKE 'PT2QE%'
ORDER BY table_name, index_name, column_position;

Voir l'espace utilisé :

SELECT 
    segment_name, 
    ROUND(bytes/1024/1024, 2) as MB
FROM user_segments
WHERE segment_name LIKE 'PT2QE%'
ORDER BY bytes DESC;

Supprimer toutes les tables PT2QE :

BEGIN
    FOR t IN (SELECT table_name FROM user_tables WHERE table_name LIKE 'PT2QE%') LOOP
        EXECUTE IMMEDIATE 'DROP TABLE ' || t.table_name || ' PURGE';
    END LOOP;
END;
/

Comment forcer la recréation d'une table : 1. Option manuelle :

DROP TABLE PT2QE_RECOMMENDATIONS PURGE;
2. Option automatique : Relancer le calcul (Option 1 ou 2)


Stratégies d'optimisation

Optimisation des requêtes SQL

1. Parallélisation

Directive PARALLEL(8) sur les requêtes lourdes :

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

Où appliqué : - Extraction historique 4Q (HISTORICAL_PERFORMANCE) - Récupération LAST_FG_HM - Récupération LAST_QT_UF

Comment modifier le degré de parallélisme : 1. Éditer extract_price_offers.py 2. Chercher /*+ PARALLEL(8) */ 3. Remplacer 8 par la valeur souhaitée (ex: PARALLEL(4)) 4. Attention : Ne pas dépasser les ressources disponibles

2. Compression et NOLOGGING

Tables avec COMPRESS NOLOGGING :

CREATE TABLE PT2QE_PRICE_OFFERS COMPRESS NOLOGGING AS ...

Avantages : - COMPRESS : Réduit espace disque (~50%) - NOLOGGING : Accélère création (pas d'écriture redo logs)

Inconvénient : - NOLOGGING : Table non récupérable en cas de crash (acceptable car recréée)

3. Index compressés

Tous les index avec COMPRESS :

CREATE INDEX IX_... ON PT2QE_... (...) COMPRESS;

Avantage : - Réduit espace disque index (~30-50%) - Accélère scans d'index

4. Pré-calcul des cappings par cube

Au lieu de joindre à chaque calcul :

-- ❌ Lent : Join répété
SELECT ...
FROM offres o
JOIN ( calcul capping dynamique ) cap
ON ...

-- ✅ Rapide : Pre-calculé
SELECT ...
FROM offres o
JOIN PT2QE_CAPPING_CUBES cap
ON o.UNIVERS = cap.UNIVERS
   AND o.TYPE_CLIENT = cap.TYPE_CLIENT
   ...

Table PT2QE_CAPPING_CUBES : - Générée une fois avant calculs - Contient tous les cappings par cube - Jointure simple et rapide

5. CTEs vs tables temporaires

PT2QE utilise des CTEs (WITH) plutôt que tables temporaires :

Avantages : - Pas de création/suppression de tables - Optimiseur Oracle peut inliner - Code plus lisible

Inconvénient : - CTEs réexécutées si référencées plusieurs fois - (Mais PT2QE référence chaque CTE une seule fois)

Optimisation mémoire

1. Pas de chargement complet en mémoire Python

Toutes les opérations en SQL : - Calculs côté base Oracle - Pas de .read() massif en Python - Python orchestre, Oracle calcule

2. Lecture par chunks pour exports

Dans analyze_recommendations.py :

q = self.db.create_query(f"SELECT ... ORDER BY ... FETCH FIRST 10000 ROWS ONLY")
df = q.read()
q.close()

Comment modifier la taille des chunks : - Pas de paramètre chunk explicite dans PT2QE - Si nécessaire, éditer les requêtes SQL avec FETCH FIRST N ROWS ONLY

3. Fermeture systématique des curseurs

Pattern utilisé partout :

q = self.db.create_query(query)
result = q.read()  # ou q.read_value() ou q.execute()
q.close()  # ← Toujours présent

Pourquoi important : - Libère les ressources Oracle - Évite les fuites mémoire - Limite le nombre de curseurs ouverts

Optimisation des index

Stratégie de placement : - Index sur colonnes de jointure fréquentes - Index composites pour accélerer les WHERE composés - Pas d'index sur les tables intermédiaires (CTE)

Comment ajouter un index : 1. Éditer extract_price_offers.py 2. Modifier la méthode _create_indexes() 3. Ajouter une ligne dans le tableau indexes :

indexes = [
    (f"IX_{table_name}_CLN", "ID_CLN"),
    (f"IX_{table_name}_NOUVEL_INDEX", "COLONNE1, COLONNE2"),  # Nouveau
]

Comment supprimer un index :

DROP INDEX IX_PT2QE_PRICE_OFFERS_CLN;


Configuration et paramétrage

Fichier de configuration

Emplacement : config/pt2qe_config.json

Structure complète :

{
    "processing": {
        "batch_size": 100000,
        "parallel_degree": 8
    },
    "capping": {
        "default_high": 0.05,
        "default_medium": 0.15,
        "default_low": 0.20,
        "basiques": 0.50,
        "allow_overrides": true
    },
    "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"
            }
        ],
        "selection_rule": "max"
    },
    "output": {
        "csv_separator": ";",
        "csv_encoding": "cp1252",
        "decimal_places": 3,
        "percentage_format": 2
    }
}

Paramètres modifiables

1. Processing

"processing": {
    "batch_size": 100000,      // Non utilisé actuellement
    "parallel_degree": 8       // Non utilisé actuellement (hardcodé dans SQL)
}

Comment modifier : - Pour parallélisme : Éditer directement les requêtes SQL (voir section Optimisation)

2. Cappings

"capping": {
    "default_high": 0.05,      // 5% pour sensibilité HIGH
    "default_medium": 0.15,    // 15% pour sensibilité MEDIUM
    "default_low": 0.20,       // 20% pour sensibilité LOW
    "basiques": 0.50,          // 50% max pour produits basiques
    "allow_overrides": true    // Autorise corrections par cube
}

Comment modifier : 1. Éditer config/pt2qe_config.json 2. Changer les valeurs (format décimal : 0.05 = 5%) 3. Relancer le calcul (Option 1)

Attention : Ces valeurs sont utilisées par défaut si pas de fichier capping_type_client.csv

3. Règles RECO1

Voir section "Module de calcul des recommandations" pour détails.

Comment ajouter une règle : 1. Éditer config/pt2qe_config.json 2. Ajouter un élément dans reco1_rules :

{
    "position": "NOUVELLE_POSITION",
    "condition": "PRIX_TARIF_ACTUEL > SEUIL",
    "action": "DESCRIPTION_ACTION",
    "target": "COLONNE_CIBLE",
    "comment": "Explication"
}
3. Attention : L'ordre dans le tableau = ordre des WHEN dans le CASE SQL 4. Dernière règle doit avoir "condition": "DEFAULT" (ELSE)

Comment modifier une règle existante : 1. Éditer config/pt2qe_config.json 2. Modifier les champs condition, target, ou comment 3. Exemple : Changer PL2_PL3 pour remonter vers PL2 au lieu de PL1 :

{
    "position": "PL2_PL3",
    "condition": "PRIX_TARIF_ACTUEL > NEW_BORNE_PL3_PL4",
    "action": "TO_PL2",              // ← Changé
    "target": "NEW_BORNE_PL2_PL3",   // ← Changé
    "comment": "Remonter vers PL2"   // ← Changé
}

4. Output

"output": {
    "csv_separator": ";",       // Séparateur CSV (défaut: point-virgule)
    "csv_encoding": "cp1252",   // Encodage (Windows)
    "decimal_places": 3,        // Décimales pour prix
    "percentage_format": 2      // Décimales pour pourcentages
}

Comment modifier : 1. Éditer config/pt2qe_config.json 2. Changer les valeurs 3. Impact sur tous les exports CSV

Fichier de cappings type client

Emplacement : inputs/capping_type_client.csv

Format :

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

Colonnes obligatoires : - TYPE_CLIENT : Type client (doit correspondre au mapping PT0CE) - CAPPING_HIGH : Capping pour sensibilité HIGH (format décimal avec virgule) - CAPPING_MEDIUM : Capping pour sensibilité MEDIUM - CAPPING_LOW : Capping pour sensibilité LOW

Comment modifier : 1. Ouvrir inputs/capping_type_client.csv avec Excel ou éditeur texte 2. Modifier les valeurs (format : 0,025 = 2.5%) 3. Sauvegarder en CSV avec séparateur point-virgule 4. Relancer le calcul (Option 1)

Ligne NATIONAL : - Spéciale : Appliquée aux corridors NATIONAL (fallback) - Si absente, valeurs par défaut de la config

Variables d'environnement

PYTHONPATH :

set PYTHONPATH=P:\PRD\Python\00_Commun\libs;%PYTHONPATH%

Pourquoi nécessaire : - Charge les modules sysco.* (application, db.oracle) - Défini dans les fichiers .bat

Comment modifier : 1. Éditer START.bat, 1_calculer_recommendations.bat, etc. 2. Changer la ligne set PYTHONPATH=... 3. Attention : Ne pas supprimer, sinon erreurs d'import


Gestion des erreurs et récupération

Validation des prérequis

Au démarrage de pt2qe_main.py :

  1. Vérification fichier capping :

    if not capping_path.exists():
        app.log.error(f"Fichier capping manquant : {capping_path}")
        sys.exit(1)
    

  2. Vérification tables PT1CE : Via test_connection.py --check-pt1ce :

    SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME LIKE 'PT1CE_OPTIMAL_%'
    

  3. Vérification mappings : Via test_connection.py --check-mappings :

    SELECT COUNT(*) FROM PT0CE_TYPE_CLIENT_MAPPING;
    SELECT COUNT(*) FROM PT0CE_TYPE_RESTAURANT_MAPPING;
    

  4. Détermination périodes : Via PeriodManager.get_last_four_complete_fiscal_quarters() :

  5. Si échec : ValueError avec message explicite
  6. Si < 4 trimestres complets : ValueError

Comment vérifier manuellement les prérequis :

python test_connection.py TARIFAIRE --check-pt1ce
python test_connection.py TARIFAIRE --check-mappings

Logging détaillé

Niveaux de log : - INFO : Étapes principales, statistiques - WARNING : Situations anormales non bloquantes - ERROR : Erreurs bloquantes - DEBUG : Détails techniques (avec -vv)

Structure des logs :

2025-01-15 14:30:15 - INFO - Démarrage PRICING_TIER2_QUOTE_ENGINE v1.0.0
2025-01-15 14:30:15 - INFO - ⚠️  MODE ZOOM1 EXCLUSIF
2025-01-15 14:30:16 - INFO - Période d'analyse: 2024-02-01 → 2024-12-31
2025-01-15 14:30:16 - INFO - Trimestres fiscaux: 2024_Q01, 2024_Q02, 2024_Q03, 2024_Q04

=== Phase 1: Extraction des offres de prix avec historique ===
2025-01-15 14:30:20 - INFO -   Extraction des offres avec historique de performance
2025-01-15 14:35:42 - INFO -     ✓ 2,345,678 offres de prix extraites (ZOOM1 uniquement)
2025-01-15 14:35:42 - INFO -     Statistiques d'enrichissement:
2025-01-15 14:35:42 - INFO -       - Total offres: 2,345,678
2025-01-15 14:35:42 - INFO -       - Clients uniques: 12,345
2025-01-15 14:35:42 - INFO -       - Articles uniques: 23,456
...

Comment activer le mode debug :

python pt2qe_main.py TARIFAIRE -vv --capping-file inputs/capping_type_client.csv

Comment désactiver les logs :

python pt2qe_main.py TARIFAIRE -qq --capping-file inputs/capping_type_client.csv

Points de contrôle et validation

1. Après extraction (Phase 1) : - Vérifier nb offres > 0 - Vérifier taux enrichissement TYPE_CLIENT/TYPE_RESTAURANT > 80%

2. Après matching corridors (Phase 2) : - Vérifier taux matching > 70% - Afficher répartition MASTER/NATIONAL/NO_MATCH

3. Après calcul recommandations (Phase 3) : - Vérifier nb recommandations > 0 - Afficher hausse moyenne - Vérifier pas de hausses < 0% (anomalie)

4. Après génération finale (Phase 4) : - Vérifier nb offres exportées - Afficher répartition VALIDATION_STATUS

Comment ajouter un point de contrôle : 1. Éditer le module concerné 2. Ajouter une requête SQL de validation :

q = self.db.create_query(f"SELECT COUNT(*) FROM {table} WHERE condition_anomalie")
anomalies = q.read_value()
q.close()

if anomalies > 0:
    self.app.log.warning(f"⚠️ {anomalies} anomalies détectées")

Gestion des cas limites

1. Division par zéro :

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

2. Valeurs NULL :

-- Utilisation de NVL et COALESCE
NVL(GEO, 'NULL')
COALESCE(FG_HM, '0')

3. Offres sans corridor : - Marquées HAS_CORRIDOR = 0, MATCH_TYPE = 'NO_MATCH' - Exclues des calculs de recommandations - Comptabilisées dans les stats

4. Cappings manquants : - Si TYPE_CLIENT absent du fichier : Utilise valeurs par défaut config - Si NATIONAL absent : Utilise valeurs par défaut config

5. Périodes invalides : - Si < 4 trimestres complets : ValueError avec message explicite - Si période incohérente : ValueError

Récupération après erreur

Stratégie : - Pas de checkpoint automatique - Tables recréées à chaque run - Mode --skip-extraction permet de reprendre après Phase 1

Scénarios de récupération :

1. Erreur en Phase 1 (extraction) : - Corriger le problème (connexion, prérequis, etc.) - Relancer Option 1 complète

2. Erreur en Phase 2 ou 3 (calculs) : - Si erreur transitoire (connexion) : Relancer Option 1 - Si erreur de données : Corriger les données sources, relancer Option 1

3. Erreur en Phase 4 (analyses) : - Les tables PT2QE_RECOMMENDATIONS existent déjà - Corriger le problème - Relancer le calcul : Les analyses seront régénérées

4. Corruption de tables :

-- Supprimer toutes les tables PT2QE
DROP TABLE PT2QE_PRICE_OFFERS PURGE;
DROP TABLE PT2QE_PRICE_OFFERS_ENRICHED PURGE;
DROP TABLE PT2QE_CAPPING_CUBES PURGE;
DROP TABLE PT2QE_RECOMMENDATIONS PURGE;

-- Relancer Option 1

Comment forcer une récupération propre : 1. Supprimer toutes les tables PT2QE (voir commandes ci-dessus) 2. Vérifier les prérequis (PT1CE, mappings) 3. Relancer Option 1 complète

Messages d'erreur courants

1. "Fichier capping manquant" :

ERREUR: Fichier inputs\capping_type_client.csv introuvable !
Solution : Créer le fichier avec le bon format

2. "Tables PT1CE_OPTIMAL_* manquantes" :

PT2QE NECESSITE que PT1CE ait ete execute auparavant !
Solution : Exécuter PT1CE Option 1 puis Option 2

3. "Aucune offre extraite" :

✓ 0 offres de prix extraites (ZOOM1 uniquement)
Solution : Vérifier filtres, vérifier données sources

4. "Impossible de trouver 4 trimestres complets" :

ValueError: Impossible de trouver 4 trimestres complets avant 2025-01-15
Solution : Vérifier données dans SYS_MD_CALENDRIER_SYSCO

5. "Connexion Oracle perdue" :

ORA-03113: end-of-file on communication channel
Solution : Vérifier connexion réseau, relancer


Points techniques d'implémentation

Connexions Oracle

Classe utilisée : SysOraDB (de sysco.db.oracle)

Initialisation :

db = SysOraDB(app.args.database)  # Ex: 'TARIFAIRE'

Gestion des curseurs :

# Créer un curseur
q = db.create_query(sql_query)

# Exécuter et récupérer données
df = q.read()           # DataFrame pandas
value = q.read_value()  # Valeur unique
rows = q.execute()      # Nombre de lignes affectées

# TOUJOURS fermer
q.close()

Commit :

# À la fin de chaque phase
db.commit()

# Ou automatique à la fin du script

Dispose :

# Fermeture connexion (automatique en fin de script)
db.dispose()

Gestion des dates

Classe utilisée : date et datetime de Python

Conversion Python → Oracle :

from datetime import date

def format_date_oracle(dt: date) -> str:
    return f"TO_DATE('{dt.strftime('%Y-%m-%d')}', 'YYYY-MM-DD')"

Utilisation dans requêtes :

start_date = date(2024, 1, 1)
query = f"""
    SELECT * FROM table
    WHERE DT >= {format_date_oracle(start_date)}
"""

Récupération depuis Oracle :

# Les dates Oracle sont converties automatiquement en datetime.date Python
q = db.create_query("SELECT ID_SEM FROM SYS_MD_CALENDRIER_SYSCO FETCH FIRST 1 ROW ONLY")
sem_date = q.read_value()  # Type: date
q.close()

Pandas et exports

Lecture depuis Oracle :

q = db.create_query("SELECT * FROM table")
df = q.read()  # Type: pandas.DataFrame
q.close()

Export CSV :

df.to_csv(
    output_file,
    index=False,           # Pas d'index de ligne
    sep=';',              # Point-virgule (Excel français)
    decimal=',',          # Virgule décimale (Excel français)
    encoding='cp1252'     # Windows-1252 (Excel Windows)
)

Pourquoi cp1252 : - Encodage natif Excel Windows - Supporte accents français - Alternative : 'utf-8-sig' (avec BOM)

Arrondis :

# Montants : 3 décimales
df['PRIX'] = df['PRIX'].round(3)

# Pourcentages : 2 décimales (après conversion en %)
df['PCT_HAUSSE'] = (df['PCT_HAUSSE'] * 100).round(2)

Gestion des chemins

Classe utilisée : pathlib.Path

Création dossiers :

from pathlib import Path

output_folder = Path('outputs') / 'run_20250115_143000'
output_folder.mkdir(parents=True, exist_ok=True)

Concaténation chemins :

output_file = output_folder / 'recommendations_detail.csv'
# Équivalent: Path('outputs/run_20250115_143000/recommendations_detail.csv')

Vérification existence :

if output_file.exists():
    print(f"Fichier existe : {output_file}")

Lecture :

with open(output_file, 'r', encoding='utf-8') as f:
    content = f.read()

Construction de requêtes SQL dynamiques

1. Avec f-strings :

table_name = "PT2QE_PRICE_OFFERS"
query = f"""
    SELECT COUNT(*) FROM {table_name}
    WHERE UNIVERS = 'ZOOM1'
"""

Attention : Valider table_name pour éviter injection SQL

2. Avec paramètres :

# Pas de paramètres bindés dans PT2QE (car DDL et DML complexes)
# Alternative : Validation stricte des valeurs
if table_name not in ['PT2QE_PRICE_OFFERS', 'PT2QE_RECOMMENDATIONS']:
    raise ValueError(f"Table invalide: {table_name}")

3. Génération CASE SQL depuis config :

def build_reco1_sql_case(self, table_alias: str = "ed") -> str:
    rules = self.get_reco1_rules()
    case_parts = ["CASE"]

    for rule in rules:
        condition = rule['condition'].replace("PRIX_TARIF_ACTUEL", f"{table_alias}.PRIX_TARIF_ACTUEL")
        target = rule['target'].replace("NEW_", f"{table_alias}.NEW_")

        if condition == "DEFAULT":
            case_parts.append(f"    ELSE {target}")
        else:
            case_parts.append(f"    WHEN {condition} THEN {target}")

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

Pattern de traitement

Structure standard :

class ModuleProcessor:
    def __init__(self, app: SysApplication, db: SysOraDB):
        self.app = app
        self.db = db

    def process(self, input_table: str, output_table: str):
        self.app.log.info("  Traitement en cours")

        # 1. Drop table si existe
        try:
            q = self.db.create_query(f"DROP TABLE {output_table} PURGE")
            q.execute()
            q.close()
        except:
            pass

        # 2. Créer table
        query = f"CREATE TABLE {output_table} COMPRESS NOLOGGING AS SELECT ..."
        q = self.db.create_query(query)
        q.execute()
        q.close()

        # 3. Stats
        q = self.db.create_query(f"SELECT COUNT(*) FROM {output_table}")
        count = q.read_value()
        q.close()

        self.app.log.info(f"    ✓ {count:,} lignes traitées")

Formattage des nombres

Dans les logs :

# Séparateurs de milliers
print(f"{value:,}")  # Ex: 1,234,567

# Pourcentages
print(f"{pct:.1%}")  # Ex: 12.3%

# Décimales
print(f"{montant:.2f}")  # Ex: 123.45

Dans les DataFrames :

# Arrondi
df['MONTANT'] = df['MONTANT'].round(2)

# Conversion pourcentages
df['PCT'] = (df['RATIO'] * 100).round(2)

# Format avec NVL
df['VALEUR'] = df['VALEUR'].fillna(0)


Maintenance et évolution

Comment ajouter une nouvelle métrique

Exemple : Ajouter le nombre de commandes 4Q

  1. Modifier l'extraction (extract_price_offers.py) :

    -- Dans la CTE HISTORICAL_PERFORMANCE
    COUNT(DISTINCT f.ID_CDE) as NB_COMMANDES_4Q,
    

  2. Ajouter dans la sélection finale :

    NVL(hp.NB_COMMANDES_4Q, 0) as NB_COMMANDES_4Q,
    

  3. Propager dans les tables suivantes :

  4. PT2QE_PRICE_OFFERS_ENRICHED (via jointure)
  5. PT2QE_RECOMMENDATIONS (via SELECT)

  6. Ajouter dans les exports (analyze_recommendations.py) :

  7. Ajouter colonne dans _export_recommendations_detail()

Comment ajouter un nouveau type de capping

Exemple : Ajouter un capping "produits fragiles" à 30%

  1. Modifier la cascade (calculate_recommandations.py) :

    -- Après RECO_WITH_CAPPING_BASIQUES, ajouter :
    RECO_WITH_CAPPING_FRAGILES AS (
        SELECT
            rwb.*,
            CASE
                WHEN rwb.LC_ATTRIBUT = 'Fragile' THEN
                    LEAST(rwb.RECO1_AVEC_CAPPING, rwb.PRIX_TARIF_ACTUEL * 1.30)
                ELSE rwb.RECO1_AVEC_CAPPING
            END as RECO1_AVEC_CAPPING_FRAGILE,
    
            CASE
                WHEN rwb.LC_ATTRIBUT = 'Fragile' 
                    AND rwb.RECO1_AVEC_CAPPING > rwb.PRIX_TARIF_ACTUEL * 1.30
                THEN 1
                ELSE 0
            END as CAPPING_FRAGILE_APPLIED
        FROM RECO_WITH_CAPPING_BASIQUES rwb
    ),
    

  2. Modifier l'arbre de décision :

  3. Remplacer RECO_WITH_CAPPING_BASIQUES par RECO_WITH_CAPPING_FRAGILES
  4. Utiliser RECO1_AVEC_CAPPING_FRAGILE au lieu de RECO1_AVEC_CAPPING

  5. Modifier le calcul de CAPPING_APPLIED :

    CASE
        WHEN ... THEN 'GEL_PAS'
        WHEN ... THEN 'PRB_FINAL'
        WHEN ... THEN 'PLANCHER_PL2_PL3'
        WHEN CAPPING_FRAGILE_APPLIED = 1 THEN 'FRAGILE_30PCT'  -- Nouveau
        WHEN ... THEN 'BASIQUES_50PCT'
        WHEN ... THEN 'SENSIBILITE'
        ELSE 'NONE'
    END
    

  6. Documenter :

  7. Ajouter dans CAPPING.md
  8. Ajouter dans CONCEPTS.md

Comment modifier l'arbre de décision

Exemple : Ajouter un chemin pour clients premium avec KAM

  1. Définir la condition :

    IS_PREMIUM_KAM = CASE 
        WHEN ID_KAM <> 'NO_KAM' AND IS_PL1_ANCIENNES = 1 
        THEN 1 ELSE 0 
    END
    

  2. Modifier DECISION_TREE :

    CASE
        WHEN rwc.IS_PAS_BAISSE = 1 THEN 'PAS_BAISSE_GEL_PRIX'
        WHEN rwc.IS_PREMIUM_KAM = 1 THEN 'PREMIUM_KAM_SPECIAL'  -- Nouveau
        WHEN rwc.IS_PL1_ANCIENNES = 1 THEN 'PL1_CONSERVATION_PREMIUM'
        ELSE 'OPTIMISATION_STANDARD'
    END
    

  3. Définir l'action :

    CASE
        WHEN rwc.IS_PAS_BAISSE = 1 THEN rwc.PRIX_TARIF_ACTUEL
        WHEN rwc.IS_PREMIUM_KAM = 1 THEN 
            GREATEST(rwc.PRIX_TARIF_ACTUEL * 1.02, rwc.NEW_BORNE_PL1_PL2)  -- Nouveau
        WHEN rwc.IS_PL1_ANCIENNES = 1 THEN ...
        ELSE ...
    END
    

  4. Ajouter le RECO_TYPE :

    CASE
        WHEN rwc.IS_PAS_BAISSE = 1 THEN 'GEL_PRIX'
        WHEN rwc.IS_PREMIUM_KAM = 1 THEN 'PREMIUM_KAM_HAUSSE'  -- Nouveau
        WHEN rwc.IS_PL1_ANCIENNES = 1 THEN 'CONSERVATION_PREMIUM'
        ELSE ...
    END
    

  5. Documenter :

  6. Ajouter dans WORKFLOW.md
  7. Ajouter dans CALCULS.md

Comment ajouter une nouvelle dimension d'analyse

Exemple : Ajouter une analyse par GEO

  1. Ajouter dans analyze_recommendations.py :

    def _export_statistics_by_geo(self, table: str, output_folder: Path):
        output_file = output_folder / 'statistics_by_geo.csv'
    
        query = f"""
        SELECT
            'GEO' as DIMENSION,
            GEO 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) as PCT_HAUSSE_MOY
        FROM {table}
        GROUP BY GEO
        ORDER BY GEO
        """
    
        q = self.db.create_query(query)
        df = q.read()
        q.close()
    
        df.to_csv(output_file, index=False, sep=';', decimal=',', encoding='cp1252')
        self.app.log.info(f"    → Statistiques par GEO exportées")
    

  2. Appeler dans analyze_and_export() :

    def analyze_and_export(self, recommendations_table: str, output_folder: Path):
        ...
        self._export_statistics_by_geo(recommendations_table, output_folder)  # Nouveau
    

  3. Documenter :

  4. Ajouter dans EXPORTS.md

Checklist de déploiement

Avant le premier run

  • [ ] PT1CE Option 1 exécutée (application PAS/PRB)
  • [ ] PT1CE Option 2 exécutée (finalisation corridors → création PT1CE_OPTIMAL_*)
  • [ ] Tables PT0CE_TYPE_CLIENT_MAPPING créée et remplie
  • [ ] Tables PT0CE_TYPE_RESTAURANT_MAPPING créée et remplie
  • [ ] Fichier inputs/capping_type_client.csv créé avec les cappings souhaités
  • [ ] Dossier outputs/ existe (créé automatiquement sinon)
  • [ ] Dossier corrections/ existe si corrections prévues (créé automatiquement sinon)
  • [ ] Connexion Oracle testée : python test_connection.py TARIFAIRE
  • [ ] Prérequis validés : python test_connection.py TARIFAIRE --check-pt1ce --check-mappings

Après chaque run

  • [ ] Vérifier logs dans la console (pas d'erreurs)
  • [ ] Vérifier fichiers générés dans outputs/run_*/
  • [ ] Analyser recommendations_detail.csv (hausses cohérentes)
  • [ ] Analyser decision_path_analysis.csv (répartition chemins)
  • [ ] Vérifier capping_distribution.csv (cappings appliqués)
  • [ ] Comparer avec run précédent si applicable

Avant mise en production

  • [ ] Tests sur plusieurs scénarios de cappings
  • [ ] Validation métier des recommandations
  • [ ] Documentation à jour
  • [ ] Backup des tables PT2QE existantes (si nécessaire)
  • [ ] Communication aux utilisateurs (changements, impacts)

Maintenance régulière

  • [ ] Vérifier espace disque Oracle
  • [ ] Archiver anciens runs dans outputs/
  • [ ] Purger tables PT2QE anciennes si nécessaire
  • [ ] Mettre à jour PT1CE si changements de corridors
  • [ ] Réviser cappings selon évolution du marché

Annexes techniques

Hiérarchie des tables Oracle

SYS_* (données sources)
  ├── SYS_TARIF_SIMULATION
  ├── SYS_FACTURE_LIGNE
  ├── SYS_MD_CLIENT
  ├── SYS_MD_CONDITION
  ├── SYS_MD_ARTICLE
  ├── SYS_MD_CALENDRIER_SYSCO
  └── SYS_MD_ORIGINE_PRIX

PT0CE_* (mappings Tier 0)
  ├── PT0CE_TYPE_CLIENT_MAPPING
  └── PT0CE_TYPE_RESTAURANT_MAPPING

PT1CE_* (corridors Tier 1)
  ├── PT1CE_OPTIMAL_ZOOM1
  ├── PT1CE_OPTIMAL_ZOOM2
  └── PT1CE_OPTIMAL_ZOOM3

PT2QE_* (recommandations Tier 2)
  ├── PT2QE_PRICE_OFFERS
  ├── PT2QE_PRICE_OFFERS_ENRICHED
  ├── PT2QE_CAPPING_CUBES
  └── PT2QE_RECOMMENDATIONS

Dépendances Python

Fichier : requirements.txt

oracledb>=1.4.0        # Connexion Oracle
pandas>=2.0.0          # Manipulation données
numpy>=1.24.0          # Calculs numériques
python-dateutil>=2.8.2 # Gestion dates
openpyxl>=3.1.0        # Export Excel (si nécessaire)
tqdm>=4.64.0           # Barre progression (non utilisé)
scipy>=1.10.0          # Stats avancées (non utilisé)

Installation :

pip install -r requirements.txt

Remarque : Les modules sysco.* sont dans P:\PRD\Python\00_Commun\libs

Conventions de nommage

Tables Oracle : - Préfixe : PT2QE_ - Format : PT2QE_<NOM_LOGIQUE> - Exemples : PT2QE_PRICE_OFFERS, PT2QE_RECOMMENDATIONS

Index Oracle : - Préfixe : IX_<TABLE>_ - Format : IX_<TABLE>_<COLONNES> - Exemples : IX_PT2QE_PRICE_OFFERS_CLN, IX_PT2QE_PRICE_OFFERS_MAIN

Fichiers CSV : - Format : <nom_descriptif>.csv - Séparateur : Point-virgule (;) - Encodage : CP1252 - Décimale : Virgule (,)

Dossiers de sortie : - Format : <type>_YYYYMMDD_HHMMSS - Exemples : run_20250115_143000, corrections_20250115_150000, final_20250115_153000

Variables Python : - Tables : snake_case (ex: offers_table, recommendations_table) - Classes : PascalCase (ex: RecommendationCalculator) - Constantes : UPPER_CASE (ex: TABLE_PREFIX, APP_VERSION)

Glossaire technique

  • CTE : Common Table Expression (WITH clause)
  • COMPRESS : Compression Oracle (économise espace disque)
  • NOLOGGING : Pas de logs redo (accélère création tables)
  • PURGE : Suppression immédiate (pas de corbeille)
  • PARALLEL : Exécution parallèle (utilise plusieurs threads Oracle)
  • Fallback : Solution de secours (ex: NATIONAL si MASTER échoue)
  • Cascade : Traitement séquentiel (ex: cappings successifs)
  • Tier : Niveau de traitement (Tier 1 = corridors, Tier 2 = recommandations)
  • Scope temporel : Période d'analyse des données
  • 4Q : 4 derniers quarters (trimestres fiscaux)