Aller au contenu

Détails des calculs - PT0CE

Table des matières

  1. Calcul de la marge nette
  2. Calcul des statistiques par cube
  3. Calcul des bornes de prix
  4. Algorithme de remontée hiérarchique
  5. Calcul de la sensibilité prix
  6. Exemples concrets

Calcul de la marge nette

Formule de base

Marge nette = (Prix de vente unitaire - Coût d'achat unitaire) / Prix de vente unitaire

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

  1. Protection division par zéro : Vérification QT_UF > 0 et MT_CAB > 0
  2. PAS historique : Utilisation du PAS au moment de la transaction
  3. 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 :

Borne = PAS_actif / (1 - P)

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

Exemple 4 : Sensibilité prix

Article dans cube :
- Nombre commandes = 250
- CA article = 50 000€
- Seuil fréquence cube = 180 commandes
- CA total cube = 200 000€

Calcul :
1. Fréquence : 250 > 180 → F1
2. % CA : 50k/200k = 25% (< 70%) → S1
3. Sensibilité : F1 × S1 → HIGH