"""
Module: Réseau de neurones from scratch
Catégorie : Deep learning
Difficulté : Intermédiaire

Généré 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')

# Charger et visualiser les données
# Type: Code exécutable
print("=" * 70)
print("   EXPLORATION DES DONNEES DE CLASSIFICATION BINAIRE")
print("   Pour l'entrainement de notre reseau de neurones")
print("=" * 70)

# =================================================================
# 1. APERCU DES DONNEES
# =================================================================
print("\n" + "-" * 40)
print("1. APERCU DU DATASET")
print("-" * 40)
print("""
Ce dataset contient des exemples pour un probleme de classification binaire.
Chaque exemple a 2 features et appartient a une classe (0 ou 1).
Notre reseau de neurones devra apprendre a distinguer les deux classes.
""")
display(df.head(10), title="Premiers exemples du dataset")

# =================================================================
# 2. DIMENSIONS ET STRUCTURE
# =================================================================
print("\n" + "-" * 40)
print("2. DIMENSIONS ET STRUCTURE")
print("-" * 40)
n_samples, n_features = df.shape[0], 2
print(f"""
Nombre total d'exemples : {n_samples}
Nombre de features      : {n_features} (feature1, feature2)
Variable cible          : label (0 ou 1)

En termes de reseau de neurones:
- Couche d'entree  : {n_features} neurones (un par feature)
- Couche de sortie : 1 neurone (probabilite de classe 1)
""")

# =================================================================
# 3. DISTRIBUTION DES CLASSES
# =================================================================
print("\n" + "-" * 40)
print("3. EQUILIBRE DES CLASSES")
print("-" * 40)
class_counts = df['label'].value_counts().sort_index()
total = len(df)
print(f"""
Classe 0 : {class_counts[0]:3d} exemples ({class_counts[0]/total*100:.1f}%)
Classe 1 : {class_counts[1]:3d} exemples ({class_counts[1]/total*100:.1f}%)
""")

ratio = min(class_counts) / max(class_counts)
if ratio > 0.8:
    print("  [OK] Classes bien equilibrees!")
    print("       Le reseau ne sera pas biaise vers une classe.")
elif ratio > 0.5:
    print("  [ATTENTION] Leger desequilibre.")
    print("              Surveiller la precision par classe.")
else:
    print("  [ATTENTION] Desequilibre significatif!")
    print("              Considerer des techniques de reequilibrage.")

# =================================================================
# 4. STATISTIQUES DES FEATURES
# =================================================================
print("\n" + "-" * 40)
print("4. STATISTIQUES DES FEATURES")
print("-" * 40)
print("""
Avant d'entrainer un reseau de neurones, il est crucial de comprendre
l'echelle des donnees. Des features avec des echelles tres differentes
peuvent ralentir ou empecher la convergence.
""")
for col in ['feature1', 'feature2']:
    data = df[col]
    print(f"  {col}:")
    print(f"    Min = {data.min():.3f} | Max = {data.max():.3f}")
    print(f"    Moyenne = {data.mean():.3f} | Ecart-type = {data.std():.3f}")
    print()

# =================================================================
# 5. VISUALISATION 2D
# =================================================================
print("\n" + "-" * 40)
print("5. VISUALISATION DES CLASSES")
print("-" * 40)
print("""
Le graphique ci-dessous montre la repartition des deux classes.
Si les classes sont bien separees, le reseau apprendra facilement.
Si elles se chevauchent beaucoup, la tache sera plus difficile.
""")

plt.figure(figsize=(10, 6))
colors = ['#9B7AC4' if l == 0 else '#F7E64D' for l in df['label']]
plt.scatter(df['feature1'], df['feature2'], c=colors, alpha=0.6,
            edgecolors='black', s=60)
plt.xlabel('Feature 1', fontsize=12)
plt.ylabel('Feature 2', fontsize=12)
plt.title('Donnees de classification binaire\n(Violet=Classe 0, Jaune=Classe 1)',
          fontsize=14)
plt.grid(True, alpha=0.3)
plt.legend(handles=[
    plt.scatter([], [], c='#9B7AC4', edgecolors='black', label='Classe 0'),
    plt.scatter([], [], c='#F7E64D', edgecolors='black', label='Classe 1')
], loc='upper right')
plt.tight_layout()
plt.show()

# =================================================================
# CONCLUSION
# =================================================================
print("\n" + "=" * 70)
print("SYNTHESE POUR L'ENTRAINEMENT")
print("=" * 70)
print(f"""
- {n_samples} exemples disponibles pour l'apprentissage
- 2 features en entree --> 1 probabilite en sortie
- Classes equilibrees: {'OUI' if ratio > 0.8 else 'NON'}
- Les features ont des echelles similaires (normalisation recommandee)

Prochaine etape: Normaliser les donnees et diviser en train/test.
""")


# Implémenter les fonctions d'activation
# Type: Code exécutable
print("=" * 70)
print("   FONCTIONS D'ACTIVATION: LE COEUR DE LA NON-LINEARITE")
print("=" * 70)

# =================================================================
# 1. DEFINITION DES FONCTIONS
# =================================================================
print("\n" + "-" * 40)
print("1. IMPLEMENTATION DES FONCTIONS")
print("-" * 40)

def sigmoid(z):
    """Fonction sigmoid: compresse les valeurs entre 0 et 1"""
    return 1 / (1 + np.exp(-np.clip(z, -500, 500)))

def sigmoid_derivative(z):
    """Derivee de sigmoid pour le backpropagation"""
    s = sigmoid(z)
    return s * (1 - s)

def relu(z):
    """Fonction ReLU: max(0, z)"""
    return np.maximum(0, z)

def relu_derivative(z):
    """Derivee de ReLU"""
    return (z > 0).astype(float)

print("""
Nous avons defini 4 fonctions essentielles:

