"""
Module: Random Forest Regressor
Categorie: Supervised Regression
Difficulte: Intermediaire

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('housing_simple.csv')

# Explorer les donnees
# Type: Code executable
# Fonction pour formater les nombres au style francais (espace comme separateur)
def fmt(n, decimals=0):
    return f"{n:,.{decimals}f}".replace(",", " ")

print("=" * 70)
print("   EXPLORATION DU DATASET IMMOBILIER")
print("   Pour l'entrainement de notre Random Forest")
print("=" * 70)

# =================================================================
# 1. APERCU DES DONNEES
# =================================================================
print("\n" + "-" * 40)
print("1. APERCU DU DATASET")
print("-" * 40)
print("""
Ce dataset contient des biens immobiliers avec leurs caracteristiques
et prix de vente. Nous allons utiliser Random Forest pour predire
le prix a partir de la surface et du nombre de pieces.
""")
display(df.head(10), title="Dataset Immobilier")

# =================================================================
# 2. DIMENSIONS
# =================================================================
print("\n" + "-" * 40)
print("2. DIMENSIONS DU DATASET")
print("-" * 40)
n_samples = len(df)
n_features = 2
print(f"""
Nombre de biens immobiliers : {n_samples}
Nombre de features          : {n_features} (surface, nb_rooms)
Variable cible              : price (prix en euros)

Ce dataset est adapte pour une foret aleatoire:
- Assez d'echantillons pour le bootstrap sampling
- Features numeriques directement utilisables
""")

# =================================================================
# 3. STATISTIQUES DESCRIPTIVES
# =================================================================
print("\n" + "-" * 40)
print("3. STATISTIQUES DESCRIPTIVES")
print("-" * 40)
print("""
Comprendre la distribution des donnees aide a interpreter
les predictions du modele.
""")
display(df.describe().round(2), title="Statistiques")

# Interpretation
print(f"""
INTERPRETATION:

SURFACE:
  - Moyenne: {df['surface'].mean():.0f} m2
  - Etendue: {df['surface'].min():.0f} a {df['surface'].max():.0f} m2
  - Ecart-type: {df['surface'].std():.0f} m2

NOMBRE DE PIECES:
  - Moyenne: {df['nb_rooms'].mean():.1f} pieces
  - Etendue: {df['nb_rooms'].min():.0f} a {df['nb_rooms'].max():.0f} pieces

PRIX:
  - Moyenne: {fmt(df['price'].mean())} EUR
  - Etendue: {fmt(df['price'].min())} a {fmt(df['price'].max())} EUR
  - Prix median: {fmt(df['price'].median())} EUR
""")

# =================================================================
# 4. CORRELATIONS
# =================================================================
print("\n" + "-" * 40)
print("4. MATRICE DE CORRELATION")
print("-" * 40)
print("""
Les correlations montrent les liens entre variables.
Plus la correlation avec le prix est forte, plus la feature
est potentiellement predictive.
""")
corr_matrix = df.corr().round(3)
display(corr_matrix, title="Correlations")

# Interpreter les correlations
corr_surface = corr_matrix.loc['surface', 'price']
corr_rooms = corr_matrix.loc['nb_rooms', 'price']

print(f"""
INTERPRETATION DES CORRELATIONS:

Surface <-> Prix: {corr_surface:.3f}
  - {'Forte' if abs(corr_surface) > 0.7 else 'Moderee' if abs(corr_surface) > 0.4 else 'Faible'} correlation {'positive' if corr_surface > 0 else 'negative'}
  - {'La surface est un bon predicteur du prix' if abs(corr_surface) > 0.5 else 'Relation moderee'}

Nb_rooms <-> Prix: {corr_rooms:.3f}
  - {'Forte' if abs(corr_rooms) > 0.7 else 'Moderee' if abs(corr_rooms) > 0.4 else 'Faible'} correlation {'positive' if corr_rooms > 0 else 'negative'}

Surface <-> Nb_rooms: {corr_matrix.loc['surface', 'nb_rooms']:.3f}
  - Les grandes surfaces ont generalement plus de pieces
