"""
Module: Autoencoders
Categorie: Deep Learning
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('anomaly_data.csv')

# Explorer les donnees (transactions)
# Type: Code executable
print("=" * 70)
print("       EXPLORATION DES DONNEES DE TRANSACTIONS FINANCIERES")
print("=" * 70)
print("""
Les autoencoders excellent dans la DETECTION D'ANOMALIES car ils
apprennent ce qui est "normal" et signalent ce qui est "inhabituel".

Notre dataset simule des transactions bancaires avec:
- La grande majorite: transactions normales
- Une petite minorite: transactions frauduleuses (anomalies)

C'est un probleme de DESEQUILIBRE DE CLASSES tres realiste!
""")

# ========== 1. APERCU DES DONNEES ==========
print("=" * 70)
print("1. APERCU DU DATASET DE TRANSACTIONS")
print("=" * 70)
display(df.head(10), title="Echantillon de Transactions")

print("""
Chaque ligne represente une transaction avec:
- transaction_amount : Montant de la transaction (euros)
- transaction_time   : Heure de la transaction (0-24h)
- account_age        : Anciennete du compte (jours)
- num_transactions   : Nombre de transactions recentes
- is_anomaly         : 0 = normal, 1 = fraude potentielle
""")

# ========== 2. DIMENSIONS ==========
print("-" * 50)
print("2. DIMENSIONS DU DATASET")
print("-" * 50)
n_samples, n_features = df.shape
feature_cols = ['transaction_amount', 'transaction_time', 'account_age', 'num_transactions']
print(f"   Nombre de transactions : {n_samples}")
print(f"   Nombre de features     : {len(feature_cols)}")
print(f"   Features utilisees     : {', '.join(feature_cols)}")

# ========== 3. DISTRIBUTION DES CLASSES ==========
print("-" * 50)
print("3. DISTRIBUTION NORMALES vs ANOMALIES")
print("-" * 50)
counts = df['is_anomaly'].value_counts()
n_normal = counts.get(0, 0)
n_anomaly = counts.get(1, 0)
normal_pct = n_normal / len(df) * 100
anomaly_pct = n_anomaly / len(df) * 100

print(f"""
┌─────────────────────────────────────────────────┐
│  Transactions normales : {n_normal:5} ({normal_pct:5.1f}%)           │
│  Anomalies (fraudes)   : {n_anomaly:5} ({anomaly_pct:5.1f}%)            │
└─────────────────────────────────────────────────┘
""")

# ========== 4. INTERPRETATION DU DESEQUILIBRE ==========
print("-" * 50)
print("4. POURQUOI CE DESEQUILIBRE EST REALISTE?")
print("-" * 50)
ratio = n_normal / max(n_anomaly, 1)
print(f"""
Ratio normal/anomalie: {ratio:.1f}:1

Dans la vraie vie, les fraudes representent typiquement:
- Cartes bancaires : 0.1% des transactions
- Assurance        : 1-5% des reclamations
- Comptabilite     : <1% des ecritures

Notre dataset avec {anomaly_pct:.1f}% d'anomalies est deja
"genereux" en anomalies pour l'apprentissage!

DEFI: Detecter ces rares anomalies sans trop de faux positifs.
""")

# ========== 5. STATISTIQUES PAR GROUPE ==========
print("=" * 70)
print("5. COMPARAISON STATISTIQUE: NORMALES vs ANOMALIES")
print("=" * 70)
display(df.groupby('is_anomaly')[feature_cols].mean().round(2), title="Moyennes par groupe")

print("""
INTERPRETATION DES DIFFERENCES:
""")

# Calcul des differences
normal_stats = df[df['is_anomaly'] == 0][feature_cols].mean()
anomaly_stats = df[df['is_anomaly'] == 1][feature_cols].mean()

for col in feature_cols:
    diff_pct = (anomaly_stats[col] - normal_stats[col]) / max(normal_stats[col], 0.01) * 100
    direction = "↑ plus eleve" if diff_pct > 0 else "↓ plus bas"
    print(f"   {col:25}: {direction} de {abs(diff_pct):.0f}% pour les anomalies")

print("""
Ces differences sont les "signatures" que l'autoencoder apprendra
a reconnaitre comme inhabituelles!
""")


# Visualiser les donnees
# Type: Code executable
print("=" * 70)
print("       VISUALISATION DES DONNEES: NORMALES vs ANOMALIES")
print("=" * 70)
print("""
Avant de construire un autoencoder, visualisons les donnees pour
comprendre COMMENT les anomalies different des transactions normales.