SIGMOID σ(z) = 1/(1+e^(-z))
- Compresse toute valeur entre 0 et 1
- Utile en sortie pour obtenir une probabilite
- Probleme: gradient tres faible aux extremites

RELU relu(z) = max(0, z)
- Simple: garde les positifs, annule les negatifs
- Efficace pour les couches cachees
- Resout le probleme du "vanishing gradient"

DERIVEES (pour backpropagation):
- sigmoid'(z) = sigmoid(z) * (1 - sigmoid(z))
- relu'(z) = 1 si z > 0, sinon 0
""")

# =================================================================
# 2. DEMONSTRATION AVEC DES VALEURS
# =================================================================
print("\n" + "-" * 40)
print("2. EXEMPLES DE CALCULS")
print("-" * 40)

test_values = [-2, -1, 0, 1, 2, 5]
print("""
Voyons comment chaque fonction transforme differentes valeurs:
""")
print(f"{'z':>8} | {'sigmoid(z)':>12} | {'relu(z)':>10} | {'d_sigmoid':>12} | {'d_relu':>10}")
print("-" * 60)
for z in test_values:
    sig = sigmoid(z)
    rel = relu(z)
    sig_d = sigmoid_derivative(z)
    rel_d = relu_derivative(np.array([z]))[0]
    print(f"{z:>8} | {sig:>12.4f} | {rel:>10.1f} | {sig_d:>12.4f} | {rel_d:>10.1f}")

print("""
Observations importantes:
- Sigmoid: z=-2 donne 0.12, z=+2 donne 0.88 (compression!)
- ReLU: z negatif donne 0, z positif reste inchange
- Derivee sigmoid max a z=0 (0.25), decroit aux extremites
- Derivee ReLU: 0 ou 1 seulement (gradient constant ou nul)
""")

# =================================================================
# 3. VISUALISATION GRAPHIQUE
# =================================================================
print("\n" + "-" * 40)
print("3. VISUALISATION DES FONCTIONS")
print("-" * 40)
print("""
Les graphiques ci-dessous montrent les fonctions et leurs derivees.
Comprendre ces courbes est essentiel pour le backpropagation!
""")

z = np.linspace(-5, 5, 100)

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

# Sigmoid
axes[0].plot(z, sigmoid(z), 'b-', linewidth=2.5, label='sigmoid(z)')
axes[0].plot(z, sigmoid_derivative(z), 'r--', linewidth=2, label="sigmoid'(z)")
axes[0].axhline(y=0.5, color='gray', linestyle=':', alpha=0.5, label='y=0.5')
axes[0].axhline(y=0, color='k', linewidth=0.5)
axes[0].axhline(y=1, color='gray', linestyle=':', alpha=0.5, label='y=1')
axes[0].axvline(x=0, color='k', linewidth=0.5)
axes[0].fill_between(z, sigmoid_derivative(z), alpha=0.2, color='red')
axes[0].set_title('Fonction Sigmoid', fontsize=14, fontweight='bold')
axes[0].set_xlabel('z (pre-activation)')
axes[0].set_ylabel('Valeur')
axes[0].legend(loc='upper left')
axes[0].grid(True, alpha=0.3)
axes[0].set_ylim(-0.1, 1.1)

# ReLU
axes[1].plot(z, relu(z), 'b-', linewidth=2.5, label='relu(z)')
axes[1].plot(z, relu_derivative(z), 'r--', linewidth=2, label="relu'(z)")
axes[1].axhline(y=0, color='k', linewidth=0.5)
axes[1].axvline(x=0, color='k', linewidth=0.5)
axes[1].set_title('Fonction ReLU', fontsize=14, fontweight='bold')
axes[1].set_xlabel('z (pre-activation)')
axes[1].set_ylabel('Valeur')
axes[1].legend(loc='upper left')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# =================================================================
# INTERPRETATION PEDAGOGIQUE
# =================================================================
print("\n" + "-" * 40)
print("INTERPRETATION")
print("-" * 40)
print("""
SIGMOID:
- Zone centrale (|z| < 2): gradient significatif, apprentissage actif
- Zones extremes (|z| > 4): gradient quasi-nul ("saturation")
--> Probleme: les neurones peuvent "mourir" (arret d'apprentissage)

RELU:
- z > 0: gradient constant de 1, apprentissage stable
- z < 0: gradient nul, neurone inactif
--> Solution au vanishing gradient, mais "dying ReLU" possible

EN PRATIQUE:
- Couches cachees: ReLU (rapide, efficace)
- Sortie binaire: Sigmoid (probabilite entre 0 et 1)
- Sortie multi-classe: Softmax (non couvert ici)
""")


# ----------------------------------------------------------------------
# La suite de ce module demande un compte.
# Les cellules de code restantes ne sont pas dans ce fichier.
# ----------------------------------------------------------------------