""")

# =================================================================
# CONCLUSION
# =================================================================
print("\n" + "=" * 70)
print("SYNTHESE POUR LE RANDOM FOREST")
print("=" * 70)
print(f"""
- {n_samples} echantillons disponibles (suffisant pour le bootstrap)
- Les deux features sont correlees au prix
- Pas de normalisation necessaire (Random Forest y est insensible)
- Les donnees sont pretes pour l'entrainement!
""")


# Visualiser les relations
# Type: Code executable
# Fonction pour formater les nombres au style francais (espace comme separateur)
def fmt(n, decimals=0):
    return f"{n:,.{decimals}f}".replace(",", " ")

print("=" * 70)
print("   VISUALISATION DES RELATIONS")
print("   Comprendre les liens entre features et prix")
print("=" * 70)

# =================================================================
# 1. CREATION DES GRAPHIQUES
# =================================================================
print("\n" + "-" * 40)
print("1. SCATTER PLOTS: FEATURES vs PRIX")
print("-" * 40)
print("""
Ces graphiques montrent comment le prix varie en fonction de chaque feature.
Un Random Forest peut capturer des relations non-lineaires!
""")

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

# Surface vs Prix
axes[0].scatter(df['surface'], df['price'], alpha=0.6, color='#9B7AC4',
                edgecolors='black', linewidth=0.5)
axes[0].set_xlabel('Surface (m2)', fontsize=12)
axes[0].set_ylabel('Prix (euros)', fontsize=12)
axes[0].set_title('Surface vs Prix', fontsize=14, fontweight='bold')
axes[0].grid(True, alpha=0.3)

# Ajouter une tendance
z = np.polyfit(df['surface'], df['price'], 1)
p = np.poly1d(z)
axes[0].plot(df['surface'].sort_values(), p(df['surface'].sort_values()),
             'r--', linewidth=2, label=f'Tendance lineaire')
axes[0].legend()

# Nb rooms vs Prix
axes[1].scatter(df['nb_rooms'], df['price'], alpha=0.6, color='#C09CF0',
                edgecolors='black', linewidth=0.5)
axes[1].set_xlabel('Nombre de pieces', fontsize=12)
axes[1].set_ylabel('Prix (euros)', fontsize=12)
axes[1].set_title('Nb Pieces vs Prix', fontsize=14, fontweight='bold')
axes[1].grid(True, alpha=0.3)

# Moyenne par nombre de pieces
mean_by_rooms = df.groupby('nb_rooms')['price'].mean()
axes[1].plot(mean_by_rooms.index, mean_by_rooms.values, 'ro-',
             linewidth=2, markersize=8, label='Moyenne par nb pieces')
axes[1].legend()

plt.tight_layout()
plt.show()

# =================================================================
# 2. INTERPRETATION DES GRAPHIQUES
# =================================================================
print("\n" + "-" * 40)
print("2. INTERPRETATION")
print("-" * 40)

# Calculer le prix moyen par m2
prix_m2 = df['price'].mean() / df['surface'].mean()

print(f"""
GRAPHIQUE 1 - Surface vs Prix:
- Relation {'lineaire' if df['surface'].corr(df['price']) > 0.9 else 'globalement lineaire avec dispersion'}
- Prix moyen par m2: {fmt(prix_m2)} EUR/m2
- La surface est le principal determinant du prix

GRAPHIQUE 2 - Nb Pieces vs Prix:
- Tendance croissante avec le nombre de pieces
- Dispersion plus importante (a nb pieces egal, les prix varient)
- Relation moins directe qu'avec la surface