L'autoencoder apprendra ces differences implicitement, mais nous
pouvons les observer directement avec des graphiques.
""")

# ========== SEPARATION DES DONNEES ==========
normal_df = df[df['is_anomaly'] == 0]
anomaly_df = df[df['is_anomaly'] == 1]

print(f"   Transactions normales a visualiser : {len(normal_df)}")
print(f"   Anomalies a visualiser             : {len(anomaly_df)}")

# ========== CREATION DES GRAPHIQUES ==========
print("\n" + "-" * 50)
print("GRAPHIQUES: Distribution et Separation")
print("-" * 50)

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

# --- Graphique 1: Distribution des montants ---
normal = df[df['is_anomaly'] == 0]['transaction_amount']
anomaly = df[df['is_anomaly'] == 1]['transaction_amount']

axes[0].hist(normal, bins=20, alpha=0.7, label=f'Normal (n={len(normal)})', color='#9B7AC4')
axes[0].hist(anomaly, bins=20, alpha=0.7, label=f'Anomalie (n={len(anomaly)})', color='#F7E64D')
axes[0].set_xlabel('Montant de transaction (euros)', fontsize=11)
axes[0].set_ylabel('Frequence', fontsize=11)
axes[0].set_title('Distribution des Montants', fontsize=12, fontweight='bold')
axes[0].legend(loc='upper right')
axes[0].grid(True, alpha=0.3)

# Ajouter lignes de moyenne
axes[0].axvline(normal.mean(), color='#9B7AC4', linestyle='--', linewidth=2, label='Moy. Normal')
axes[0].axvline(anomaly.mean(), color='#F7E64D', linestyle='--', linewidth=2, label='Moy. Anomalie')

# --- Graphique 2: Scatter 2D ---
axes[1].scatter(normal_df['transaction_amount'], normal_df['account_age'],
               alpha=0.6, label=f'Normal', color='#9B7AC4', s=40)
axes[1].scatter(anomaly_df['transaction_amount'], anomaly_df['account_age'],
               alpha=0.9, label=f'Anomalie', color='#F7E64D', s=120, marker='*', edgecolors='#e74c3c')
axes[1].set_xlabel('Montant (euros)', fontsize=11)
axes[1].set_ylabel('Age du compte (jours)', fontsize=11)
axes[1].set_title('Montant vs Age du Compte', fontsize=12, fontweight='bold')
axes[1].legend(loc='upper right')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# ========== INTERPRETATION ==========
print("""
INTERPRETATION DES GRAPHIQUES:

Graphique 1 - Distribution des montants:
─────────────────────────────────────────
• Les transactions normales suivent une distribution "typique"
• Les anomalies ont souvent des montants ATYPIQUES (tres hauts ou tres bas)
• Les lignes en pointilles montrent la difference des moyennes

Graphique 2 - Espace 2D:
─────────────────────────────────────────
• Les anomalies (etoiles jaunes) apparaissent souvent dans des zones
  peu peuplees par les transactions normales
• Un autoencoder apprendra cette "zone normale" et signalera
  les points qui en sortent
""")

# ========== STATISTIQUES COMPLEMENTAIRES ==========
print("-" * 50)
print("STATISTIQUES DE SEPARATION")
print("-" * 50)
print(f"""
Montant moyen:
  - Normal  : {normal.mean():.2f} euros
  - Anomalie: {anomaly.mean():.2f} euros
  - Ecart   : {abs(anomaly.mean() - normal.mean()):.2f} euros

Age compte moyen:
  - Normal  : {normal_df['account_age'].mean():.1f} jours
  - Anomalie: {anomaly_df['account_age'].mean():.1f} jours

Observation: Les anomalies ont des patterns differents que
l'autoencoder apprendra a distinguer!
""")


# Simuler un autoencoder simple
# Type: Code executable
from sklearn.neural_network import MLPRegressor
from sklearn.preprocessing import StandardScaler

print("=" * 70)
print("       CONSTRUCTION D'UN AUTOENCODER SIMPLE")
print("=" * 70)
print("""
Un autoencoder apprend a COMPRESSER puis RECONSTRUIRE ses entrees.

Architecture: Entree (4D) → Goulot (2D) → Sortie (4D)

ASTUCE CLE pour la detection d'anomalies:
On entraine UNIQUEMENT sur les donnees NORMALES!
Ainsi, l'autoencoder ne sait reconstruire que le "normal".
Les anomalies seront mal reconstruites → erreur elevee.
""")

# ========== 1. PREPARATION DES DONNEES ==========
print("=" * 70)
print("1. PREPARATION DES DONNEES")
print("=" * 70)

feature_cols = ['transaction_amount', 'transaction_time', 'account_age', 'num_transactions']
X = df[feature_cols].values
y_true = df['is_anomaly'].values

print(f"   Features utilisees: {feature_cols}")
print(f"   Dimensions entree : {X.shape[1]} features")
print(f"   Nombre total      : {len(X)} transactions")

# ========== 2. NORMALISATION ==========
print("\n" + "-" * 50)
print("2. NORMALISATION (StandardScaler)")
print("-" * 50)

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

print("""
Pourquoi normaliser?
- Les features ont des echelles differentes (montant: 0-5000, age: 0-365)
- Sans normalisation, les grandes valeurs domineraient l'apprentissage
- StandardScaler: moyenne=0, ecart-type=1 pour chaque feature
""")

# Montrer l'effet de la normalisation
print("   Avant normalisation:")
print(f"      Montant: min={X[:, 0].min():.1f}, max={X[:, 0].max():.1f}")
print(f"      Age    : min={X[:, 2].min():.1f}, max={X[:, 2].max():.1f}")
print("   Apres normalisation:")
print(f"      Montant: min={X_scaled[:, 0].min():.2f}, max={X_scaled[:, 0].max():.2f}")
print(f"      Age    : min={X_scaled[:, 2].min():.2f}, max={X_scaled[:, 2].max():.2f}")

# ========== 3. SEPARATION: NORMALES UNIQUEMENT ==========
print("\n" + "-" * 50)
print("3. ENTRAINEMENT SUR DONNEES NORMALES UNIQUEMENT")
print("-" * 50)

X_normal = X_scaled[y_true == 0]
n_normal = len(X_normal)
n_anomaly = (y_true == 1).sum()

print(f"""
┌─────────────────────────────────────────────────────┐
│  Donnees d'entrainement : {n_normal:5} (normales seulement) │
│  Donnees exclues        : {n_anomaly:5} (anomalies)          │
└─────────────────────────────────────────────────────┘

