"""
Module: Naive Bayes
Categorie: Supervised Classification
Difficulte: Debutant

Genere depuis la plateforme ML Formation
"""

# Imports
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, mean_squared_error, r2_score

# Charger le dataset
df = pd.read_csv('binary_classification.csv')

# Explorer les donnees
# Type: Code executable
print("=" * 70)
print("       EXPLORATION DES DONNEES POUR NAIVE BAYES")
print("=" * 70)

print("\n" + "=" * 70)
print("1. APERCU DU DATASET")
print("=" * 70)
print("""
Nous avons un dataset de classification binaire.
Naive Bayes va apprendre les distributions de probabilite de chaque
feature pour chaque classe, puis utiliser le theoreme de Bayes
pour classifier de nouvelles observations.
""")
display(df.head(10), title="Dataset de Classification")

print("\n" + "=" * 70)
print("2. DISTRIBUTION DES CLASSES (PROBABILITES A PRIORI)")
print("=" * 70)
print("""
Les probabilites a priori P(classe) representent la frequence
de chaque classe dans les donnees d'entrainement.
""")
class_counts = df['label'].value_counts().sort_index()
total = len(df)
print("Distribution des classes:")
print("-" * 40)
for label, count in class_counts.items():
    prior = count / total
    print(f"  Classe {label}: {count:4d} observations ({prior:.1%})")
    print(f"            → P(Classe {label}) = {prior:.3f}")

# Verifier l'equilibre
balance_ratio = class_counts.min() / class_counts.max()
print(f"\n  Ratio d'equilibre: {balance_ratio:.2f}")
if balance_ratio > 0.8:
    print("  → Classes bien equilibrees ✓")
elif balance_ratio > 0.5:
    print("  → Leger desequilibre (acceptable)")
else:
    print("  → Desequilibre important - attention aux metriques!")

print("\n" + "=" * 70)
print("3. STATISTIQUES PAR CLASSE")
print("=" * 70)
print("""
Naive Bayes Gaussien apprend la moyenne et la variance de chaque
feature pour chaque classe. Ces statistiques sont cruciales!
""")
display(df.groupby('label').describe().round(3), title="Statistiques par classe")

print("\n" + "-" * 40)
print("INTERPRETATION DES STATISTIQUES:")
print("-" * 40)
for feature in ['feature1', 'feature2']:
    mean_0 = df[df['label'] == 0][feature].mean()
    mean_1 = df[df['label'] == 1][feature].mean()
    std_0 = df[df['label'] == 0][feature].std()
    std_1 = df[df['label'] == 1][feature].std()
    separation = abs(mean_1 - mean_0) / ((std_0 + std_1) / 2)

    print(f"\n  {feature}:")
    print(f"    Classe 0: moyenne={mean_0:.2f}, ecart-type={std_0:.2f}")
    print(f"    Classe 1: moyenne={mean_1:.2f}, ecart-type={std_1:.2f}")
    print(f"    Separation des classes: {separation:.2f}")
    if separation > 1.5:
        print(f"    → Bonne separation - feature discriminante!")
    elif separation > 0.5:
        print(f"    → Separation moderee - feature utile")
    else:
        print(f"    → Faible separation - feature peu discriminante")

print("\n" + "=" * 70)
print("RESUME")
print("=" * 70)
print(f"""
Naive Bayes va utiliser:
• Les probabilites a priori (frequence des classes)
• La moyenne et variance de chaque feature par classe

Pour predire, il calcule P(classe|features) avec Bayes!
""")


# Visualiser la distribution des features
# Type: Code executable
print("=" * 70)
print("    VISUALISATION DES DISTRIBUTIONS PAR CLASSE")
print("=" * 70)
print("""
Naive Bayes Gaussien suppose que chaque feature suit une
distribution normale (gaussienne) pour chaque classe.

Visualisons si cette hypothese est raisonnable!
""")

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Feature 1 par classe
for label in [0, 1]:
    data = df[df['label'] == label]['feature1']
    axes[0].hist(data, bins=15, alpha=0.6, label=f'Classe {label}',
                color='#9B7AC4' if label == 0 else '#F7E64D')