POUR LE RANDOM FOREST:
- Peut capturer les interactions entre surface et nb_rooms
- N'est pas limite aux relations lineaires
- Chaque arbre peut trouver des regles differentes
""")

# =================================================================
# 3. DISTRIBUTION DES PRIX
# =================================================================
print("\n" + "-" * 40)
print("3. DISTRIBUTION DES PRIX")
print("-" * 40)

plt.figure(figsize=(10, 5))
plt.hist(df['price'], bins=20, color='#9B7AC4', edgecolor='black', alpha=0.7)
plt.axvline(df['price'].mean(), color='#F7E64D', linewidth=2,
            linestyle='--', label=f'Moyenne: {fmt(df["price"].mean())} EUR')
plt.axvline(df['price'].median(), color='#27ae60', linewidth=2,
            linestyle='--', label=f'Mediane: {fmt(df["price"].median())} EUR')
plt.xlabel('Prix (euros)', fontsize=12)
plt.ylabel('Nombre de biens', fontsize=12)
plt.title('Distribution des prix immobiliers', fontsize=14, fontweight='bold')
plt.legend()
plt.grid(True, alpha=0.3, axis='y')
plt.show()

print(f"""
La distribution des prix permet de comprendre:
- Le marche couvre une gamme de {fmt(df['price'].min())} a {fmt(df['price'].max())} EUR
- La majorite des biens sont proches de la moyenne
- {'Distribution a peu pres symetrique' if abs(df['price'].mean() - df['price'].median()) < df['price'].std()*0.1 else 'Distribution legerement asymetrique'}
""")


# Entrainer un Random Forest
# Type: Code executable
print("=" * 70)
print("   ENTRAINEMENT DU RANDOM FOREST")
print("   Construire notre foret de 100 arbres")
print("=" * 70)

from sklearn.ensemble import RandomForestRegressor

# =================================================================
# 1. PREPARATION DES DONNEES
# =================================================================
print("\n" + "-" * 40)
print("1. PREPARATION DES DONNEES")
print("-" * 40)

X = df[['surface', 'nb_rooms']].values
y = df['price'].values

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

print(f"""
Division des donnees:
  - Entrainement: {len(X_train)} biens ({len(X_train)/len(X)*100:.0f}%)
  - Test: {len(X_test)} biens ({len(X_test)/len(X)*100:.0f}%)

Pas de normalisation necessaire pour Random Forest!
Les arbres de decision ne sont pas sensibles a l'echelle des features.
""")

# =================================================================
# 2. CONFIGURATION DU MODELE
# =================================================================
print("\n" + "-" * 40)
print("2. CONFIGURATION DU RANDOM FOREST")
print("-" * 40)

rf = RandomForestRegressor(
    n_estimators=100,      # Nombre d'arbres
    max_depth=10,          # Profondeur max par arbre
    min_samples_split=5,   # Min echantillons pour split
    random_state=42
)

print(f"""
HYPERPARAMETRES CHOISIS:

n_estimators = 100
  - Nous allons creer 100 arbres de decision
  - Chaque arbre est entraine sur un echantillon bootstrap
  - Plus d'arbres = predictions plus stables, mais plus lent

max_depth = 10
  - Chaque arbre peut avoir jusqu'a 10 niveaux
  - Limite l'overfitting tout en permettant des regles complexes

min_samples_split = 5
  - Un noeud doit avoir au moins 5 echantillons pour etre divise
  - Evite les splits sur des cas isoles

random_state = 42
  - Garantit la reproductibilite des resultats
""")

# =================================================================
# 3. ENTRAINEMENT
# =================================================================
print("\n" + "-" * 40)
print("3. ENTRAINEMENT DE LA FORET")
print("-" * 40)

import time
start_time = time.time()
rf.fit(X_train, y_train)
training_time = time.time() - start_time

print(f"""
ENTRAINEMENT TERMINE!

Temps d'entrainement: {training_time:.2f} secondes

Ce qui s'est passe:
1. 100 echantillons bootstrap ont ete crees
2. 100 arbres de decision ont ete entraines
3. Chaque split n'a considere qu'un sous-ensemble aleatoire de features

STATISTIQUES DU MODELE:
  Nombre d'arbres: {rf.n_estimators}
  Nombre de features utilisees: {rf.n_features_in_}
  Profondeur max configuree: {rf.max_depth}
""")

# =================================================================
# 4. APERCU DE LA FORET
# =================================================================
print("\n" + "-" * 40)
print("4. ANALYSE DE LA FORET CREEE")
print("-" * 40)

# Analyser les arbres individuels
depths = [tree.get_depth() for tree in rf.estimators_]
n_leaves = [tree.get_n_leaves() for tree in rf.estimators_]

print(f"""
STRUCTURE DES 100 ARBRES:

Profondeur des arbres:
  - Minimum: {min(depths)}
  - Maximum: {max(depths)}
  - Moyenne: {np.mean(depths):.1f}

Nombre de feuilles par arbre:
  - Minimum: {min(n_leaves)}
  - Maximum: {max(n_leaves)}
  - Moyenne: {np.mean(n_leaves):.1f}

