Détails des calculs - PT0CE¶
Table des matières¶
- Calcul de la marge nette
- Calcul des statistiques par cube
- Calcul des bornes de prix
- Algorithme de remontée hiérarchique
- Calcul de la sensibilité prix
- Exemples concrets
Calcul de la marge nette¶
Formule de base¶
Implémentation SQL¶
-- Dans extract_master_data.py
CASE
WHEN b.QT_UF > 0 AND b.MT_CAB > 0 AND b.PAS_NATIONAL_UNITAIRE IS NOT NULL
THEN ((b.MT_CAB / b.QT_UF) - b.PAS_NATIONAL_UNITAIRE) / (b.MT_CAB / b.QT_UF)
ELSE 0
END AS MARGE_NETTE
Points importants¶
- Protection division par zéro : Vérification QT_UF > 0 et MT_CAB > 0
- PAS historique : Utilisation du PAS au moment de la transaction
- Valeur par défaut : 0 si calcul impossible
Filtrage ZOOM1¶
-- Exclusion des marges négatives pour ZOOM1
WHERE (UNIVERS <> 'ZOOM1'
OR (UNIVERS = 'ZOOM1' AND CAB_UNITAIRE >= PAS_NATIONAL_UNITAIRE))
Calcul des statistiques par cube¶
Métriques calculées¶
Pour chaque cube (groupe de transactions) :
def calculate_margin_statistics(series):
return pd.Series({
'PERCENTILE_10': series.quantile(0.10),
'PERCENTILE_30': series.quantile(0.30),
'PERCENTILE_40': series.quantile(0.40),
'PERCENTILE_50': series.quantile(0.50), # Médiane
'PERCENTILE_60': series.quantile(0.60),
'PERCENTILE_80': series.quantile(0.80),
'PERCENTILE_90': series.quantile(0.90),
'ECART_TYPE': series.std(),
'MARGE_MIN': series.min(),
'MARGE_MAX': series.max(),
'DISTINCT_MARGINS': series.nunique()
})
Agrégations de base¶
basic_aggs = {
'MT_CAB': 'sum', # CA total
'MT_GM4': 'sum', # Marge brute 4
'QT_KG': 'sum', # Volume total
'ID_FAC': 'count' # Nombre de transactions
}
Seuil de fiabilité¶
- min_distinct_margins = 30 (par défaut)
- Un cube doit avoir au moins 30 valeurs de marge distinctes
- Sinon → remontée hiérarchique nécessaire
Calcul des bornes de prix¶
Formule générale¶
Pour chaque percentile P de marge :
Correspondance bornes/percentiles¶
| Borne | Percentile | Formule | Interprétation |
|---|---|---|---|
| PL1/PL2 | P90 | PAS / (1 - 0.90) | Prix pour marge ≥ 90% |
| PL2/PL3 | P80 | PAS / (1 - 0.80) | Prix pour marge ≥ 80% |
| PL3/PL4 | P60 | PAS / (1 - 0.60) | Prix pour marge ≥ 60% |
| PL4/PL5 | P50 | PAS / (1 - 0.50) | Prix pour marge ≥ 50% |
| PL5/PL6 | P30 | PAS / (1 - 0.30) | Prix pour marge ≥ 30% |
| PL6/PLX | P10 | PAS / (1 - 0.10) | Prix pour marge ≥ 10% |
Application des contraintes¶
# 1. Calcul de base
calculated_price = pas_actif / (1 - percentile)
# 2. Contrainte minimale
calculated_price = max(calculated_price, pas_actif)
# 3. Contrainte maximale
if prb_to_use == 1:
calculated_price = min(calculated_price, prb_rc)
elif prb_to_use == 2:
calculated_price = min(calculated_price, prb_coll)
# 4. Résultat final
borne_finale = calculated_price
Calcul vectorisé (numpy)¶
# Pour toutes les bornes en une fois
valid_mask = (pas_actif > 0) & (percentiles < 1)
# Calcul en masse
calculated_prices = np.where(
valid_mask,
pas_actif / (1 - percentiles),
np.nan
)
# Contraintes en masse
calculated_prices = np.maximum(calculated_prices, pas_actif)
calculated_prices = np.minimum(calculated_prices, prb_max)
Calcul des écarts¶
# Pour chaque borne
ecart = borne - pas_actif
# Exemple
ECART_PL1_PL2_PAS = BORNE_PL1_PL2 - PAS_ACTIF
Algorithme de remontée hiérarchique¶
Vue d'ensemble¶
def find_statistics_for_cube(cube, grouped_data, min_margins=30):
# Cubes NATIONAL : pas de remontée
if cube.type == 'NATIONAL':
return -1, cube.statistics
# Cubes MASTER : recherche dans hiérarchie
for level in range(0, max_levels):
key = build_key_for_level(cube, level)
stats = grouped_data[level].get(key)
if stats and stats['nunique'] >= min_margins:
return level + 1, stats
# Aucune donnée trouvée
return max_levels + 1, None
Construction des clés par niveau¶
ZOOM1/ZOOM2 (21 niveaux)¶
# Niveau 1-3 : ID_ART
Level 1: (ID_ART, TYPE_CLIENT, TYPE_RESTAURANT, GEO)
Level 2: (ID_ART, TYPE_CLIENT, TYPE_RESTAURANT)
Level 3: (ID_ART, TYPE_CLIENT)
# Niveau 4-6 : HIE_N6
Level 4: (HIE_N6, TYPE_CLIENT, TYPE_RESTAURANT, GEO)
Level 5: (HIE_N6, TYPE_CLIENT, TYPE_RESTAURANT)
Level 6: (HIE_N6, TYPE_CLIENT)
# ... jusqu'à HIE_N1 (niveau 19-21)
ZOOM3 (14 niveaux)¶
# Même principe mais sans GEO
Level 1: (ID_ART, TYPE_CLIENT, TYPE_RESTAURANT)
Level 2: (ID_ART, TYPE_CLIENT)
# ...
Level 13: (HIE_N1, TYPE_CLIENT, TYPE_RESTAURANT)
Level 14: (HIE_N1, TYPE_CLIENT)
Optimisation : pré-calcul des clés¶
# Au lieu de reconstruire pour chaque cube
level_keys = {}
for level_idx, level_dims in enumerate(granularity_levels):
# Construction vectorisée
arrays_for_level = [cols_data[dim] for dim in level_dims]
keys = list(zip(*arrays_for_level))
level_keys[level_idx] = keys
# Utilisation directe
for idx, cube_idx in enumerate(all_cubes):
for level_idx in range(0, max_levels):
key = level_keys[level_idx][idx]
# Lookup direct
Calcul de la sensibilité prix¶
Étape 1 : Métriques par article¶
# Pour chaque cube
metrics = data.groupby(cube_dimensions + ['ID_ART']).agg({
'ID_FAC': 'nunique', # Nombre de commandes
'MT_CAN': 'sum', # CA total article
'MT_CAB': 'sum' # CA brut
})
# Ratio de fréquence
metrics['FREQUENCY_RATIO'] = metrics['NB_ORDERS'] / cube_total_orders
Étape 2 : Classification fréquence¶
# Seuil = 75e percentile (top 25%)
frequency_threshold = metrics['FREQUENCY_RATIO'].quantile(0.75)
# Classification
if frequency_ratio >= frequency_threshold:
frequency_class = 'F1'
else:
frequency_class = 'F2'
Étape 3 : Classification Pareto (ventes)¶
# Tri par CA décroissant
cube_sorted = cube_data.sort_values('TOTAL_SALES', ascending=False)
# Calcul cumulatif
cube_sorted['CUMULATIVE_SALES'] = cube_sorted['TOTAL_SALES'].cumsum()
cube_sorted['PCT_CUMULATIVE'] = (
cube_sorted['CUMULATIVE_SALES'] /
cube_sorted['TOTAL_SALES'].sum()
)
# Classification (seuil 70%)
if pct_cumulative <= 0.70:
sales_class = 'S1' # Top 70% du CA
else:
sales_class = 'S2' # Reste
Étape 4 : Attribution sensibilité¶
sensitivity_matrix = {
('F1', 'S1'): 'HIGH', # Fréquent ET gros CA
('F1', 'S2'): 'MEDIUM', # Fréquent mais petit CA
('F2', 'S1'): 'MEDIUM', # Rare mais gros CA
('F2', 'S2'): 'LOW' # Rare ET petit CA
}
price_sensitivity = sensitivity_matrix[(frequency_class, sales_class)]
Exemples concrets¶
Exemple 1 : Calcul marge nette¶
Transaction :
- MT_CAB = 1000€
- QT_UF = 50 unités
- PAS historique = 15€/unité
Calcul :
- Prix unitaire = 1000 / 50 = 20€
- Marge = (20 - 15) / 20 = 0.25 = 25%
Exemple 2 : Calcul borne PL1/PL2¶
Données :
- PAS actif = 10€
- Percentile 90 = 0.30 (30% de marge)
- PRB_RC = 14€
- PRB_TO_USE = 1
Calcul :
1. Borne théorique = 10 / (1 - 0.30) = 10 / 0.70 = 14.29€
2. Contrainte min : max(14.29, 10) = 14.29€
3. Contrainte max : min(14.29, 14) = 14€
4. Borne finale PL1/PL2 = 14€
Exemple 3 : Remontée hiérarchique¶
Cube MASTER :
- ID_ART = 'ART123'
- TYPE_CLIENT = 'RCI PI GI'
- TYPE_RESTAURANT = 'REST. TRADI'
- GEO = 'IDF'
- DISTINCT_MARGINS = 15 (< 30)
Recherche :
1. Niveau 1 : (ART123, RCI PI GI, REST. TRADI, IDF) → 15 marges ❌
2. Niveau 2 : (ART123, RCI PI GI, REST. TRADI) → 45 marges ✓
3. SOURCE_LEVEL = 2