axes[0].set_xlabel('Feature 1')
axes[0].set_ylabel('Frequence')
axes[0].set_title('Distribution de Feature 1 par classe')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Feature 2 par classe
for label in [0, 1]:
    data = df[df['label'] == label]['feature2']
    axes[1].hist(data, bins=15, alpha=0.6, label=f'Classe {label}',
                color='#9B7AC4' if label == 0 else '#F7E64D')
axes[1].set_xlabel('Feature 2')
axes[1].set_ylabel('Frequence')
axes[1].set_title('Distribution de Feature 2 par classe')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("\n" + "=" * 70)
print("INTERPRETATION DES DISTRIBUTIONS")
print("=" * 70)

print("""
Ce que nous observons:

1. FORME DES DISTRIBUTIONS:
   → Si les histogrammes ressemblent a des cloches, l'hypothese
      gaussienne est raisonnable
   → Des distributions asymetriques peuvent reduire la precision

2. CHEVAUCHEMENT DES CLASSES:
   → Peu de chevauchement = classification plus facile
   → Beaucoup de chevauchement = zone d'incertitude importante

3. CENTRES DES DISTRIBUTIONS:
   → Plus les moyennes sont eloignees, plus les classes sont separables
   → C'est ce que Naive Bayes utilise pour decider!
""")

# Calculer le chevauchement approximatif
for feature in ['feature1', 'feature2']:
    data_0 = df[df['label'] == 0][feature]
    data_1 = df[df['label'] == 1][feature]

    overlap_min = max(data_0.min(), data_1.min())
    overlap_max = min(data_0.max(), data_1.max())

    if overlap_max > overlap_min:
        total_range = max(data_0.max(), data_1.max()) - min(data_0.min(), data_1.min())
        overlap_pct = (overlap_max - overlap_min) / total_range * 100
        print(f"\n  {feature}: {overlap_pct:.0f}% de chevauchement entre classes")
    else:
        print(f"\n  {feature}: Pas de chevauchement - separation parfaite!")

print("\n" + "-" * 40)
print("CONCLUSION")
print("-" * 40)
print("""
Naive Bayes suppose que chaque feature suit une distribution
gaussienne par classe. Meme si cette hypothese n'est pas
parfaitement respectee, l'algorithme fonctionne souvent bien!
""")


# Entrainer Gaussian Naive Bayes
# Type: Code executable
from sklearn.naive_bayes import GaussianNB

print("=" * 70)
print("         ENTRAINEMENT DE GAUSSIAN NAIVE BAYES")
print("=" * 70)

# Preparer les donnees
X = df[['feature1', 'feature2']].values
y = df['label'].values
feature_names = ['feature1', 'feature2']

print("\n" + "=" * 70)
print("1. PREPARATION DES DONNEES")
print("=" * 70)
print(f"""
Dimensions des donnees:
  • X (features): {X.shape[0]} echantillons x {X.shape[1]} features
  • y (labels): {y.shape[0]} etiquettes
""")

# Division train/test
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

print(f"  Division train/test (80/20):")
print(f"    • Train: {len(X_train)} echantillons")
print(f"    • Test:  {len(X_test)} echantillons")

print("\n" + "=" * 70)
print("2. ENTRAINEMENT DU MODELE")
print("=" * 70)
print("""
GaussianNB apprend pour chaque classe:
  • La probabilite a priori P(classe) - frequence dans les donnees
  • La moyenne (theta) de chaque feature
  • La variance de chaque feature
""")

import time
start_time = time.time()

# Creer et entrainer le modele
nb = GaussianNB()
nb.fit(X_train, y_train)

training_time = (time.time() - start_time) * 1000

print(f"  Entrainement termine en {training_time:.2f} ms !")
print(f"  (Naive Bayes est extremement rapide!)")

print("\n" + "=" * 70)
print("3. PARAMETRES APPRIS PAR LE MODELE")
print("=" * 70)