Total des regles apprises: ~{sum(n_leaves)} feuilles
(chaque feuille represente une prediction possible)
""")


# Evaluer les performances
# Type: Code executable
# Fonction pour formater les nombres au style francais (espace comme separateur)
def fmt(n, decimals=0):
    return f"{n:,.{decimals}f}".replace(",", " ")

print("=" * 70)
print("   EVALUATION DU RANDOM FOREST")
print("   Mesurer la qualite des predictions")
print("=" * 70)

from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import LinearRegression

# =================================================================
# PREPARATION ET ENTRAINEMENT
# =================================================================
X = df[['surface', 'nb_rooms']].values
y = df['price'].values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

rf = RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42)
rf.fit(X_train, y_train)
y_pred = rf.predict(X_test)

# =================================================================
# 1. METRIQUES DE PERFORMANCE
# =================================================================
print("\n" + "-" * 40)
print("1. METRIQUES DE PERFORMANCE")
print("-" * 40)

mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
mae = np.mean(np.abs(y_test - y_pred))
r2 = r2_score(y_test, y_pred)

print(f"""
RESULTATS SUR LE JEU DE TEST ({len(y_test)} biens):

MSE (Mean Squared Error): {fmt(mse)}
  - Erreur quadratique moyenne
  - Penalise fortement les grandes erreurs

RMSE (Root MSE): {fmt(rmse)} EUR
  - Ecart moyen des predictions en euros
  - En moyenne, on se trompe de {fmt(rmse)} EUR

MAE (Mean Absolute Error): {fmt(mae)} EUR
  - Erreur absolue moyenne
  - Moins sensible aux outliers que RMSE

R2 Score: {r2:.4f} ({r2*100:.1f}%)
  - Proportion de variance expliquee par le modele
  - 1.0 = parfait, 0.0 = aussi bon que la moyenne
""")

# =================================================================
# 2. GUIDE D'INTERPRETATION
# =================================================================
print("\n" + "-" * 40)
print("2. GUIDE D'INTERPRETATION")
print("-" * 40)

prix_moyen = y_test.mean()
erreur_relative = rmse / prix_moyen * 100

print(f"""
COMPRENDRE CES METRIQUES:

RMSE = {fmt(rmse)} EUR
  - Prix moyen du test set: {fmt(prix_moyen)} EUR
  - Erreur relative: {erreur_relative:.1f}%
  - {'Excellent' if erreur_relative < 5 else 'Bon' if erreur_relative < 10 else 'Correct' if erreur_relative < 15 else 'A ameliorer'}

R2 = {r2:.4f}
  - > 0.95: Excellent
  - 0.80 - 0.95: Tres bon
  - 0.60 - 0.80: Correct
  - < 0.60: A ameliorer

VOTRE MODELE:
  - R2 = {r2:.2f} --> {'Excellent!' if r2 > 0.95 else 'Tres bon!' if r2 > 0.8 else 'Correct' if r2 > 0.6 else 'A ameliorer'}
  - Erreur relative = {erreur_relative:.1f}% --> {'Excellent!' if erreur_relative < 5 else 'Bon!' if erreur_relative < 10 else 'Correct' if erreur_relative < 15 else 'A ameliorer'}
""")

# =================================================================
# 3. COMPARAISON AVEC REGRESSION LINEAIRE
# =================================================================
print("\n" + "-" * 40)
print("3. COMPARAISON AVEC REGRESSION LINEAIRE")
print("-" * 40)

lr = LinearRegression()
lr.fit(X_train, y_train)
y_pred_lr = lr.predict(X_test)
r2_lr = r2_score(y_test, y_pred_lr)
rmse_lr = np.sqrt(mean_squared_error(y_test, y_pred_lr))

print(f"""
COMPARAISON DES MODELES:

                    Regression Lineaire    Random Forest
-----------------------------------------------------------
R2 Score:           {r2_lr:.4f}                 {r2:.4f}
RMSE:               {fmt(rmse_lr)} EUR              {fmt(rmse)} EUR