POURQUOI exclure les anomalies?
L'autoencoder doit apprendre CE QUI EST NORMAL.
Si on inclut les fraudes, il apprendrait aussi a les reconstruire!
""")

# ========== 4. CREATION DE L'AUTOENCODER ==========
print("=" * 70)
print("4. ARCHITECTURE DE L'AUTOENCODER")
print("=" * 70)

autoencoder = MLPRegressor(
    hidden_layer_sizes=(2,),  # Goulot d'etranglement: 2 dimensions
    activation='relu',
    max_iter=1000,
    random_state=42,
    verbose=False
)

print("""
Architecture choisie (simulation avec MLPRegressor):

    ENTREE          GOULOT          SORTIE
   [4 dim]  →→→    [2 dim]    →→→  [4 dim]

   montant  ──┐              ┌──  montant'
   heure    ──┼──→ [z1] ──┬──┼──  heure'
   age      ──┼──→ [z2] ──┘──┼──  age'
   nb_trans ──┘              └──  nb_trans'

Le GOULOT (2 dimensions) force le reseau a apprendre
une representation COMPRIMEE des transactions normales.
""")

# ========== 5. ENTRAINEMENT ==========
print("-" * 50)
print("5. ENTRAINEMENT DE L'AUTOENCODER")
print("-" * 50)
print("   Entrainement en cours (max 1000 iterations)...")

autoencoder.fit(X_normal, X_normal)

print(f"""
Entrainement termine!

Resume:
- Iterations effectuees : jusqu'a convergence ou 1000 max
- Objectif              : minimiser ||X - X_reconstruit||²
- Donnees utilisees     : {n_normal} transactions normales

L'autoencoder sait maintenant reconstruire les transactions
"normales". Les anomalies auront une ERREUR plus elevee!
""")


# Detection d'anomalies
# Type: Code executable
from sklearn.neural_network import MLPRegressor
from sklearn.preprocessing import StandardScaler

print("=" * 70)
print("       DETECTION D'ANOMALIES PAR ERREUR DE RECONSTRUCTION")
print("=" * 70)
print("""
Le principe de detection est simple et elegant:

1. L'autoencoder apprend a reconstruire les donnees NORMALES
2. On lui donne TOUTES les donnees (normales + anomalies)
3. Les ANOMALIES seront MAL reconstruites (erreur elevee)
4. On definit un SEUIL pour separer normal/anomalie

C'est comme un "detecteur de mensonge" pour les donnees!
""")

# ========== 1. PREPARATION ET ENTRAINEMENT ==========
print("-" * 50)
print("1. PREPARATION ET ENTRAINEMENT")
print("-" * 50)

feature_cols = ['transaction_amount', 'transaction_time', 'account_age', 'num_transactions']
X = df[feature_cols].values
y_true = df['is_anomaly'].values

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
X_normal = X_scaled[y_true == 0]

autoencoder = MLPRegressor(hidden_layer_sizes=(2,), activation='relu', max_iter=1000, random_state=42)
autoencoder.fit(X_normal, X_normal)

print(f"   Autoencoder entraine sur {len(X_normal)} transactions normales")

# ========== 2. RECONSTRUCTION DE TOUTES LES DONNEES ==========
print("\n" + "-" * 50)
print("2. RECONSTRUCTION DE TOUTES LES DONNEES")
print("-" * 50)

X_reconstructed = autoencoder.predict(X_scaled)

print(f"""
On demande a l'autoencoder de reconstruire TOUTES les {len(X_scaled)} transactions.

- Transactions normales : Bien reconstruites (il les connait)
- Anomalies            : Mal reconstruites (jamais vues!)
""")

# ========== 3. CALCUL DE L'ERREUR DE RECONSTRUCTION ==========
print("-" * 50)
print("3. CALCUL DE L'ERREUR DE RECONSTRUCTION (MSE)")
print("-" * 50)

# Erreur par transaction: moyenne des erreurs sur les 4 features
reconstruction_error = np.mean((X_scaled - X_reconstructed) ** 2, axis=1)

print("""
Formule: Erreur = moyenne((X_original - X_reconstruit)²)