print("\n  a) PROBABILITES A PRIORI P(classe):")
print("  " + "-" * 38)
for i, prior in enumerate(nb.class_prior_):
    bar = "█" * int(prior * 30)
    print(f"    Classe {i}: P = {prior:.4f}  {bar}")

print("""
Interpretation:
  → Ces probabilites representent la frequence de chaque classe
  → Si P(1) = 0.6, alors 60% des donnees sont de classe 1
  → A probabilites egales, le modele est neutre a priori
""")

print("\n  b) MOYENNES PAR CLASSE (theta):")
print("  " + "-" * 38)
for i, means in enumerate(nb.theta_):
    print(f"    Classe {i}: {dict(zip(feature_names, means.round(4)))}")

print("""
Interpretation:
  → Le modele a appris le "centre" de chaque classe
  → Une nouvelle observation est comparee a ces centres
  → Plus elle est proche d'un centre, plus probable est la classe
""")

print("\n  c) VARIANCES PAR CLASSE:")
print("  " + "-" * 38)
for i, variances in enumerate(nb.var_):
    print(f"    Classe {i}: {dict(zip(feature_names, variances.round(4)))}")

print("""
Interpretation:
  → La variance mesure l'etalement des donnees autour de la moyenne
  → Faible variance = classe compacte, predictions plus confiantes
  → Forte variance = classe dispersee, plus d'incertitude
""")

print("\n" + "=" * 70)
print("RESUME DE L'ENTRAINEMENT")
print("=" * 70)
print(f"""
Modele entraine avec succes!

Naive Bayes a appris:
  • {len(nb.classes_)} classes differentes
  • {X.shape[1]} distributions gaussiennes par classe
  • Total: {len(nb.classes_) * X.shape[1]} paires (moyenne, variance)

Pour predire: P(classe|X) ∝ P(classe) × ∏ P(feature|classe)
""")


# Evaluer et predire des probabilites
# Type: Code executable
from sklearn.naive_bayes import GaussianNB
from sklearn.metrics import confusion_matrix

print("=" * 70)
print("       EVALUATION ET PREDICTIONS PROBABILISTES")
print("=" * 70)

# Preparation et entrainement
X = df[['feature1', 'feature2']].values
y = df['label'].values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

nb = GaussianNB()
nb.fit(X_train, y_train)

# Predictions
y_pred = nb.predict(X_test)
y_proba = nb.predict_proba(X_test)

print("\n" + "=" * 70)
print("1. METRIQUES DE PERFORMANCE")
print("=" * 70)

acc = accuracy_score(y_test, y_pred)
print(f"""
ACCURACY: {acc:.2%}

Interpretation:
  → {acc:.2%} des predictions sont correctes
  → Benchmarks pour la classification binaire:
    • > 90%: Excellent
    • 80-90%: Bon
    • 70-80%: Acceptable
    • < 70%: A ameliorer
""")

print("\n" + "=" * 70)
print("2. RAPPORT DE CLASSIFICATION DETAILLE")
print("=" * 70)
print(classification_report(y_test, y_pred))

print("-" * 40)
print("Guide d'interpretation:")
print("-" * 40)
print("""
• Precision: parmi les predictions positives, combien sont correctes?
  → Important si les faux positifs coutent cher

• Recall: parmi les vrais positifs, combien sont detectes?
  → Important si manquer un positif coute cher

• F1-score: moyenne harmonique de precision et recall
  → Bon compromis entre les deux
""")

print("\n" + "=" * 70)
print("3. MATRICE DE CONFUSION")
print("=" * 70)
cm = confusion_matrix(y_test, y_pred)
print(f"""
                  Predictions
               Classe 0  Classe 1
Reel Classe 0    {cm[0,0]:4d}      {cm[0,1]:4d}
     Classe 1    {cm[1,0]:4d}      {cm[1,1]:4d}
""")