INTERPRETATION:
""")

if r2 > r2_lr:
    improvement = (r2 - r2_lr) / r2_lr * 100
    print(f"  Le Random Forest surpasse la regression lineaire de {improvement:.1f}%!")
    print(f"  Cela suggere que les relations ne sont pas purement lineaires.")
else:
    print(f"  La regression lineaire fait aussi bien ou mieux.")
    print(f"  Les relations sont probablement lineaires dans ce dataset.")

# =================================================================
# 4. ANALYSE DES ERREURS
# =================================================================
print("\n" + "-" * 40)
print("4. ANALYSE DES ERREURS")
print("-" * 40)

erreurs = y_test - y_pred
erreurs_pct = erreurs / y_test * 100

print(f"""
DISTRIBUTION DES ERREURS:

Erreur minimale: {'+' if erreurs.min() >= 0 else ''}{fmt(erreurs.min())} EUR ({erreurs_pct[np.argmin(erreurs)]:.1f}%)
Erreur maximale: {'+' if erreurs.max() >= 0 else ''}{fmt(erreurs.max())} EUR ({erreurs_pct[np.argmax(erreurs)]:.1f}%)

Biens sous-estimes (erreur > 0): {(erreurs > 0).sum()} ({(erreurs > 0).mean()*100:.0f}%)
Biens surestimes (erreur < 0): {(erreurs < 0).sum()} ({(erreurs < 0).mean()*100:.0f}%)

Predictions a moins de 5% d'erreur: {(np.abs(erreurs_pct) < 5).sum()} ({(np.abs(erreurs_pct) < 5).mean()*100:.0f}%)
Predictions a moins de 10% d'erreur: {(np.abs(erreurs_pct) < 10).sum()} ({(np.abs(erreurs_pct) < 10).mean()*100:.0f}%)
""")


# Importance des features
# Type: Code executable
print("=" * 70)
print("   IMPORTANCE DES FEATURES")
print("   Comprendre ce qui influence le plus les predictions")
print("=" * 70)

from sklearn.ensemble import RandomForestRegressor

# =================================================================
# ENTRAINEMENT
# =================================================================
X = df[['surface', 'nb_rooms']].values
y = df['price'].values
feature_names = ['surface', 'nb_rooms']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

rf = RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42)
rf.fit(X_train, y_train)

# =================================================================
# 1. CALCUL DE L'IMPORTANCE
# =================================================================
print("\n" + "-" * 40)
print("1. IMPORTANCE DES FEATURES (MDI)")
print("-" * 40)

importances = rf.feature_importances_
std = np.std([tree.feature_importances_ for tree in rf.estimators_], axis=0)

print("""
L'importance MDI (Mean Decrease in Impurity) mesure combien
chaque feature contribue a reduire l'erreur dans les arbres.
""")

print(f"\n{'Feature':<15} | {'Importance':>12} | {'Ecart-type':>12} | {'Interpretation'}")
print("-" * 65)
for name, imp, s in sorted(zip(feature_names, importances, std), key=lambda x: -x[1]):
    interp = "Predicteur principal" if imp > 0.6 else "Important" if imp > 0.3 else "Secondaire"
    bar = "█" * int(imp * 30)
    print(f"{name:<15} | {imp:>11.1%} | {s:>12.4f} | {interp}")
    print(f"{'':15} | {bar}")

# =================================================================
# 2. INTERPRETATION
# =================================================================
print("\n" + "-" * 40)
print("2. INTERPRETATION DE L'IMPORTANCE")
print("-" * 40)

dominant = feature_names[np.argmax(importances)]
dominant_imp = importances.max()
secondary = feature_names[np.argmin(importances)]
secondary_imp = importances.min()

ratio = dominant_imp / secondary_imp if secondary_imp > 0 else float('inf')

print(f"""
ANALYSE:

Feature dominante: {dominant}
  - Importance: {dominant_imp:.1%}
  - Utilisee dans la majorite des splits des arbres
  - Reduit le plus l'erreur de prediction

Feature secondaire: {secondary}
  - Importance: {secondary_imp:.1%}
  - Apporte une information complementaire
  - Peut etre importante pour certains cas

Ratio d'importance: {ratio:.1f}x
  - {dominant} est {ratio:.1f} fois plus importante que {secondary}

EN PRATIQUE (immobilier):
  - La surface est generalement LE facteur cle du prix
  - Le nombre de pieces affine l'estimation
  - Un 50m2 avec 3 pieces vs 2 pieces n'a pas le meme prix