Plus l'erreur est ELEVEE, plus la transaction est SUSPECTE!
""")

# Statistiques par groupe
error_normal = reconstruction_error[y_true == 0]
error_anomaly = reconstruction_error[y_true == 1]

print(f"""
Statistiques des erreurs:
┌───────────────────────────────────────────────────────┐
│  Groupe     │  Moyenne  │  Ecart-type  │  Max        │
├───────────────────────────────────────────────────────┤
│  Normal     │  {error_normal.mean():.4f}   │  {error_normal.std():.4f}      │  {error_normal.max():.4f}     │
│  Anomalie   │  {error_anomaly.mean():.4f}   │  {error_anomaly.std():.4f}      │  {error_anomaly.max():.4f}     │
└───────────────────────────────────────────────────────┘

Ratio erreur anomalie/normal: {error_anomaly.mean() / error_normal.mean():.1f}x plus eleve!
""")

# ========== 4. VISUALISATION DES ERREURS ==========
print("=" * 70)
print("4. VISUALISATION DES ERREURS DE RECONSTRUCTION")
print("=" * 70)

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

# --- Graphique 1: Distribution des erreurs ---
axes[0].hist(error_normal, bins=30, alpha=0.7, label=f'Normal (n={len(error_normal)})', color='#9B7AC4')
axes[0].hist(error_anomaly, bins=30, alpha=0.7, label=f'Anomalie (n={len(error_anomaly)})', color='#F7E64D')
axes[0].axvline(error_normal.mean(), color='#9B7AC4', linestyle='--', linewidth=2)
axes[0].axvline(error_anomaly.mean(), color='#F7E64D', linestyle='--', linewidth=2)
axes[0].set_xlabel('Erreur de reconstruction (MSE)', fontsize=11)
axes[0].set_ylabel('Frequence', fontsize=11)
axes[0].set_title('Distribution des Erreurs par Groupe', fontsize=12, fontweight='bold')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# --- Graphique 2: Erreurs par transaction ---
# Seuil a 90e percentile des donnees normales
threshold = np.percentile(error_normal, 90)

colors = ['#9B7AC4' if y == 0 else '#F7E64D' for y in y_true]
axes[1].scatter(range(len(reconstruction_error)), reconstruction_error,
               c=colors, alpha=0.6, s=30)
axes[1].axhline(y=threshold, color='#e74c3c', linestyle='--', linewidth=2, label=f'Seuil (90%): {threshold:.4f}')
axes[1].set_xlabel('Index de transaction', fontsize=11)
axes[1].set_ylabel('Erreur de reconstruction', fontsize=11)
axes[1].set_title('Erreur par Transaction (Violet=Normal, Jaune=Anomalie)', fontsize=12, fontweight='bold')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# ========== 5. INTERPRETATION ==========
print("""
INTERPRETATION DES GRAPHIQUES:

Graphique 1 - Distribution:
─────────────────────────────────────────
• Les anomalies (jaune) ont des erreurs generalement PLUS ELEVEES
• Il y a un chevauchement: certaines anomalies ressemblent au normal
• Les lignes en pointilles montrent les moyennes de chaque groupe

Graphique 2 - Par transaction:
─────────────────────────────────────────
• La ligne rouge est le SEUIL de detection (90e percentile)
• Les points AU-DESSUS sont detectes comme anomalies
• Les points jaunes au-dessus = vrais positifs (bien detectes)
• Les points violets au-dessus = faux positifs (fausses alertes)
""")

# Compter les detections
detected_anomalies = (reconstruction_error > threshold) & (y_true == 1)
missed_anomalies = (reconstruction_error <= threshold) & (y_true == 1)
false_positives = (reconstruction_error > threshold) & (y_true == 0)

print(f"""
RESULTATS DE DETECTION (seuil = 90e percentile):
┌────────────────────────────────────────────┐
│  Anomalies detectees   : {detected_anomalies.sum():5} / {(y_true == 1).sum():5}      │
│  Anomalies manquees    : {missed_anomalies.sum():5}              │
│  Faux positifs         : {false_positives.sum():5}              │
└────────────────────────────────────────────┘
""")


# Evaluer la detection
# Type: Code executable
from sklearn.neural_network import MLPRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import precision_score, recall_score, f1_score, confusion_matrix

print("=" * 70)
print("       EVALUATION DE LA DETECTION D'ANOMALIES")
print("=" * 70)
print("""
Pour evaluer un detecteur d'anomalies, on utilise des metriques
adaptees aux problemes DESEQUILIBRES:

- PRECISION : Parmi les alertes, combien sont de vraies fraudes?
- RECALL    : Parmi les fraudes, combien sont detectees?
- F1-SCORE  : Equilibre entre precision et recall

