"""
Module: DBSCAN
Catégorie : Apprentissage non supervisé
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('clustering_2d.csv')

# Explorer les données
# Type: Code exécutable
print("=" * 70)
print("      EXPLORATION DES DONNEES POUR DBSCAN")
print("=" * 70)

print("\n" + "=" * 70)
print("1. APERCU DU DATASET")
print("=" * 70)
print("""
DBSCAN est un algorithme de clustering BASE SUR LA DENSITE.
Il trouve des regions denses separees par des regions creuses.

Contrairement a K-Means, DBSCAN:
  • Ne necessite pas de specifier le nombre de clusters
  • Peut trouver des clusters de formes arbitraires
  • Detecte automatiquement les outliers (bruit)
""")
display(df.head(10), title="Dataset de Clustering")

print(f"\n  Dimensions du dataset:")
print(f"    • {df.shape[0]} points")
print(f"    • {df.shape[1]} features (x, y)")

print("\n" + "=" * 70)
print("2. STATISTIQUES DE BASE")
print("=" * 70)
print(f"\n{'Feature':<10} | {'Min':^10} | {'Max':^10} | {'Moyenne':^10} | {'Std':^10}")
print("-" * 55)
for col in ['x', 'y']:
    print(f"{col:<10} | {df[col].min():^10.2f} | {df[col].max():^10.2f} | {df[col].mean():^10.2f} | {df[col].std():^10.2f}")

print("\n" + "=" * 70)
print("3. VISUALISATION INITIALE")
print("=" * 70)
print("""
Regardons les donnees sans aucun clustering.
Y voyez-vous des groupes? Des points isoles?
""")

plt.figure(figsize=(10, 6))
plt.scatter(df['x'], df['y'], alpha=0.6, color='#9B7AC4', s=40, edgecolors='black')
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Donnees de Clustering (structure a decouvrir)')
plt.grid(True, alpha=0.3)
plt.show()

print("""
Questions a se poser:
  • Combien de groupes distincts voyez-vous?
  • Y a-t-il des points isoles (outliers potentiels)?
  • Les groupes sont-ils spheriques ou de formes irregulieres?

DBSCAN va repondre a toutes ces questions automatiquement!
""")


# Appliquer DBSCAN
# Type: Code exécutable
from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler

print("=" * 70)
print("         APPLICATION DE DBSCAN")
print("=" * 70)

# Preparer les donnees
X = df[['x', 'y']].values

print("\n" + "=" * 70)
print("1. NORMALISATION DES DONNEES")
print("=" * 70)
print("""
IMPORTANT: Le parametre eps est une DISTANCE.
Si les features ont des echelles differentes, la normalisation
est cruciale pour que eps ait un sens coherent.
""")

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

print(f"  Avant normalisation: X dans [{X[:, 0].min():.1f}, {X[:, 0].max():.1f}]")
print(f"  Apres normalisation: X dans [{X_scaled[:, 0].min():.2f}, {X_scaled[:, 0].max():.2f}]")

print("\n" + "=" * 70)
print("2. CONFIGURATION DE DBSCAN")
print("=" * 70)
print("""
Les deux parametres cles de DBSCAN:

• eps (epsilon): Rayon de voisinage
  - Trop petit → trop de clusters et d'outliers
  - Trop grand → tous les points dans un seul cluster

• min_samples: Minimum de points pour former un core point
  - Trop petit → sensible au bruit
  - Trop grand → trop d'outliers

Parametres choisis pour cette demonstration:
""")
eps_val = 0.5
min_samples_val = 5
print(f"    • eps = {eps_val}")
print(f"    • min_samples = {min_samples_val}")

print("\n" + "=" * 70)
print("3. EXECUTION DE DBSCAN")
print("=" * 70)

import time
start = time.time()

dbscan = DBSCAN(eps=eps_val, min_samples=min_samples_val)
labels = dbscan.fit_predict(X_scaled)

exec_time = (time.time() - start) * 1000

print(f"  DBSCAN execute en {exec_time:.2f} ms")

print("\n" + "=" * 70)
print("4. RESULTATS DU CLUSTERING")
print("=" * 70)

n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
n_noise = (labels == -1).sum()

print(f"""
RESULTATS:
  • Nombre de clusters trouves: {n_clusters}
  • Points identifies comme bruit (outliers): {n_noise} ({n_noise/len(X)*100:.1f}%)
  • Points dans des clusters: {len(X) - n_noise} ({(len(X) - n_noise)/len(X)*100:.1f}%)
""")

print("\n  Distribution des labels:")
print("  " + "-" * 40)
for label in sorted(set(labels)):
    count = (labels == label).sum()
    pct = count / len(labels) * 100
    name = "Bruit (outliers)" if label == -1 else f"Cluster {label}"
    bar = "█" * int(pct / 3)
    print(f"    {name:<18}: {count:4d} ({pct:5.1f}%)  {bar}")

print("\n" + "=" * 70)
print("INTERPRETATION")
print("=" * 70)
print(f"""
DBSCAN a trouve {n_clusters} clusters AUTOMATIQUEMENT!

Labels speciaux:
  • -1 = BRUIT (point isole, outlier)
  • 0, 1, 2, ... = Numeros de clusters

Avantage majeur: DBSCAN n'a pas besoin qu'on lui dise
combien de clusters chercher - il les decouvre!
""")


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