""")

# =================================================================
# 3. VISUALISATION
# =================================================================
print("\n" + "-" * 40)
print("3. VISUALISATION")
print("-" * 40)

plt.figure(figsize=(10, 5))
colors = ['#9B7AC4', '#F7E64D']
bars = plt.barh(feature_names, importances, color=colors, xerr=std, capsize=5)
plt.xlabel('Importance (reduction d\'impurete)', fontsize=12)
plt.title('Importance des Features - Random Forest', fontsize=14, fontweight='bold')
plt.grid(True, alpha=0.3, axis='x')

# Ajouter les pourcentages sur les barres
for bar, imp in zip(bars, importances):
    plt.text(bar.get_width() + 0.02, bar.get_y() + bar.get_height()/2,
             f'{imp:.1%}', va='center', fontsize=12, fontweight='bold')

plt.xlim(0, 1.1)
plt.tight_layout()
plt.show()

print("""
Les barres d'erreur montrent la variabilite de l'importance
entre les differents arbres de la foret.
""")


# Visualiser les predictions
# Type: Code executable
# Fonction pour formater les nombres au style francais (espace comme separateur)
def fmt(n, decimals=0):
    return f"{n:,.{decimals}f}".replace(",", " ")

print("=" * 70)
print("   VISUALISATION DES PREDICTIONS")
print("   Comparer predictions vs valeurs reelles")
print("=" * 70)

from sklearn.ensemble import RandomForestRegressor

# =================================================================
# PREPARATION
# =================================================================
X = df[['surface', 'nb_rooms']].values
y = df['price'].values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

rf = RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42)
rf.fit(X_train, y_train)
y_pred = rf.predict(X_test)

# =================================================================
# 1. SCATTER PLOT PREDICTIONS VS REELLES
# =================================================================
print("\n" + "-" * 40)
print("1. PREDICTIONS VS VALEURS REELLES")
print("-" * 40)
print("""
Le graphique ideal montre tous les points sur la diagonale.
Plus les points sont proches de la ligne, meilleures sont les predictions.
""")

plt.figure(figsize=(10, 8))

# Points
plt.scatter(y_test, y_pred, alpha=0.6, color='#9B7AC4', edgecolors='black',
            s=80, label='Predictions')

# Ligne parfaite
min_val = min(y_test.min(), y_pred.min())
max_val = max(y_test.max(), y_pred.max())
plt.plot([min_val, max_val], [min_val, max_val], 'k--', linewidth=2,
         label='Prediction parfaite')

# Zone de tolerance +/- 10%
plt.fill_between([min_val, max_val],
                 [min_val * 0.9, max_val * 0.9],
                 [min_val * 1.1, max_val * 1.1],
                 alpha=0.2, color='#27ae60', label='Zone +/- 10%')

plt.xlabel('Prix Reel (euros)', fontsize=12)
plt.ylabel('Prix Predit (euros)', fontsize=12)
plt.title('Random Forest: Predictions vs Valeurs Reelles', fontsize=14, fontweight='bold')
plt.legend(loc='upper left')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# =================================================================
# 2. ANALYSE VISUELLE
# =================================================================
print("\n" + "-" * 40)
print("2. ANALYSE DU GRAPHIQUE")
print("-" * 40)

# Calculer les erreurs
erreurs_pct = np.abs(y_test - y_pred) / y_test * 100
dans_10pct = (erreurs_pct <= 10).mean() * 100

r2 = r2_score(y_test, y_pred)

print(f"""
INTERPRETATION:

Alignement avec la diagonale:
  - R2 = {r2:.4f} ({r2*100:.1f}% de variance expliquee)
  - {'Excellent alignement!' if r2 > 0.95 else 'Bon alignement' if r2 > 0.8 else 'Alignement correct'}

Predictions dans la zone verte (+/- 10%):
  - {dans_10pct:.0f}% des predictions
  - {'Tres bonne precision!' if dans_10pct > 80 else 'Bonne precision' if dans_10pct > 60 else 'Precision a ameliorer'}