En detection de fraude, le RECALL est souvent prioritaire:
mieux vaut quelques fausses alertes que rater une vraie fraude!
""")

# ========== 1. PREPARATION ET ENTRAINEMENT ==========
print("-" * 50)
print("1. PREPARATION (recap)")
print("-" * 50)

feature_cols = ['transaction_amount', 'transaction_time', 'account_age', 'num_transactions']
X = df[feature_cols].values
y_true = df['is_anomaly'].values

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
X_normal = X_scaled[y_true == 0]

autoencoder = MLPRegressor(hidden_layer_sizes=(2,), activation='relu', max_iter=1000, random_state=42)
autoencoder.fit(X_normal, X_normal)

X_reconstructed = autoencoder.predict(X_scaled)
reconstruction_error = np.mean((X_scaled - X_reconstructed) ** 2, axis=1)

print(f"   Autoencoder entraine sur {len(X_normal)} transactions normales")
print(f"   Erreurs calculees pour {len(X_scaled)} transactions")

# ========== 2. DEFINITION DU SEUIL ==========
print("\n" + "-" * 50)
print("2. CHOIX DU SEUIL DE DETECTION")
print("-" * 50)

# Seuil au 95e percentile des erreurs normales
threshold = np.percentile(reconstruction_error[y_true == 0], 95)

print(f"""
Strategie: Seuil au 95e percentile des erreurs NORMALES

Interpretation:
- 95% des transactions normales ont une erreur INFERIEURE au seuil
- Donc ~5% des normales seront des faux positifs (accepte)
- Les vraies anomalies devraient avoir des erreurs SUPERIEURES

Seuil choisi: {threshold:.6f}
""")

# Predictions
y_pred = (reconstruction_error > threshold).astype(int)

# ========== 3. METRIQUES DE PERFORMANCE ==========
print("=" * 70)
print("3. METRIQUES DE PERFORMANCE")
print("=" * 70)

precision = precision_score(y_true, y_pred, zero_division=0)
recall = recall_score(y_true, y_pred, zero_division=0)
f1 = f1_score(y_true, y_pred, zero_division=0)

print(f"""
┌─────────────────────────────────────────────────────────┐
│                    RESULTATS                            │
├─────────────────────────────────────────────────────────┤
│  PRECISION : {precision:6.1%}                                   │
│             "Parmi les alertes, {precision*100:.0f}% sont vraies"      │
│                                                         │
│  RECALL    : {recall:6.1%}                                   │
│             "{recall*100:.0f}% des fraudes sont detectees"         │
│                                                         │
│  F1-SCORE  : {f1:6.1%}                                   │
│             "Equilibre precision-recall"                │
└─────────────────────────────────────────────────────────┘
""")

# ========== 4. MATRICE DE CONFUSION ==========
print("-" * 50)
print("4. MATRICE DE CONFUSION DETAILLEE")
print("-" * 50)

cm = confusion_matrix(y_true, y_pred)
tn, fp, fn, tp = cm.ravel()

print(f"""
                      PREDICTION
                Normal    Anomalie
              ┌─────────┬─────────┐
   Normal     │  {tn:5}  │  {fp:5}  │  ← Faux Positifs
REALITE       ├─────────┼─────────┤
   Anomalie   │  {fn:5}  │  {tp:5}  │  ← Vrais Positifs
              └─────────┴─────────┘
                   ↑
              Faux Negatifs
""")

# ========== 5. INTERPRETATION ==========
print("=" * 70)
print("5. INTERPRETATION METIER")
print("=" * 70)

print(f"""
Analyse des resultats:

Vrais Positifs (TP={tp}):
  Fraudes correctement detectees → OBJECTIF PRINCIPAL

Faux Positifs (FP={fp}):
  Transactions normales signalees comme suspectes
  → Cout: verification manuelle, client mecontent
  → Taux: {fp/(tn+fp)*100:.1f}% des normales

Faux Negatifs (FN={fn}):
  Fraudes non detectees → RISQUE MAJEUR
  → Cout: perte financiere, dommage reputation
  → Taux: {fn/(fn+tp)*100:.1f}% des fraudes manquees

Vrais Negatifs (TN={tn}):
  Transactions normales correctement validees
""")

# ========== 6. BENCHMARKS ==========
print("-" * 50)
print("6. BENCHMARKS DETECTION DE FRAUDE")
print("-" * 50)

print(f"""
┌──────────────────────────────────────────────────────────┐
│  Metrique     │ Notre modele │ Industrie (typique)      │
├──────────────────────────────────────────────────────────┤
│  Precision    │    {precision*100:5.1f}%    │    70-90%                │
│  Recall       │    {recall*100:5.1f}%    │    80-95% (priorite)     │
│  F1-Score     │    {f1*100:5.1f}%    │    75-90%                │
└──────────────────────────────────────────────────────────┘

