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 volumemain_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
siFlag_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)
siFlag_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 = 1
→ compute_volume_effects_with_sell_out()
- Sinon :
- Si italy_galbani
→ compute_volume_effects_italy_galbani()
- Si italy_parmalat
→ compute_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
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_
parstep_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')