POINTS A OBSERVER:
- Points au-dessus de la ligne = sous-estimation
- Points en-dessous de la ligne = surestimation
- Points eloignes = erreurs importantes (outliers?)
""")

# =================================================================
# 3. DISTRIBUTION DES ERREURS
# =================================================================
print("\n" + "-" * 40)
print("3. DISTRIBUTION DES ERREURS")
print("-" * 40)

erreurs = y_test - y_pred

plt.figure(figsize=(10, 5))
plt.hist(erreurs, bins=20, color='#9B7AC4', edgecolor='black', alpha=0.7)
plt.axvline(0, color='#27ae60', linewidth=2, linestyle='--', label='Erreur = 0')
plt.axvline(erreurs.mean(), color='#F7E64D', linewidth=2,
            linestyle='--', label=f'Moyenne: {fmt(erreurs.mean())}')
plt.xlabel('Erreur (Prix Reel - Prix Predit)', fontsize=12)
plt.ylabel('Nombre de biens', fontsize=12)
plt.title('Distribution des erreurs de prediction', fontsize=14, fontweight='bold')
plt.legend()
plt.grid(True, alpha=0.3, axis='y')
plt.show()

print(f"""
La distribution des erreurs devrait etre:
- Centree sur 0 (pas de biais systematique)
- En forme de cloche (erreurs aleatoires)

VOTRE MODELE:
- Erreur moyenne: {fmt(erreurs.mean())} EUR {'(pas de biais)' if abs(erreurs.mean()) < erreurs.std()*0.1 else '(leger biais)'}
- Ecart-type: {fmt(erreurs.std())} EUR
- {'Distribution symetrique' if abs(erreurs.mean()) < erreurs.std()*0.1 else 'Leger decentrage'}
""")


# Exercice: Optimiser le nombre d'arbres
# Type: Exercice
# Exercice: Trouvez le nombre optimal d'arbres

from sklearn.ensemble import RandomForestRegressor

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

# Testez differents nombres d'arbres
n_trees_list = [10, 50, 100, 200, 500]
results = []

for n_trees in n_trees_list:
    # TODO: Creez et entrainez un RF avec n_trees arbres
    # TODO: Calculez le R2 sur les donnees de test
    # TODO: Ajoutez le resultat a la liste results
    pass

# TODO: Visualisez les resultats


# SHAP pour Random Forest
# Type: Code executable
# Fonction pour formater les nombres au style francais (espace comme separateur)
def fmt(n, decimals=0):
    n = float(n)  # Convertit les arrays numpy en scalaire
    return f"{n:,.{decimals}f}".replace(",", " ")

print("=" * 70)
print("   EXPLICABILITE AVEC SHAP")
print("   Comprendre chaque prediction individuellement")
print("=" * 70)

from sklearn.ensemble import RandomForestRegressor

# =================================================================
# ENTRAINEMENT
# =================================================================
X = df[['surface', 'nb_rooms']].values
y = df['price'].values
feature_names = ['surface', 'nb_rooms']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

rf = RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42)
rf.fit(X_train, y_train)

# =================================================================
# 1. CALCUL DES VALEURS SHAP
# =================================================================
print("\n" + "-" * 40)
print("1. CALCUL DES VALEURS SHAP")
print("-" * 40)
print("""
SHAP (SHapley Additive exPlanations) decompose chaque prediction
en contributions de chaque feature.

TreeExplainer est optimise pour les modeles a base d'arbres.
""")

explainer = shap.TreeExplainer(rf)
shap_values = explainer.shap_values(X_test)

print(f"""
Valeurs SHAP calculees pour {len(X_test)} echantillons.

Pour chaque prediction:
  Prix predit = Prix moyen + SHAP(surface) + SHAP(nb_rooms)
""")

# =================================================================
# 2. VISUALISATION GLOBALE
# =================================================================
print("\n" + "-" * 40)
print("2. IMPACT GLOBAL DES FEATURES")
print("-" * 40)
print("""
Ce graphique montre l'impact de chaque feature sur toutes les predictions.
- Chaque point = un bien immobilier
- Couleur = valeur de la feature (rouge = haute, bleu = basse)
- Position X = impact sur le prix
""")

plt.figure(figsize=(10, 5))
shap.summary_plot(shap_values, X_test, feature_names=feature_names, show=False)
plt.title('Impact SHAP des features sur le prix', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

print("""
INTERPRETATION:
- Surface rouge (grande) = impact positif fort (augmente le prix)
- Nb_rooms rouge = impact positif (plus de pieces = plus cher)
- La dispersion montre la variabilite de l'impact
""")

# =================================================================
# 3. EXPLICATION D'UNE PREDICTION
# =================================================================
print("\n" + "-" * 40)
print("3. EXPLICATION D'UNE PREDICTION INDIVIDUELLE")
print("-" * 40)

idx = 0
sample = X_test[idx]
prediction = rf.predict([sample])[0]
base_value = explainer.expected_value

print(f"""
BIEN IMMOBILIER #{idx + 1}:
  Surface: {sample[0]:.0f} m2
  Nombre de pieces: {sample[1]:.0f}