Note: En production, on combine souvent:
- Autoencoder pour detection unsupervised
- Regles metier (montant > X, heure inhabituelle)
- Modele supervise si labels disponibles
""")


# Visualiser l'espace latent
# Type: Code executable
from sklearn.neural_network import MLPRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

print("=" * 70)
print("       EXPLORATION DE L'ESPACE LATENT")
print("=" * 70)
print("""
L'ESPACE LATENT est la representation COMPRIMEE des donnees.

Dans notre autoencoder: 4 dimensions → 2 dimensions (goulot)

On peut comparer avec PCA:
- PCA       : Maximise la VARIANCE (lineaire)
- Autoencoder: Minimise l'erreur de RECONSTRUCTION (non-lineaire)

Les deux creent des espaces 2D, mais avec des objectifs differents!
""")

# ========== 1. PREPARATION ==========
print("-" * 50)
print("1. PREPARATION DES DONNEES")
print("-" * 50)

feature_cols = ['transaction_amount', 'transaction_time', 'account_age', 'num_transactions']
X = df[feature_cols].values
y_true = df['is_anomaly'].values

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

print(f"   Donnees normalisees: {X_scaled.shape}")

# ========== 2. PCA POUR COMPARAISON ==========
print("\n" + "-" * 50)
print("2. REDUCTION PAR PCA (reference)")
print("-" * 50)

pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)

print(f"""
PCA: 4D → 2D

Variance expliquee:
- PC1: {pca.explained_variance_ratio_[0]*100:.1f}% de l'information
- PC2: {pca.explained_variance_ratio_[1]*100:.1f}% de l'information
- Total: {pca.explained_variance_ratio_.sum()*100:.1f}%

PCA est LINEAIRE: trouve les directions de variance max.
""")

# ========== 3. AUTOENCODER (SIMULATION) ==========
print("-" * 50)
print("3. ESPACE LATENT DE L'AUTOENCODER")
print("-" * 50)

print("""
Note: Avec MLPRegressor, on ne peut pas extraire directement
l'espace latent. En pratique, avec Keras/PyTorch:

# Encoder separe
encoder = Model(inputs, latent_layer)
z = encoder.predict(X)  # Espace latent

L'autoencoder apprend une representation OPTIMISEE pour
reconstruire les transactions normales.
""")

# ========== 4. VISUALISATION COMPARATIVE ==========
print("=" * 70)
print("4. VISUALISATION: PCA vs INTUITION AUTOENCODER")
print("=" * 70)

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

# --- Graphique 1: Espace PCA ---
normal_mask = y_true == 0
anomaly_mask = y_true == 1

axes[0].scatter(X_pca[normal_mask, 0], X_pca[normal_mask, 1],
               alpha=0.6, label=f'Normal (n={normal_mask.sum()})', color='#9B7AC4', s=40)
axes[0].scatter(X_pca[anomaly_mask, 0], X_pca[anomaly_mask, 1],
               alpha=0.9, label=f'Anomalie (n={anomaly_mask.sum()})', color='#F7E64D', s=120, marker='*', edgecolors='#e74c3c')

# Ajouter ellipse pour zone normale
from matplotlib.patches import Ellipse
normal_pca = X_pca[normal_mask]
mean_x, mean_y = normal_pca.mean(axis=0)
std_x, std_y = normal_pca.std(axis=0)
ellipse = Ellipse((mean_x, mean_y), width=4*std_x, height=4*std_y,
                  fill=False, linestyle='--', color='#9B7AC4', linewidth=2)
axes[0].add_patch(ellipse)

axes[0].set_xlabel('PC1 (variance principale)', fontsize=11)
axes[0].set_ylabel('PC2', fontsize=11)
axes[0].set_title('Espace PCA (2D)', fontsize=12, fontweight='bold')
axes[0].legend(loc='upper right')
axes[0].grid(True, alpha=0.3)

# --- Graphique 2: Intuition de l'espace latent ---
axes[1].text(0.5, 0.85, "ESPACE LATENT DE L'AUTOENCODER",
            ha='center', va='center', fontsize=14, fontweight='bold',
            transform=axes[1].transAxes)

axes[1].text(0.5, 0.65, "L'autoencoder comprime les donnees\npour RECONSTRUIRE, pas pour la variance.",
            ha='center', va='center', fontsize=11, transform=axes[1].transAxes)

axes[1].text(0.5, 0.45, "Les transactions NORMALES forment\nune zone dense et coherente.",
            ha='center', va='center', fontsize=11, transform=axes[1].transAxes, color='#9B7AC4')

axes[1].text(0.5, 0.25, "Les ANOMALIES sont eloignees\nde cette zone → erreur elevee!",
            ha='center', va='center', fontsize=11, transform=axes[1].transAxes, color='#e74c3c')

# Schema simple
circle = plt.Circle((0.5, 0.08), 0.05, color='#9B7AC4', alpha=0.3, transform=axes[1].transAxes)
axes[1].add_patch(circle)
axes[1].scatter([0.35, 0.7], [0.08, 0.08], color='#F7E64D', s=100, marker='*',
               transform=axes[1].transAxes, zorder=5)

axes[1].axis('off')
axes[1].set_title('Intuition Conceptuelle', fontsize=12, fontweight='bold')

plt.tight_layout()
plt.show()

# ========== 5. INTERPRETATION ==========
print("""
INTERPRETATION:

