Aller au contenu

Step_05_Building_Baseline - Calcul des effets de volume

Vue d'ensemble

Cette étape calcule les effets de volume promotionnels (uplift, cannibalisation, halo, pantry loading) en comparant les volumes réels aux volumes de baseline calculés précédemment. Elle comprend deux phases : le calcul des effets (main_volume_effects) et un contrôle qualité spécifique (main_quality_check).

Objectif principal

  • Calculer l'uplift (augmentation des ventes due aux promotions)
  • Identifier la cannibalisation (baisse des ventes hors promo)
  • Pour Galbani : calculer le forward buying avec sell-out
  • Pour Parmalat : initialiser halo/pantry loading (calcul réel en Step 6.01)
  • Générer un rapport qualité sur l'effet de stock-up post-promo

Position dans la pipeline

  • Étape précédente : Step_04_Building_Model_Database
  • Étape suivante : Step_06_Building_Promo_Analysis
  • Appels distincts :
  • main_volume_effects : Calcul des effets de volume
  • main_quality_check avec paramètre : /app/outputs/{business_unit}/

Architecture technique

Flux de données

Entrée (step_3_02_model_data_baseline)
    ├── Configuration (promo_config.json)
    │   ├── Forward_buying_factors (low/mid/high)
    │   └── Halo_and_pantry_loading_computation (0/1)
    └── Données spécifiques
        ├── Galbani
        │   ├── Forward_buying_classification_Italy_galbani
        │   └── Données sell-out pour delta promo intensity
        └── Parmalat
            └── Initialisation colonnes (calcul en Step 6.01)

Traitement
    ├── 1. Uplift Volume
    │   └── Si Flag_Promo=1 : Week_total_volume - Baseline_Volume
    ├── 2. Cannibalization Volume
    │   └── Si Flag_qualified_week=1 : min(Volume - Baseline, 0)
    ├── 3. Forward Buying (Galbani uniquement)
    │   ├── Delta promo intensity sell-in vs sell-out
    │   ├── Forward buying factor par sous-catégorie
    │   └── Delta_adjusted = Delta × Factor
    └── 4. Halo/Pantry Loading
        ├── Galbani/Sell-out : calcul si paramètre=1
        └── Parmalat : initialisation NaN (Step 6.01)

Sortie
    ├── step_4_01_model_with_effects_baseline
    │   ├── Colonnes originales +
    │   ├── Uplift_Volume
    │   ├── Cannib_Volume
    │   ├── Halo_Volume
    │   ├── Pantry_Loading_Volume
    │   └── Delta_adjusted (Galbani)
    └── Quality checks/qc1_stock_up_effect_{business_unit}_{country}.csv

Concepts clés