PREDICTION DU RANDOM FOREST:
  Prix predit: {fmt(prediction)} EUR

DECOMPOSITION SHAP:
  Prix de base (moyenne): {fmt(base_value)} EUR
""")

for i, name in enumerate(feature_names):
    shap_val = shap_values[idx][i]
    direction = "augmente" if shap_val > 0 else "diminue"
    print(f"    + {name}: {'+' if shap_val >= 0 else ''}{fmt(shap_val)} EUR ({direction} le prix)")

total = base_value + sum(shap_values[idx])
print(f"""
  = Prix final: {fmt(total)} EUR

INTERPRETATION EN LANGAGE NATUREL:
""")

# Generer une explication textuelle
surface_impact = shap_values[idx][0]
rooms_impact = shap_values[idx][1]

if surface_impact > 0:
    print(f"  - La surface de {sample[0]:.0f}m2 AUGMENTE le prix de {fmt(abs(surface_impact))} EUR")
    print(f"    (c'est au-dessus de la moyenne)")
else:
    print(f"  - La surface de {sample[0]:.0f}m2 DIMINUE le prix de {fmt(abs(surface_impact))} EUR")
    print(f"    (c'est en-dessous de la moyenne)")

if rooms_impact > 0:
    print(f"  - Les {sample[1]:.0f} pieces AUGMENTENT le prix de {fmt(abs(rooms_impact))} EUR")
else:
    print(f"  - Les {sample[1]:.0f} pieces DIMINUENT le prix de {fmt(abs(rooms_impact))} EUR")

# =================================================================
# 4. COMPARAISON DE PLUSIEURS BIENS
# =================================================================
print("\n" + "-" * 40)
print("4. COMPARAISON DE PLUSIEURS BIENS")
print("-" * 40)

print(f"\n{'Bien':<6} | {'Surface':>10} | {'Pieces':>7} | {'Prix predit':>12} | {'SHAP surface':>14} | {'SHAP pieces':>12}")
print("-" * 75)

for i in range(min(5, len(X_test))):
    s = X_test[i]
    pred = rf.predict([s])[0]
    shap_s = shap_values[i][0]
    shap_r = shap_values[i][1]
    print(f"#{i+1:<5} | {s[0]:>9.0f}m2 | {s[1]:>7.0f} | {fmt(pred):>11}€ | {shap_s:>+13,.0f}€ | {shap_r:>+11,.0f}€")

# =================================================================
# CONCLUSION
# =================================================================
print("\n" + "=" * 70)
print("SYNTHESE - EXPLICABILITE DU RANDOM FOREST")
print("=" * 70)
print(f"""
SHAP nous permet de:

1. EXPLIQUER CHAQUE PREDICTION
   - Decomposer le prix en contributions de chaque feature
   - Comprendre pourquoi un bien est estime a ce prix

2. IDENTIFIER LES DRIVERS PRINCIPAUX
   - La surface est le facteur principal ({np.mean(np.abs(shap_values[:, 0])):,.0f} EUR d'impact moyen)
   - Le nombre de pieces affine l'estimation ({np.mean(np.abs(shap_values[:, 1])):,.0f} EUR d'impact moyen)

3. COMMUNIQUER AVEC LES CLIENTS
   "Votre bien de {sample[0]:.0f}m2 est estime a {fmt(prediction)} EUR.
    La surface contribue pour {fmt(abs(surface_impact))} EUR
    et les {sample[1]:.0f} pieces pour {fmt(abs(rooms_impact))} EUR."

Le Random Forest, bien que complexe (100 arbres!), devient
interpretable grace a SHAP.
""")