tn, fp, fn, tp = cm.ravel()
print(f"  • Vrais Negatifs (TN):  {tn:4d} - Classe 0 correctement predite")
print(f"  • Faux Positifs (FP):   {fp:4d} - Classe 0 predite comme 1")
print(f"  • Faux Negatifs (FN):   {fn:4d} - Classe 1 predite comme 0")
print(f"  • Vrais Positifs (TP):  {tp:4d} - Classe 1 correctement predite")

print("\n" + "=" * 70)
print("4. EXEMPLES DE PREDICTIONS PROBABILISTES")
print("=" * 70)
print("""
L'avantage de Naive Bayes: il donne des PROBABILITES!
On peut voir a quel point le modele est sur de lui.
""")
print("-" * 60)
print(f"{'Idx':^5} | {'Feature1':^8} | {'Feature2':^8} | {'P(0)':^7} | {'P(1)':^7} | {'Pred':^5} | {'Reel':^5} | {'OK?':^4}")
print("-" * 60)

n_examples = min(10, len(X_test))
for i in range(n_examples):
    p0, p1 = y_proba[i]
    pred = y_pred[i]
    real = y_test[i]
    correct = "✓" if pred == real else "✗"
    confidence = max(p0, p1)

    # Indicateur de confiance
    if confidence > 0.9:
        conf_ind = "très sûr"
    elif confidence > 0.7:
        conf_ind = "sûr"
    else:
        conf_ind = "incertain"

    print(f"{i:^5} | {X_test[i,0]:^8.2f} | {X_test[i,1]:^8.2f} | {p0:^7.3f} | {p1:^7.3f} | {pred:^5} | {real:^5} | {correct:^4}")

print("-" * 60)

# Analyser la confiance
confidences = np.max(y_proba, axis=1)
print(f"""
ANALYSE DE LA CONFIANCE:
  • Confiance moyenne: {confidences.mean():.1%}
  • Confiance min:     {confidences.min():.1%}
  • Confiance max:     {confidences.max():.1%}

  • Predictions tres sures (>90%): {(confidences > 0.9).sum()} / {len(confidences)}
  • Predictions incertaines (<60%): {(confidences < 0.6).sum()} / {len(confidences)}
""")

print("\n" + "=" * 70)
print("CONCLUSION")
print("=" * 70)
print(f"""
Naive Bayes donne des probabilites interpretables!

• Accuracy globale: {acc:.2%}
• Avantage: on peut ajuster le seuil de decision
  selon le cout des erreurs (voir exercice)
""")


# Visualiser la frontiere de decision
# Type: Code executable
from sklearn.naive_bayes import GaussianNB

print("=" * 70)
print("    VISUALISATION DE LA FRONTIERE DE DECISION")
print("=" * 70)
print("""
La frontiere de decision est la ligne ou P(classe 0) = P(classe 1).
De chaque cote, une classe est plus probable.
""")

# Entrainement
X = df[['feature1', 'feature2']].values
y = df['label'].values
nb = GaussianNB()
nb.fit(X, y)

# Creer une grille
margin = 1
xx, yy = np.meshgrid(
    np.linspace(X[:, 0].min() - margin, X[:, 0].max() + margin, 200),
    np.linspace(X[:, 1].min() - margin, X[:, 1].max() + margin, 200)
)
Z = nb.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)

# Obtenir aussi les probabilites pour la colormap
Z_proba = nb.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1]
Z_proba = Z_proba.reshape(xx.shape)

# Visualisation
fig, axes = plt.subplots(1, 2, figsize=(15, 6))

# Plot 1: Frontiere de decision binaire
axes[0].contourf(xx, yy, Z, alpha=0.3, cmap='coolwarm')
axes[0].contour(xx, yy, Z, colors='black', linewidths=2, levels=[0.5])
axes[0].scatter(X[y == 0, 0], X[y == 0, 1],
           c='#9B7AC4', label='Classe 0', alpha=0.7, s=60)
axes[0].scatter(X[y == 1, 0], X[y == 1, 1],
           c='#F7E64D', label='Classe 1', alpha=0.7, s=60, edgecolors='black')