Dans l'espace PCA (gauche):
─────────────────────────────────────────
• L'ellipse montre la zone "normale" (±2 ecarts-types)
• Les anomalies (etoiles jaunes) sortent souvent de cette zone
• C'est une approximation de ce que fait l'autoencoder

Dans l'espace latent (conceptuel, droite):
─────────────────────────────────────────
• L'autoencoder cree sa propre representation optimisee
• Les normales sont "proches" dans cet espace
• Les anomalies sont "loin" → difficiles a reconstruire

AVANTAGE de l'autoencoder sur PCA:
→ Peut capturer des relations NON-LINEAIRES
→ Meilleure reconstruction des patterns complexes
""")


# Exercice: Optimiser le seuil
# Type: Exercice
# ================================================================
# EXERCICE: OPTIMISATION DU SEUIL DE DETECTION
# ================================================================
#
# OBJECTIF:
# Trouver le meilleur seuil pour equilibrer detection et fausses alertes.
#
# Le choix du seuil est un COMPROMIS:
# - Seuil BAS → Plus de detections, mais plus de faux positifs
# - Seuil HAUT → Moins de fausses alertes, mais on rate des fraudes
#
# VOTRE MISSION:
# 1. Tester plusieurs percentiles (80, 85, 90, 95, 99)
# 2. Calculer Precision, Recall, F1 pour chaque seuil
# 3. Visualiser les courbes pour choisir le meilleur compromis
# ================================================================

from sklearn.neural_network import MLPRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import precision_score, recall_score, f1_score

print("=" * 70)
print("       EXERCICE: OPTIMISATION DU SEUIL DE DETECTION")
print("=" * 70)

# --- Preparation (code fourni) ---
feature_cols = ['transaction_amount', 'transaction_time', 'account_age', 'num_transactions']
X = df[feature_cols].values
y_true = df['is_anomaly'].values

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
X_normal = X_scaled[y_true == 0]

autoencoder = MLPRegressor(hidden_layer_sizes=(2,), activation='relu', max_iter=1000, random_state=42)
autoencoder.fit(X_normal, X_normal)

X_reconstructed = autoencoder.predict(X_scaled)
reconstruction_error = np.mean((X_scaled - X_reconstructed) ** 2, axis=1)

print("   Autoencoder entraine et erreurs calculees!")
print(f"   {len(y_true)} transactions a evaluer")
print(f"   {(y_true == 1).sum()} vraies anomalies a detecter")

# ================================================================
# TODO 1: Definir les percentiles a tester
# ================================================================
# percentiles = [80, 85, 90, 95, 99]

# ================================================================
# TODO 2: Pour chaque percentile, calculer:
# - Le seuil correspondant
# - Les predictions (erreur > seuil = anomalie)
# - Precision, Recall, F1-Score
# ================================================================
# Indice: np.percentile(reconstruction_error[y_true == 0], p)

# ================================================================
# TODO 3: Visualiser les metriques avec un graphique a barres
# ================================================================
# Indice: plt.bar() avec plusieurs series

# ================================================================
# TODO 4: Identifier le meilleur seuil (F1 max)
# ================================================================


# Quand utiliser les autoencoders?
# Type: Code executable
print("=" * 70)
print("       GUIDE PRATIQUE: QUAND UTILISER LES AUTOENCODERS?")
print("=" * 70)
print("""
Les autoencoders sont des outils puissants mais pas universels.
Ce guide vous aide a choisir quand les utiliser... ou pas!
""")

# ========== 1. CAS D'USAGE RECOMMANDES ==========
print("=" * 70)
print("1. CAS D'USAGE OU LES AUTOENCODERS EXCELLENT")
print("=" * 70)

print("""
┌────────────────────────────────────────────────────────────────────┐
│  CAS D'USAGE              │  EXEMPLES CONCRETS        │ VERDICT   │
├────────────────────────────────────────────────────────────────────┤
│  Detection d'anomalies    │  Fraude, intrusion,       │  ★★★★★   │
│                           │  defauts industriels      │  IDEAL    │
├────────────────────────────────────────────────────────────────────┤
│  Compression de donnees   │  Images, audio,           │  ★★★★☆   │
│                           │  representations          │  TRES BON │
├────────────────────────────────────────────────────────────────────┤
│  Reduction dimensionnelle │  Visualisation,           │  ★★★★☆   │
│  non-lineaire             │  pre-traitement          │  TRES BON │
├────────────────────────────────────────────────────────────────────┤
│  Debruitage (denoising)   │  Photos degradees,        │  ★★★★☆   │
│                           │  signaux bruites          │  TRES BON │
├────────────────────────────────────────────────────────────────────┤
│  Generation (VAE)         │  Visages, art,            │  ★★★☆☆   │
│                           │  augmentation donnees     │  BON      │
└────────────────────────────────────────────────────────────────────┘
""")

# ========== 2. CAS OU D'AUTRES METHODES SONT MEILLEURES ==========
print("-" * 50)
print("2. CAS OU D'AUTRES METHODES SONT PREFERABLES")
print("-" * 50)

print("""
┌────────────────────────────────────────────────────────────────────┐
│  TACHE                    │  MEILLEUR CHOIX           │ POURQUOI  │
├────────────────────────────────────────────────────────────────────┤
│  Classification           │  CNN, Random Forest       │  Supervise│
│  supervisee               │                           │  = mieux  │
├────────────────────────────────────────────────────────────────────┤
│  Reduction lineaire       │  PCA                      │  +rapide  │
│  simple                   │                           │  +simple  │
├────────────────────────────────────────────────────────────────────┤
│  Generation haute         │  GAN, Diffusion           │  +realiste│
│  qualite                  │  Models                   │           │
├────────────────────────────────────────────────────────────────────┤
│  Petits datasets          │  Methodes classiques      │  Overfitb │
│  (< 1000 samples)         │                           │  risque   │
└────────────────────────────────────────────────────────────────────┘
""")

# ========== 3. AVANTAGES ET INCONVENIENTS ==========
print("=" * 70)
print("3. AVANTAGES ET INCONVENIENTS")
print("=" * 70)

print("""
AVANTAGES:
─────────────────────────────────────────
+ Apprentissage NON SUPERVISE (pas besoin de labels!)
+ Capture des relations NON-LINEAIRES (vs PCA lineaire)
+ Detection d'anomalies sans exemples d'anomalies
+ Architecture flexible (profondeur, taille latente)
+ Base pour generation (VAE) et transfer learning