1. Uplift Volume

  • Définition : Augmentation des ventes pendant la promotion
  • Calcul : Week_total_volume - Baseline_Volume si Flag_Promo = 1
  • Neutralisation : Si négatif → NaN (pas d'uplift négatif)

2. Cannibalization Volume

  • Définition : Réduction des ventes hors promotion
  • Calcul : min(Volume - Baseline, 0) si Flag_qualified_week = 1
  • Condition : Semaine qualifiée ET pas outlier

3. Forward Buying (Galbani uniquement)

  • Définition : Stockage anticipé par les retailers
  • Composants :
  • Delta promo intensity : différence sell-in vs sell-out
  • Forward buying factor : selon risque sous-catégorie (low/mid/high)
  • Delta adjusted : Delta × Factor

4. Halo & Pantry Loading

  • Halo : Augmentation post-promo (ventes additionnelles)
  • Pantry Loading : Diminution post-promo (stockage consommateur)
  • Calcul : Basé sur Flag_Buying_period_post_promo

Implémentation détaillée

1. Fonction principale : main_volume_effects()

def main_volume_effects():
    countries_parameters_df = get_country_specific_parameters_from_json(json_path)

    for index, row in countries_parameters_df.iterrows():
        country = row['Country']
        apply_volume_effects_computation(engine, countries_parameters_df, country)

2. Dispatcher : apply_volume_effects_computation()

Logique de sélection : - Si Sell_out_availability = 1compute_volume_effects_with_sell_out() - Sinon : - Si italy_galbanicompute_volume_effects_italy_galbani() - Si italy_parmalatcompute_volume_effects_italy_parmalat()

3. Galbani : compute_volume_effects_italy_galbani()

3.1 Calcul Forward Buying

Étape 1 : Delta promo intensity

def fetch_promo_intensities_italy_galbani():
    # Sell-in aggregation
    sell_in_query = """
    SELECT New_category, EAN, Year,
           SUM(Week_total_volume) AS Total_Volume,
           SUM(Week_promo_volume) AS Promo_Volume
    FROM step_3_01_after_scoping
    GROUP BY New_category, EAN, Year
    """

    # Sell-out aggregation (Channel spécifique)
    sell_out_query = """
    SELECT EAN, YEAR(Week_start) AS Year,
           SUM(Sales_Volumes) AS Total_Volume,
           SUM(Sales_Volume_Promo) AS Promo_Volume
    FROM {sell_out_table}
    WHERE Channel = 'IS+LSP (100-399mq) (7002)'
    GROUP BY EAN, YEAR(Week_start)
    """

    # Delta = Promo_intensity_sell_in - Promo_intensity_sell_out
    # Si négatif → 0

Gestion valeurs manquantes : 1. Cas 1 : EAN trouvé → moyenne catégorie/année 2. Cas 2 : EAN absent sell-out → mapping Sub_Category via Forward_buying_classification_Italy_galbani

Étape 2 : Forward buying factors

def fetch_forward_buying_factors_italy_galbani():
    # Mapping risque → facteur
    risk_to_factor = {
        'low': forward_buying_low_factor,    # Ex: 0.2
        'medium': forward_buying_mid_factor,  # Ex: 0.4
        'high': forward_buying_high_factor,   # Ex: 0.6
    }

    # Join avec classification par SUB_CAT_PROD_LIV2

Étape 3 : Calcul final

Delta_adjusted = Delta_sell_in_sell_out × Forward_buying_factor

4. Parmalat : compute_volume_effects_italy_parmalat()

Spécificités : - Pas de calcul forward buying - Halo/Pantry Loading initialisés à NaN - Message explicite : "Actual calculation will be done in Step 6.01" - Raison : nécessite les codes promotionnels (carryover)

5. Sell-out : compute_volume_effects_with_sell_out()

Paramètre clé : Halo_and_pantry_loading_volume_effect_computation - Si = 1 : calcul effectif - Si = 0 : colonnes initialisées à NaN

Calculs spécifiques :

# Si Flag_Buying_period_post_promo = 1 ET Flag_Outliers = 0
Halo_Volume = max(Sales_Volumes - Baseline_Volume, 0)
Pantry_Loading_Volume = min(Sales_Volumes - Baseline_Volume, 0)

Quality Check : Stock-up Effect

Fonction : quality_check_stock_up_effect()

Objectif : Analyser l'évolution des volumes post-promotion

Métriques calculées : - Base 100 : moyenne volumes semaines qualifiées - Promo : index pendant promotion - Week+1, +2, +3 : index semaines suivantes

Agrégation : Year × Brand × Category

Output CSV :

Year;Brand;Category;No_promo;Promo;Week_plus_one;Week_plus_two;Week_plus_three
2023;GALBANI;MOZZARELLA;100;150;95;98;100

Gestion des erreurs

Types d'erreurs gérés

Type Traitement Impact
Table manquante Skip + log Non bloquant
Colonnes manquantes Adaptation auto Transparent
Division par zéro Check préalable Évité
Valeurs négatives uplift → NaN Neutralisation

Points de contrôle

# Vérification existence table
if table_exists(engine, table_name):
    # Traitement
else:
    log_message(f"Table {table_name} does not exist. Skipping...")

# Adaptation colonnes volume
volume_col = 'Sales_Volumes' if 'Sales_Volumes' in df.columns else 'Week_total_volume'

Performance et optimisation

1. Requêtes SQL optimisées

  • Agrégations côté DB pour promo intensities
  • Filtrage précoce (Channel, Year)
  • GROUP BY efficaces

2. Calculs vectorisés

# Au lieu de boucles
df['Uplift_Volume'] = df.apply(lambda row: ...)

# Neutralisation vectorisée
df.loc[df['Uplift_Volume'] < 0, 'Uplift_Volume'] = np.nan

3. Gestion mémoire

  • Types explicites : astype({'Year':'int', ...})
  • Drop colonnes intermédiaires non sauvegardées

Points d'attention maintenance

1. Channel sell-out Galbani

Code en dur : 'IS+LSP (100-399mq) (7002)' - Représente le canal moderne (supermarchés moyens) - À paramétrer si extension à d'autres canaux

2. Initialisation colonnes

Toujours créer les 4 colonnes effets même si NaN : - Uplift_Volume - Cannib_Volume - Halo_Volume - Pantry_Loading_Volume

3. Cohérence nommage tables

  • Input : step_3_02_model_data_baseline
  • Output : step_4_01_model_with_effects_baseline
  • Pattern : remplacer step_3_02_ par step_4_01_model_with_effects_

4. Dépendance Step 6.01 (Parmalat)

Les vrais calculs halo/pantry loading nécessitent : - Promotion code carryover - Baseline par code promo - Exécutés dans Substep_601_forward_buying_halo_pantry_loading.py

Troubleshooting

Problème : Delta promo intensity très élevé

Symptôme : Delta_sell_in_sell_out > 0.5

Causes possibles : - Désalignement temporel sell-in/sell-out - Channel sell-out non représentatif - Promotions non trackées en sell-out

Diagnostic :

-- Vérifier cohérence volumes
SELECT Year, 
       SUM(Week_total_volume) as sell_in_total,
       SUM(Week_promo_volume) as sell_in_promo
FROM step_3_01_after_scoping
GROUP BY Year;

Problème : Beaucoup d'uplift NaN

Causes : - Baseline_Volume manquant - Flag_Promo incorrect

Vérification :

SELECT COUNT(*) as total,
       SUM(CASE WHEN Flag_Promo = 1 THEN 1 ELSE 0 END) as promo_weeks,
       SUM(CASE WHEN Baseline_Volume IS NULL THEN 1 ELSE 0 END) as null_baseline
FROM step_3_02_model_data_baseline;

Problème : Forward buying factor non trouvé

Symptôme : Colonnes Delta_adjusted avec NaN

Vérification :

-- Check mapping sous-catégories
SELECT DISTINCT Sub_Category_1, Sub_Category_2
FROM sell_in_base
WHERE CONCAT(Sub_Category_1, Sub_Category_2) NOT IN (
    SELECT CONCAT(SUB_CAT_PROD_LIV2, M7_SUB_CAT_PROD)
    FROM Forward_buying_classification_Italy_galbani
);

Exemples d'utilisation

Ajout nouveau business unit sans sell-out

def compute_volume_effects_new_country(engine, table_name, country, countries_parameters_df):
    # Calculs de base
    df['Uplift_Volume'] = ...
    df['Cannib_Volume'] = ...

    # Pas de forward buying
    df['Halo_Volume'] = np.nan
    df['Pantry_Loading_Volume'] = np.nan

    # Sauvegarder
    output_table_name = table_name.replace('step_3_02_', 'step_4_01_model_with_effects_')
    df.to_sql(output_table_name, ...)

Modification seuils forward buying

Dans promo_config.json :

{
    "Forward_buying_low_factor": 0.15,   // Au lieu de 0.2
    "Forward_buying_mid_factor": 0.35,   // Au lieu de 0.4
    "Forward_buying_high_factor": 0.55   // Au lieu de 0.6
}

Debug quality check

# Ajouter détails par retailer
results_df = pd.DataFrame(results)
# Avant agrégation finale
results_df.to_csv('debug_stock_up_details.csv')

Forcer recalcul avec sell-out

# Dans apply_volume_effects_computation()
# Forcer utilisation fonction sell-out même si sell_out_availability = 0
if country == "TEST_COUNTRY":
    compute_volume_effects_with_sell_out(engine, table_name, country, countries_parameters_df)