axes[0].set_xlabel('Feature 1')
axes[0].set_ylabel('Feature 2')
axes[0].set_title('Frontiere de Decision Naive Bayes')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Plot 2: Carte de probabilite
im = axes[1].contourf(xx, yy, Z_proba, levels=20, cmap='RdYlGn', alpha=0.8)
axes[1].contour(xx, yy, Z_proba, colors='black', linewidths=1, levels=[0.5])
axes[1].scatter(X[y == 0, 0], X[y == 0, 1],
           c='#9B7AC4', label='Classe 0', alpha=0.7, s=60)
axes[1].scatter(X[y == 1, 0], X[y == 1, 1],
           c='#F7E64D', label='Classe 1', alpha=0.7, s=60, edgecolors='black')
axes[1].set_xlabel('Feature 1')
axes[1].set_ylabel('Feature 2')
axes[1].set_title('Carte de Probabilite P(Classe 1)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.colorbar(im, ax=axes[1], label='P(Classe 1)')

plt.tight_layout()
plt.show()

print("\n" + "=" * 70)
print("INTERPRETATION DES VISUALISATIONS")
print("=" * 70)
print("""
GRAPHIQUE DE GAUCHE - Frontiere de decision:
  • La ligne noire separe les deux classes
  • Zone bleue: le modele predit classe 0
  • Zone rouge: le modele predit classe 1
  • Frontiere courbee? Naive Bayes peut creer des frontieres
    non-lineaires grace aux distributions gaussiennes!

GRAPHIQUE DE DROITE - Carte de probabilite:
  • Montre P(classe 1) sur tout l'espace
  • Rouge fonce: P(1) proche de 1 (tres sur que c'est classe 1)
  • Vert fonce: P(1) proche de 0 (tres sur que c'est classe 0)
  • Jaune: P(1) proche de 0.5 (zone d'incertitude)
""")

# Identifier les points mal classes
y_pred = nb.predict(X)
misclassified = np.sum(y_pred != y)
print(f"\n  Points mal classes: {misclassified} / {len(y)} ({misclassified/len(y):.1%})")
print("  → Ces points sont generalement dans la zone de chevauchement")


# Comparer avec d'autres algorithmes
# Type: Code executable
from sklearn.naive_bayes import GaussianNB
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
import time

print("=" * 70)
print("    COMPARAISON NAIVE BAYES vs AUTRES ALGORITHMES")
print("=" * 70)
print("""
Comparons Naive Bayes avec d'autres classifieurs classiques.
Focus sur: Accuracy, temps d'entrainement, et interpretabilite.
""")

# Preparation
X = df[['feature1', 'feature2']].values
y = df['label'].values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Comparer les modeles
models = {
    'Naive Bayes': GaussianNB(),
    'Logistic Regression': LogisticRegression(random_state=42),
    'Decision Tree': DecisionTreeClassifier(max_depth=5, random_state=42),
    'SVM (RBF)': SVC(kernel='rbf', random_state=42)
}

print("\n" + "=" * 70)
print("RESULTATS DE LA COMPARAISON")
print("=" * 70)
print(f"\n{'Modele':<22} | {'Accuracy':^10} | {'Temps (ms)':^12} | {'Rang':^6}")
print("-" * 60)

results = []
for name, model in models.items():
    start = time.time()
    model.fit(X_train, y_train)
    train_time = (time.time() - start) * 1000

    acc = accuracy_score(y_test, model.predict(X_test))
    results.append({
        'name': name,
        'accuracy': acc,
        'time': train_time
    })

# Trier par accuracy
results_sorted = sorted(results, key=lambda x: x['accuracy'], reverse=True)
for rank, r in enumerate(results_sorted, 1):
    print(f"{r['name']:<22} | {r['accuracy']:^10.2%} | {r['time']:^12.3f} | {rank:^6}")

# Trouver le plus rapide
fastest = min(results, key=lambda x: x['time'])

print("\n" + "-" * 60)
print("ANALYSE COMPARATIVE")
print("-" * 60)

nb_result = next(r for r in results if r['name'] == 'Naive Bayes')
best_result = results_sorted[0]

print(f"""
NAIVE BAYES:
  • Accuracy: {nb_result['accuracy']:.2%}
  • Position: #{next(i+1 for i, r in enumerate(results_sorted) if r['name'] == 'Naive Bayes')} / {len(results)}
  • Temps: {nb_result['time']:.3f} ms

OBSERVATIONS:
  • Modele le plus rapide: {fastest['name']} ({fastest['time']:.3f} ms)
  • Meilleure accuracy: {best_result['name']} ({best_result['accuracy']:.2%})
""")

# Visualisation comparative
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

names = [r['name'] for r in results]
accuracies = [r['accuracy'] for r in results]
times = [r['time'] for r in results]

# Accuracy
colors = ['#9B7AC4' if n == 'Naive Bayes' else '#C09CF0' for n in names]
bars1 = axes[0].bar(names, accuracies, color=colors, edgecolor='black')
axes[0].set_ylabel('Accuracy')
axes[0].set_title('Comparaison des Accuracy')
axes[0].set_ylim(0, 1)
axes[0].tick_params(axis='x', rotation=15)
for bar, acc in zip(bars1, accuracies):
    axes[0].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,
                f'{acc:.1%}', ha='center', fontsize=10)
