diff --git a/src/features/ai-copilot/business/analysis-engine.ts b/src/features/ai-copilot/business/analysis-engine.ts index 4afd3ed..4cb2de0 100644 --- a/src/features/ai-copilot/business/analysis-engine.ts +++ b/src/features/ai-copilot/business/analysis-engine.ts @@ -38,8 +38,10 @@ IMPORTANT : La gamme C est RÉSERVÉE aux produits saisonniers et gérée MANUEL — Quadrant STAR ⭐ : Fort signal positif → A sauf inactivité ≥ 3 mois. — Quadrant TRAFIC 🚶 : Signal positif SEULEMENT si poids QTÉ fournisseur > 1%. Si poids < 1% et score < 40 → Z. - — Quadrant MARGE 💎 : Signal positif SEULEMENT si poids CA fournisseur > 0.5%. - Si poids < 0.5% et score < 35 → Z. + — Quadrant MARGE 💎 : Ce produit contribue par la rentabilité, pas le volume. C'est son rôle attendu. + Si percentileMarge >= 70 → A (capital rentabilité du lot, protéger absolument). + Si percentileMarge < 70 ET poids CA fournisseur < 0.5% ET score < 35 → Z (marginal ET sous-performant). + Sinon → A si actif (inactivité < 3 mois), Z si inactif ≥ 3 mois. — Quadrant WATCH ⚠️ : Signal négatif. Si poids CA rayon > 5% ou poids QTÉ rayon > 5% → A (surveiller). Sinon → Z si inactivité ≥ 2 mois, A si actif. diff --git a/src/features/ai-copilot/business/context-profiler.ts b/src/features/ai-copilot/business/context-profiler.ts index d4d90ac..6439af2 100644 --- a/src/features/ai-copilot/business/context-profiler.ts +++ b/src/features/ai-copilot/business/context-profiler.ts @@ -223,33 +223,45 @@ export class ContextProfiler { ? Math.round((getWeightedQty(target) / totalQtyFournisseur) * 1000) / 10 : 0; - const isLowContribution = weightCaFournisseur < 0.5 && weightQtyFournisseur < 0.5; + // Bug fix : un produit dans le top 30% de marge du lot apporte une valeur + // réelle en rentabilité. L'exclure du signal isLowContribution évite de le + // pénaliser uniquement parce que son volume/CA est faible (profil 💎 pur). + const isLowContribution = weightCaFournisseur < 0.5 && weightQtyFournisseur < 0.5 && pMarge <= 70; + + // 6. Signaux Trafic / Marge + Quadrant (sur valeurs PAR MAGASIN) + // Médianes calculées ici pour être disponibles dans les sections 6 ET 7. + const medianQtyPerStore = computeMedian(allQtyPerStore); + const medianMarge = computeMedian(allMargeValues); - // 6. Signaux Trafic / Marge (sur valeurs PAR MAGASIN) const rayonSizeForSignals = rayonProds.length; const signalsActive = rayonSizeForSignals >= MIN_RAYON_SIZE; let isHighVolumeWithLowMargin = false; let isMargePure = false; - if (signalsActive) { + // Bug fix : isMargePure est désormais calculé sur tous les rayons, même petits. + // Raison : la marge absolue est une donnée fiable quelle que soit la taille du lot. + // Le signal isHighVolumeWithLowMargin (trafic) garde le seuil MIN_RAYON_SIZE car + // il nécessite une distribution volumique suffisante pour être statistiquement valide. + { const marge40 = valueAtPercentile(allMargeValues, 40); - const marge70 = valueAtPercentile(allMargeValues, 70); const qty60PerStore = valueAtPercentile(allQtyPerStore, 60); - const medianQtyPerStore = computeMedian(allQtyPerStore); - isHighVolumeWithLowMargin = - targetQtyPerStore >= qty60PerStore && - (target.tauxMarge ?? 0) < marge40; + // Signal trafic : nécessite un rayon suffisamment large + if (signalsActive) { + isHighVolumeWithLowMargin = + targetQtyPerStore >= qty60PerStore && + (target.tauxMarge ?? 0) < marge40; + } + // Signal marge pure : au-dessus de la médiane marge + volume faible. + // Utilise la médiane (P50) plutôt que P70 pour être robuste sur petits lots. isMargePure = - (target.tauxMarge ?? 0) >= marge70 && + (target.tauxMarge ?? 0) > medianMarge && targetQtyPerStore < medianQtyPerStore; } - // 7. Quadrant (basé sur les médianes PAR MAGASIN) - const medianQtyPerStore = computeMedian(allQtyPerStore); - const medianMarge = computeMedian(allMargeValues); + // 7. Quadrant (basé sur les médianes PAR MAGASIN — déjà calculées en section 6) const { quadrant, quadrantLabel, quadrantEmoji } = ContextProfiler.resolveQuadrant( targetQtyPerStore, target.tauxMarge ?? 0,