INCONVENIENTS:
─────────────────────────────────────────
- Plus LENT a entrainer que PCA ou methodes classiques
- Nombreux HYPERPARAMETRES a tuner (architecture, lr, epochs)
- Risque d'OVERFITTING sur petits datasets
- INTERPRETABILITE limitee (boite noire)
- Necessite GPU pour gros datasets/architectures
""")

# ========== 4. ARBRE DE DECISION ==========
print("-" * 50)
print("4. ARBRE DE DECISION: AUTOENCODER OU PAS?")
print("-" * 50)

print("""
START: Ai-je des LABELS pour mon probleme?
│
├─ OUI → Classification/Regression SUPERVISEE
│         → Utilisez: RandomForest, XGBoost, CNN...
│
└─ NON → Probleme NON SUPERVISE
         │
         ├─ Detection d'anomalies?
         │   │
         │   ├─ OUI → AUTOENCODER recommande!
         │   │        (ou Isolation Forest, One-Class SVM)
         │   │
         │   └─ NON → Reduction de dimensionnalite?
         │            │
         │            ├─ Lineaire suffit → PCA (plus rapide)
         │            │
         │            └─ Non-lineaire → AUTOENCODER
         │                              (ou t-SNE, UMAP pour visu)
         │
         └─ Generation de donnees?
             │
             ├─ Simple → VAE (Variational Autoencoder)
             │
             └─ Haute qualite → GAN, Diffusion Models
""")

# ========== 5. RESUME PRATIQUE ==========
print("=" * 70)
print("5. RESUME PRATIQUE")
print("=" * 70)

print("""
┌─────────────────────────────────────────────────────────────────┐
│                    RESUME: AUTOENCODERS                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  UTILISEZ un autoencoder quand:                                │
│  ✓ Pas de labels disponibles                                   │
│  ✓ Detection d'anomalies                                       │
│  ✓ Reduction de dimension non-lineaire                         │
│  ✓ Debruitage ou compression                                   │
│                                                                 │
│  EVITEZ un autoencoder quand:                                  │
│  ✗ Vous avez des labels (utilisez supervise)                   │
│  ✗ Relations lineaires suffisent (utilisez PCA)                │
│  ✗ Tres petit dataset (risque overfitting)                     │
│  ✗ Besoin d'interpretabilite forte                             │
│                                                                 │
│  VARIANTES a connaitre:                                        │
│  • Denoising AE  : Robuste au bruit                            │
│  • Variational AE: Generation de nouvelles donnees             │
│  • Sparse AE     : Features interpretables                     │
│  • Contractive AE: Representations stables                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

CONCLUSION:
─────────────────────────────────────────
Les autoencoders sont un outil PUISSANT pour l'apprentissage
non supervise, particulierement en DETECTION D'ANOMALIES.

Leur force: apprendre ce qui est "normal" sans supervision,
puis detecter ce qui est "different".

Felicitations! Vous avez termine ce module sur les autoencoders.
Vous savez maintenant:
• Construire et entrainer un autoencoder
• Detecter des anomalies par erreur de reconstruction
• Choisir le seuil optimal
• Decider quand utiliser cette technique
""")