axes[0].grid(True, alpha=0.3, axis='y')

# Temps
bars2 = axes[1].bar(names, times, color=colors, edgecolor='black')
axes[1].set_ylabel('Temps (ms)')
axes[1].set_title('Temps d\'entrainement')
axes[1].tick_params(axis='x', rotation=15)
for bar, t in zip(bars2, times):
    axes[1].text(bar.get_x() + bar.get_width()/2, bar.get_height() + max(times)*0.02,
                f'{t:.2f}', ha='center', fontsize=10)
axes[1].grid(True, alpha=0.3, axis='y')

plt.tight_layout()
plt.show()

print("\n" + "=" * 70)
print("QUAND CHOISIR NAIVE BAYES?")
print("=" * 70)
print("""
AVANTAGES de Naive Bayes:
  ✓ Extremement rapide (entrainement et prediction)
  ✓ Peu de donnees suffisent
  ✓ Donne des probabilites interpretables
  ✓ Pas d'hyperparametres a regler
  ✓ Fonctionne bien en haute dimension

INCONVENIENTS:
  ✗ Hypothese d'independance rarement vraie
  ✗ Peut etre moins precis que des modeles complexes
  ✗ Sensible aux features non pertinentes

CAS D'USAGE IDEAUX:
  • Classification de texte (spam, sentiment)
  • Systemes de recommandation
  • Diagnostic medical rapide
  • Baseline pour comparer d'autres modeles
""")


# Exercice: Classification avec seuil personnalise
# Type: Exercice
# Exercice: Modifiez le seuil de decision

from sklearn.naive_bayes import GaussianNB

# Par defaut, Naive Bayes predit la classe avec P > 0.5
# Mais parfois on veut etre plus ou moins strict

# Preparez les donnees
X = df[['feature1', 'feature2']].values
y = df['label'].values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

nb = GaussianNB()
nb.fit(X_train, y_train)

# Obtenez les probabilites
y_proba = nb.predict_proba(X_test)

# TODO: Creez une fonction qui predit classe 1 si P(1) > seuil
# TODO: Testez differents seuils (0.3, 0.5, 0.7)
# TODO: Calculez precision et recall pour chaque seuil


# Interpretation probabiliste complete
# Type: Code executable
from sklearn.naive_bayes import GaussianNB

print("=" * 70)
print("    INTERPRETATION PROBABILISTE COMPLETE")
print("=" * 70)
print("""
L'un des grands avantages de Naive Bayes est son interpretabilite.
On peut comprendre POURQUOI le modele fait une prediction.
""")

# Entrainement
X = df[['feature1', 'feature2']].values
y = df['label'].values
feature_names = ['feature1', 'feature2']

nb = GaussianNB()
nb.fit(X, y)

print("\n" + "=" * 70)
print("1. PARAMETRES DU MODELE")
print("=" * 70)

print("\n  PROBABILITES A PRIORI P(classe):")
print("  " + "-" * 40)
for i, prior in enumerate(nb.class_prior_):
    print(f"    P(Classe {i}) = {prior:.4f}")

print("\n  PARAMETRES GAUSSIENS PAR CLASSE:")
print("  " + "-" * 40)
for i in range(len(nb.classes_)):
    print(f"\n    Classe {i}:")
    for j, name in enumerate(feature_names):
        mean = nb.theta_[i][j]
        var = nb.var_[i][j]
        std = np.sqrt(var)
        print(f"      {name}: μ={mean:.3f}, σ={std:.3f}")

print("\n" + "=" * 70)
print("2. EXPLICATION DETAILLEE D'UNE PREDICTION")
print("=" * 70)

# Prendre un exemple
sample_idx = 0
sample = X[sample_idx:sample_idx+1]
true_label = y[sample_idx]

print(f"\n  Echantillon analyse:")
print(f"    feature1 = {sample[0][0]:.3f}")
print(f"    feature2 = {sample[0][1]:.3f}")
print(f"    Vraie classe = {true_label}")

print("\n  CALCUL BAYESIEN ETAPE PAR ETAPE:")
print("  " + "-" * 40)

# Calculer les log-probabilites manuellement
log_probs = []
for c in range(len(nb.classes_)):
    log_prior = np.log(nb.class_prior_[c])

    print(f"\n    Classe {c}:")
    print(f"      log P(classe {c}) = log({nb.class_prior_[c]:.4f}) = {log_prior:.4f}")

    log_likelihood_total = 0
    for j, name in enumerate(feature_names):
        x_j = sample[0][j]
        mean = nb.theta_[c][j]
        var = nb.var_[c][j]

        # Formule de la gaussienne en log
        log_likelihood = -0.5 * np.log(2 * np.pi * var) - 0.5 * ((x_j - mean)**2 / var)
        log_likelihood_total += log_likelihood

        print(f"      log P({name}={x_j:.3f} | classe {c}):")
        print(f"        μ={mean:.3f}, σ²={var:.3f}")
        print(f"        = {log_likelihood:.4f}")

    total_log_prob = log_prior + log_likelihood_total
    log_probs.append(total_log_prob)
    print(f"      -----------------------------------------")
    print(f"      Total log P(classe {c} | X) ∝ {total_log_prob:.4f}")

# Convertir en probabilites
log_probs = np.array(log_probs)
probs = np.exp(log_probs - np.max(log_probs))
probs = probs / probs.sum()

print(f"\n  PROBABILITES FINALES:")
print("  " + "-" * 40)
for c, prob in enumerate(probs):
    bar = "█" * int(prob * 30)
    print(f"    P(Classe {c} | X) = {prob:.4f}  {bar}")

pred = np.argmax(probs)
print(f"\n  PREDICTION: Classe {pred} (probabilite {probs[pred]:.2%})")
print(f"  VERITE: Classe {true_label}")
print(f"  RESULTAT: {'✓ CORRECT' if pred == true_label else '✗ INCORRECT'}")

# Verification avec sklearn
sklearn_proba = nb.predict_proba(sample)[0]
print(f"\n  Verification sklearn: {sklearn_proba.round(4)}")

print("\n" + "=" * 70)
print("3. RESUME DE L'INTERPRETATION")
print("=" * 70)
print("""
COMMENT INTERPRETER NAIVE BAYES:

1. Le modele compare l'echantillon aux distributions apprises
   pour chaque classe

2. Pour chaque classe, il calcule:
   P(classe | X) ∝ P(classe) × P(X | classe)

3. La classe avec la probabilite la plus elevee gagne

4. On peut voir EXACTEMENT pourquoi une decision est prise
   en regardant les contributions de chaque feature!

C'est cette interpretabilite qui rend Naive Bayes si utile
dans des domaines comme la medecine ou la finance.
""")

