# Fichier: python_cheats/cheatsheets/scikit-learn.txt
# Cheatsheet Scikit-learn - Machine Learning Classique


[OK] INSTALLATION & IMPORTS

# Installation
pip install scikit-learn
pip install scikit-learn numpy pandas matplotlib

# Imports de base
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Imports scikit-learn courants
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix

# Version
import sklearn
print(sklearn.__version__)


[OK] WORKFLOW MACHINE LEARNING STANDARD

# 1. Charger les données
from sklearn.datasets import load_iris
data = load_iris()
X, y = data.data, data.target

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

# 3. Prétraiter (optionnel)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

# 4. Créer et entraîner le modèle
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(random_state=42)
model.fit(X_train, y_train)

# 5. Prédire
y_pred = model.predict(X_test)

# 6. Évaluer
from sklearn.metrics import accuracy_score
accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.4f}")


[OK] DATASETS (JEUX DE DONNÉES)

# === Datasets intégrés ===

from sklearn import datasets

# Classification
iris = datasets.load_iris()                    # 3 classes, 4 features
digits = datasets.load_digits()                # 10 classes (0-9), images 8x8
wine = datasets.load_wine()                    # 3 classes, 13 features
breast_cancer = datasets.load_breast_cancer()  # 2 classes, 30 features

# Régression
boston = datasets.load_boston()                # DEPRECATED (utilisez fetch_california_housing)
diabetes = datasets.load_diabetes()            # Régression, 10 features
california = datasets.fetch_california_housing()  # Prix maisons Californie

# Structure d'un dataset
X = iris.data          # Features (array numpy)
y = iris.target        # Labels (array numpy)
names = iris.feature_names    # Noms des features
target_names = iris.target_names  # Noms des classes
description = iris.DESCR   # Description

# === Générer données synthétiques ===

# Classification binaire
from sklearn.datasets import make_classification
X, y = make_classification(
    n_samples=1000,      # Nombre d'échantillons
    n_features=20,       # Nombre de features
    n_informative=15,    # Features informatives
    n_redundant=5,       # Features redondantes
    n_classes=2,         # Nombre de classes
    random_state=42
)

# Classification multi-classe
X, y = make_classification(
    n_samples=1000,
    n_features=20,
    n_informative=15,
    n_classes=3,
    n_clusters_per_class=2,
    random_state=42
)

# Régression
from sklearn.datasets import make_regression
X, y = make_regression(
    n_samples=1000,
    n_features=10,
    n_informative=5,
    noise=10.0,
    random_state=42
)

# Blobs (clustering)
from sklearn.datasets import make_blobs
X, y = make_blobs(
    n_samples=500,
    n_features=2,
    centers=4,
    cluster_std=1.0,
    random_state=42
)

# Cercles concentriques
from sklearn.datasets import make_circles
X, y = make_circles(
    n_samples=500,
    noise=0.05,
    factor=0.5,
    random_state=42
)

# Lunes (moons)
from sklearn.datasets import make_moons
X, y = make_moons(
    n_samples=500,
    noise=0.1,
    random_state=42
)

# === Charger depuis fichiers ===

# CSV avec pandas
import pandas as pd
df = pd.read_csv('data.csv')
X = df.drop('target', axis=1).values
y = df['target'].values

# Fonction load_svmlight_file pour format sparse
from sklearn.datasets import load_svmlight_file
X, y = load_svmlight_file('data.txt')


[OK] PREPROCESSING (PRÉTRAITEMENT)

# === Scaling (mise à l'échelle) ===

# StandardScaler (moyenne=0, écart-type=1)
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# MinMaxScaler (valeurs entre 0 et 1)
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X_train)

# MinMaxScaler avec range personnalisé
scaler = MinMaxScaler(feature_range=(-1, 1))
X_scaled = scaler.fit_transform(X_train)

# RobustScaler (robuste aux outliers)
from sklearn.preprocessing import RobustScaler
scaler = RobustScaler()
X_scaled = scaler.fit_transform(X_train)

# MaxAbsScaler (valeurs entre -1 et 1, préserve zéros)
from sklearn.preprocessing import MaxAbsScaler
scaler = MaxAbsScaler()
X_scaled = scaler.fit_transform(X_train)

# Normalizer (norme L2=1 par échantillon)
from sklearn.preprocessing import Normalizer
normalizer = Normalizer(norm='l2')  # 'l1', 'l2', 'max'
X_normalized = normalizer.fit_transform(X)

# === Encoding (encodage variables catégorielles) ===

# LabelEncoder (pour target)
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
y_encoded = le.fit_transform(y)
y_decoded = le.inverse_transform(y_encoded)
print(le.classes_)  # Voir les classes

# OrdinalEncoder (pour features ordinales)
from sklearn.preprocessing import OrdinalEncoder
oe = OrdinalEncoder()
X_encoded = oe.fit_transform(X_categorical)

# OneHotEncoder (pour features nominales)
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(sparse_output=False)
X_onehot = ohe.fit_transform(X_categorical)

# OneHotEncoder avec noms de colonnes
ohe = OneHotEncoder(sparse_output=False)
X_onehot = ohe.fit_transform(X_categorical)
feature_names = ohe.get_feature_names_out(['col1', 'col2'])

# LabelBinarizer (pour target multi-label)
from sklearn.preprocessing import LabelBinarizer
lb = LabelBinarizer()
y_binary = lb.fit_transform(y)

# === Imputation (valeurs manquantes) ===

# SimpleImputer
from sklearn.impute import SimpleImputer

# Moyenne
imputer = SimpleImputer(strategy='mean')
X_imputed = imputer.fit_transform(X)

# Médiane
imputer = SimpleImputer(strategy='median')
X_imputed = imputer.fit_transform(X)

# Mode (plus fréquent)
imputer = SimpleImputer(strategy='most_frequent')
X_imputed = imputer.fit_transform(X)

# Constante
imputer = SimpleImputer(strategy='constant', fill_value=0)
X_imputed = imputer.fit_transform(X)

# KNNImputer (basé sur k-plus proches voisins)
from sklearn.impute import KNNImputer
imputer = KNNImputer(n_neighbors=5)
X_imputed = imputer.fit_transform(X)

# === Transformation polynomiale ===

from sklearn.preprocessing import PolynomialFeatures

# Degré 2
poly = PolynomialFeatures(degree=2, include_bias=False)
X_poly = poly.fit_transform(X)

# Avec interactions seulement
poly = PolynomialFeatures(degree=2, interaction_only=True)
X_poly = poly.fit_transform(X)

# === Binarization ===

from sklearn.preprocessing import Binarizer
binarizer = Binarizer(threshold=0.5)
X_binary = binarizer.fit_transform(X)

# === Transformation personnalisée ===

from sklearn.preprocessing import FunctionTransformer

# Log transform
log_transformer = FunctionTransformer(np.log1p, validate=True)
X_log = log_transformer.fit_transform(X)

# Power transform
from sklearn.preprocessing import PowerTransformer
pt = PowerTransformer(method='yeo-johnson')  # ou 'box-cox'
X_transformed = pt.fit_transform(X)

# Quantile transform
from sklearn.preprocessing import QuantileTransformer
qt = QuantileTransformer(output_distribution='normal')
X_transformed = qt.fit_transform(X)


[OK] MODEL SELECTION (DIVISION DES DONNÉES)

# === Train/Test Split ===

from sklearn.model_selection import train_test_split

# Split basique (80% train, 20% test)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# Avec stratification (préserve distribution des classes)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

# Train/Validation/Test split
X_train, X_temp, y_train, y_temp = train_test_split(
    X, y, test_size=0.3, random_state=42
)
X_val, X_test, y_val, y_test = train_test_split(
    X_temp, y_temp, test_size=0.5, random_state=42
)
# Résultat: 70% train, 15% validation, 15% test

# === Cross-Validation ===

from sklearn.model_selection import cross_val_score

# K-Fold Cross-Validation
scores = cross_val_score(model, X, y, cv=5)  # 5 folds
print(f"Scores: {scores}")
print(f"Mean: {scores.mean():.4f} (+/- {scores.std():.4f})")

# Avec métrique spécifique
scores = cross_val_score(
    model, X, y, cv=5, scoring='accuracy'
)

# Métriques multiples
from sklearn.model_selection import cross_validate
results = cross_validate(
    model, X, y, cv=5,
    scoring=['accuracy', 'precision', 'recall'],
    return_train_score=True
)
print(results['test_accuracy'])

# === Stratégies de Cross-Validation ===

from sklearn.model_selection import (
    KFold, StratifiedKFold, GroupKFold,
    TimeSeriesSplit, LeaveOneOut
)

# KFold
kf = KFold(n_splits=5, shuffle=True, random_state=42)
for train_idx, test_idx in kf.split(X):
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]

# StratifiedKFold (préserve distribution des classes)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for train_idx, test_idx in skf.split(X, y):
    X_train, X_test = X[train_idx], X[test_idx]

# GroupKFold (pour données groupées)
gkf = GroupKFold(n_splits=5)
groups = np.array([0, 0, 1, 1, 2, 2, 3, 3])  # Groupes
for train_idx, test_idx in gkf.split(X, y, groups):
    X_train, X_test = X[train_idx], X[test_idx]

# TimeSeriesSplit (pour séries temporelles)
tscv = TimeSeriesSplit(n_splits=5)
for train_idx, test_idx in tscv.split(X):
    X_train, X_test = X[train_idx], X[test_idx]

# LeaveOneOut (LOOCV)
loo = LeaveOneOut()
scores = cross_val_score(model, X, y, cv=loo)


[OK] CLASSIFICATION

# === Régression Logistique ===

from sklearn.linear_model import LogisticRegression

# Basique
lr = LogisticRegression(random_state=42)
lr.fit(X_train, y_train)

# Avec régularisation
lr = LogisticRegression(
    penalty='l2',        # 'l1', 'l2', 'elasticnet', None
    C=1.0,              # Inverse de la régularisation (plus petit = plus de régularisation)
    solver='lbfgs',     # 'lbfgs', 'liblinear', 'newton-cg', 'sag', 'saga'
    max_iter=100,
    random_state=42
)
lr.fit(X_train, y_train)

# Multi-classe
lr = LogisticRegression(
    multi_class='multinomial',  # 'ovr', 'multinomial'
    random_state=42
)

# Accéder aux coefficients
print(lr.coef_)           # Coefficients
print(lr.intercept_)      # Intercept
print(lr.classes_)        # Classes

# === Support Vector Machines (SVM) ===

from sklearn.svm import SVC, LinearSVC

# SVC avec kernel RBF
svm = SVC(kernel='rbf', C=1.0, gamma='scale', random_state=42)
svm.fit(X_train, y_train)

# Kernels disponibles
svm_linear = SVC(kernel='linear')      # Linéaire
svm_poly = SVC(kernel='poly', degree=3)  # Polynomial
svm_rbf = SVC(kernel='rbf')            # RBF (Gaussian)
svm_sigmoid = SVC(kernel='sigmoid')    # Sigmoid

# LinearSVC (plus rapide pour grandes données)
svm = LinearSVC(C=1.0, max_iter=1000, random_state=42)
svm.fit(X_train, y_train)

# Avec probabilités
svm = SVC(kernel='rbf', probability=True, random_state=42)
svm.fit(X_train, y_train)
proba = svm.predict_proba(X_test)

# === K-Nearest Neighbors (KNN) ===

from sklearn.neighbors import KNeighborsClassifier

# KNN basique
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_train, y_train)

# Avec paramètres
knn = KNeighborsClassifier(
    n_neighbors=5,
    weights='uniform',    # 'uniform', 'distance'
    algorithm='auto',     # 'auto', 'ball_tree', 'kd_tree', 'brute'
    metric='minkowski',   # 'euclidean', 'manhattan', 'minkowski'
    p=2                   # p=1: Manhattan, p=2: Euclidean
)
knn.fit(X_train, y_train)

# === Decision Trees ===

from sklearn.tree import DecisionTreeClassifier

# Arbre de décision
dt = DecisionTreeClassifier(random_state=42)
dt.fit(X_train, y_train)

# Avec contrôle de la complexité
dt = DecisionTreeClassifier(
    max_depth=5,              # Profondeur maximale
    min_samples_split=2,      # Minimum échantillons pour split
    min_samples_leaf=1,       # Minimum échantillons par feuille
    max_features=None,        # Nombre max de features à considérer
    criterion='gini',         # 'gini', 'entropy', 'log_loss'
    random_state=42
)
dt.fit(X_train, y_train)

# Visualiser l'arbre
from sklearn.tree import plot_tree
plt.figure(figsize=(20,10))
plot_tree(dt, filled=True, feature_names=feature_names, class_names=class_names)
plt.show()

# Exporter l'arbre
from sklearn.tree import export_text
tree_rules = export_text(dt, feature_names=feature_names)
print(tree_rules)

# Feature importance
importances = dt.feature_importances_
print(importances)

# === Random Forest ===

from sklearn.ensemble import RandomForestClassifier

# Random Forest basique
rf = RandomForestClassifier(random_state=42)
rf.fit(X_train, y_train)

# Avec paramètres
rf = RandomForestClassifier(
    n_estimators=100,         # Nombre d'arbres
    max_depth=None,           # Profondeur maximale
    min_samples_split=2,
    min_samples_leaf=1,
    max_features='sqrt',      # 'sqrt', 'log2', None, int, float
    bootstrap=True,           # Bootstrap samples
    oob_score=False,          # Out-of-bag score
    n_jobs=-1,                # Utiliser tous les CPU
    random_state=42
)
rf.fit(X_train, y_train)

# Feature importance
importances = rf.feature_importances_
indices = np.argsort(importances)[::-1]
for i in range(X.shape[1]):
    print(f"{i+1}. feature {indices[i]} ({importances[indices[i]]:.4f})")

# Out-of-bag score
rf = RandomForestClassifier(oob_score=True, random_state=42)
rf.fit(X_train, y_train)
print(f"OOB Score: {rf.oob_score_:.4f}")

# === Gradient Boosting ===

from sklearn.ensemble import GradientBoostingClassifier

# Gradient Boosting
gb = GradientBoostingClassifier(random_state=42)
gb.fit(X_train, y_train)

# Avec paramètres
gb = GradientBoostingClassifier(
    n_estimators=100,
    learning_rate=0.1,
    max_depth=3,
    min_samples_split=2,
    min_samples_leaf=1,
    subsample=1.0,            # Fraction d'échantillons
    random_state=42
)
gb.fit(X_train, y_train)

# === AdaBoost ===

from sklearn.ensemble import AdaBoostClassifier

# AdaBoost avec Decision Tree
ada = AdaBoostClassifier(
    estimator=DecisionTreeClassifier(max_depth=1),
    n_estimators=50,
    learning_rate=1.0,
    random_state=42
)
ada.fit(X_train, y_train)

# === Naive Bayes ===

from sklearn.naive_bayes import (
    GaussianNB, MultinomialNB, BernoulliNB, ComplementNB
)

# Gaussian (features continues)
gnb = GaussianNB()
gnb.fit(X_train, y_train)

# Multinomial (comptages, fréquences)
mnb = MultinomialNB(alpha=1.0)  # alpha: Laplace smoothing
mnb.fit(X_train, y_train)

# Bernoulli (features binaires)
bnb = BernoulliNB(alpha=1.0, binarize=0.0)
bnb.fit(X_train, y_train)

# Complement (bon pour datasets déséquilibrés)
cnb = ComplementNB(alpha=1.0)
cnb.fit(X_train, y_train)

# === XGBoost (bibliothèque externe) ===

# Installation: pip install xgboost
from xgboost import XGBClassifier

xgb = XGBClassifier(
    n_estimators=100,
    max_depth=3,
    learning_rate=0.1,
    random_state=42
)
xgb.fit(X_train, y_train)


[OK] RÉGRESSION

# === Régression Linéaire ===

from sklearn.linear_model import LinearRegression

# Régression linéaire simple
lr = LinearRegression()
lr.fit(X_train, y_train)

# Accéder aux coefficients
print(f"Coefficients: {lr.coef_}")
print(f"Intercept: {lr.intercept_}")

# Prédire
y_pred = lr.predict(X_test)

# === Ridge Regression (L2) ===

from sklearn.linear_model import Ridge

# Ridge avec régularisation L2
ridge = Ridge(alpha=1.0)  # alpha: force de régularisation
ridge.fit(X_train, y_train)

# === Lasso Regression (L1) ===

from sklearn.linear_model import Lasso

# Lasso avec régularisation L1 (feature selection)
lasso = Lasso(alpha=1.0, max_iter=1000)
lasso.fit(X_train, y_train)

# Voir quelles features sont éliminées
print(f"Non-zero coefficients: {np.sum(lasso.coef_ != 0)}")

# === ElasticNet (L1 + L2) ===

from sklearn.linear_model import ElasticNet

# ElasticNet combine L1 et L2
elasticnet = ElasticNet(
    alpha=1.0,          # Force de régularisation
    l1_ratio=0.5,       # Mix L1/L2: 0=Ridge, 1=Lasso
    max_iter=1000
)
elasticnet.fit(X_train, y_train)

# === Polynomial Regression ===

from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline

# Pipeline avec transformation polynomiale
poly_model = Pipeline([
    ('poly', PolynomialFeatures(degree=2)),
    ('linear', LinearRegression())
])
poly_model.fit(X_train, y_train)

# === Decision Tree Regression ===

from sklearn.tree import DecisionTreeRegressor

# Arbre de décision pour régression
dt_reg = DecisionTreeRegressor(
    max_depth=5,
    min_samples_split=2,
    min_samples_leaf=1,
    random_state=42
)
dt_reg.fit(X_train, y_train)

# === Random Forest Regression ===

from sklearn.ensemble import RandomForestRegressor

# Random Forest pour régression
rf_reg = RandomForestRegressor(
    n_estimators=100,
    max_depth=None,
    min_samples_split=2,
    n_jobs=-1,
    random_state=42
)
rf_reg.fit(X_train, y_train)

# === Gradient Boosting Regression ===

from sklearn.ensemble import GradientBoostingRegressor

# Gradient Boosting pour régression
gb_reg = GradientBoostingRegressor(
    n_estimators=100,
    learning_rate=0.1,
    max_depth=3,
    random_state=42
)
gb_reg.fit(X_train, y_train)

# === Support Vector Regression ===

from sklearn.svm import SVR

# SVR
svr = SVR(kernel='rbf', C=1.0, epsilon=0.1)
svr.fit(X_train, y_train)

# === KNN Regression ===

from sklearn.neighbors import KNeighborsRegressor

# KNN pour régression
knn_reg = KNeighborsRegressor(
    n_neighbors=5,
    weights='uniform'  # 'uniform', 'distance'
)
knn_reg.fit(X_train, y_train)


[OK] CLUSTERING

# === K-Means ===

from sklearn.cluster import KMeans

# K-Means basique
kmeans = KMeans(n_clusters=3, random_state=42)
clusters = kmeans.fit_predict(X)

# Avec paramètres
kmeans = KMeans(
    n_clusters=3,
    init='k-means++',     # 'k-means++', 'random'
    n_init=10,            # Nombre d'initialisations
    max_iter=300,
    random_state=42
)
clusters = kmeans.fit_predict(X)

# Accéder aux centres et labels
centers = kmeans.cluster_centers_
labels = kmeans.labels_
inertia = kmeans.inertia_  # Somme distances au carré

# Prédire cluster pour nouveaux points
new_clusters = kmeans.predict(X_new)

# Méthode du coude (elbow method)
inertias = []
K_range = range(1, 11)
for k in K_range:
    kmeans = KMeans(n_clusters=k, random_state=42)
    kmeans.fit(X)
    inertias.append(kmeans.inertia_)

plt.plot(K_range, inertias, 'bo-')
plt.xlabel('Nombre de clusters')
plt.ylabel('Inertia')
plt.title('Méthode du coude')
plt.show()

# === DBSCAN ===

from sklearn.cluster import DBSCAN

# DBSCAN (Density-Based Spatial Clustering)
dbscan = DBSCAN(
    eps=0.5,              # Distance maximale entre points
    min_samples=5,        # Minimum points pour un cluster
    metric='euclidean'
)
clusters = dbscan.fit_predict(X)

# -1 indique bruit/outliers
n_clusters = len(set(clusters)) - (1 if -1 in clusters else 0)
n_noise = list(clusters).count(-1)
print(f"Clusters: {n_clusters}, Noise: {n_noise}")

# === Hierarchical Clustering ===

from sklearn.cluster import AgglomerativeClustering

# Clustering hiérarchique
agg = AgglomerativeClustering(
    n_clusters=3,
    linkage='ward'        # 'ward', 'complete', 'average', 'single'
)
clusters = agg.fit_predict(X)

# Dendrogram (avec scipy)
from scipy.cluster.hierarchy import dendrogram, linkage
Z = linkage(X, method='ward')
plt.figure(figsize=(10, 5))
dendrogram(Z)
plt.show()

# === Gaussian Mixture Models ===

from sklearn.mixture import GaussianMixture

# GMM
gmm = GaussianMixture(
    n_components=3,
    covariance_type='full',  # 'full', 'tied', 'diag', 'spherical'
    random_state=42
)
clusters = gmm.fit_predict(X)

# Probabilités d'appartenance
proba = gmm.predict_proba(X)

# Critères de sélection de modèle
aic = gmm.aic(X)  # Akaike Information Criterion
bic = gmm.bic(X)  # Bayesian Information Criterion

# === Mean Shift ===

from sklearn.cluster import MeanShift, estimate_bandwidth

# Estimer bandwidth
bandwidth = estimate_bandwidth(X, quantile=0.2, n_samples=500)

# Mean Shift
ms = MeanShift(bandwidth=bandwidth)
clusters = ms.fit_predict(X)

# === Spectral Clustering ===

from sklearn.cluster import SpectralClustering

# Spectral Clustering
sc = SpectralClustering(
    n_clusters=3,
    affinity='rbf',       # 'rbf', 'nearest_neighbors'
    random_state=42
)
clusters = sc.fit_predict(X)


[OK] DIMENSIONALITY REDUCTION

# === PCA (Principal Component Analysis) ===

from sklearn.decomposition import PCA

# PCA
pca = PCA(n_components=2)  # Réduire à 2 dimensions
X_pca = pca.fit_transform(X)

# Variance expliquée
print(f"Variance expliquée: {pca.explained_variance_ratio_}")
print(f"Variance totale: {pca.explained_variance_ratio_.sum():.4f}")

# Composantes principales
print(f"Composantes: {pca.components_}")

# Choisir nombre de composantes (95% variance)
pca = PCA(n_components=0.95)  # Garde 95% de la variance
X_pca = pca.fit_transform(X)
print(f"Nombre de composantes: {pca.n_components_}")

# Inverse transform
X_reconstructed = pca.inverse_transform(X_pca)

# === t-SNE ===

from sklearn.manifold import TSNE

# t-SNE (pour visualisation)
tsne = TSNE(
    n_components=2,
    perplexity=30,        # 5-50 typiquement
    learning_rate=200,
    n_iter=1000,
    random_state=42
)
X_tsne = tsne.fit_transform(X)

# Visualiser
plt.scatter(X_tsne[:, 0], X_tsne[:, 1], c=y, cmap='viridis')
plt.colorbar()
plt.show()

# === UMAP (nécessite umap-learn) ===

# Installation: pip install umap-learn
from umap import UMAP

# UMAP
umap = UMAP(
    n_components=2,
    n_neighbors=15,
    min_dist=0.1,
    random_state=42
)
X_umap = umap.fit_transform(X)

# === Truncated SVD ===

from sklearn.decomposition import TruncatedSVD

# SVD (pour matrices sparse)
svd = TruncatedSVD(n_components=50, random_state=42)
X_svd = svd.fit_transform(X)

# === NMF (Non-negative Matrix Factorization) ===

from sklearn.decomposition import NMF

# NMF (valeurs non-négatives uniquement)
nmf = NMF(n_components=10, random_state=42)
W = nmf.fit_transform(X)  # Features réduites
H = nmf.components_        # Composantes

# === LDA (Linear Discriminant Analysis) ===

from sklearn.discriminant_analysis import LinearDiscriminantAnalysis

# LDA (supervisé, max n_classes-1 composantes)
lda = LinearDiscriminantAnalysis(n_components=2)
X_lda = lda.fit_transform(X, y)

# === Feature Selection ===

from sklearn.feature_selection import (
    SelectKBest, SelectPercentile, 
    chi2, f_classif, mutual_info_classif
)

# SelectKBest (k meilleures features)
selector = SelectKBest(score_func=f_classif, k=10)
X_selected = selector.fit_transform(X, y)
selected_features = selector.get_support()

# SelectPercentile (pourcentage des meilleures)
selector = SelectPercentile(score_func=chi2, percentile=20)
X_selected = selector.fit_transform(X, y)

# Recursive Feature Elimination (RFE)
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestClassifier

estimator = RandomForestClassifier(random_state=42)
rfe = RFE(estimator=estimator, n_features_to_select=5)
X_rfe = rfe.fit_transform(X, y)
print(f"Selected features: {rfe.support_}")
print(f"Feature ranking: {rfe.ranking_}")


[OK] MÉTRIQUES D'ÉVALUATION

# === Classification Metrics ===

from sklearn.metrics import (
    accuracy_score, precision_score, recall_score,
    f1_score, classification_report, confusion_matrix,
    roc_auc_score, roc_curve, precision_recall_curve
)

# Accuracy
accuracy = accuracy_score(y_test, y_pred)
print(f"Accuracy: {accuracy:.4f}")

# Precision, Recall, F1-Score
precision = precision_score(y_test, y_pred, average='weighted')
recall = recall_score(y_test, y_pred, average='weighted')
f1 = f1_score(y_test, y_pred, average='weighted')

# Average options: 'binary', 'micro', 'macro', 'weighted', 'samples'

# Classification Report
report = classification_report(y_test, y_pred)
print(report)

# Avec noms de classes
report = classification_report(
    y_test, y_pred, 
    target_names=['Class 0', 'Class 1', 'Class 2']
)

# Confusion Matrix
cm = confusion_matrix(y_test, y_pred)
print(cm)

# Visualiser la matrice de confusion
import seaborn as sns
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.ylabel('True Label')
plt.xlabel('Predicted Label')
plt.show()

# ROC AUC Score
# Pour classification binaire
y_proba = model.predict_proba(X_test)[:, 1]
auc = roc_auc_score(y_test, y_proba)
print(f"AUC: {auc:.4f}")

# Pour multi-classe
auc = roc_auc_score(y_test, y_proba_multiclass, multi_class='ovr')

# ROC Curve
fpr, tpr, thresholds = roc_curve(y_test, y_proba)
plt.plot(fpr, tpr, label=f'AUC = {auc:.4f}')
plt.plot([0, 1], [0, 1], 'k--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.legend()
plt.show()

# Precision-Recall Curve
precision, recall, thresholds = precision_recall_curve(y_test, y_proba)
plt.plot(recall, precision)
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.show()

# Matthews Correlation Coefficient
from sklearn.metrics import matthews_corrcoef
mcc = matthews_corrcoef(y_test, y_pred)

# Cohen's Kappa
from sklearn.metrics import cohen_kappa_score
kappa = cohen_kappa_score(y_test, y_pred)

# Log Loss
from sklearn.metrics import log_loss
logloss = log_loss(y_test, y_proba)

# === Regression Metrics ===

from sklearn.metrics import (
    mean_squared_error, mean_absolute_error,
    r2_score, mean_absolute_percentage_error
)

# Mean Squared Error
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
print(f"MSE: {mse:.4f}, RMSE: {rmse:.4f}")

# Mean Absolute Error
mae = mean_absolute_error(y_test, y_pred)
print(f"MAE: {mae:.4f}")

# R² Score
r2 = r2_score(y_test, y_pred)
print(f"R²: {r2:.4f}")

# Mean Absolute Percentage Error
mape = mean_absolute_percentage_error(y_test, y_pred)
print(f"MAPE: {mape:.4f}")

# Max Error
from sklearn.metrics import max_error
max_err = max_error(y_test, y_pred)

# Explained Variance Score
from sklearn.metrics import explained_variance_score
evs = explained_variance_score(y_test, y_pred)

# === Clustering Metrics ===

from sklearn.metrics import (
    silhouette_score, calinski_harabasz_score,
    davies_bouldin_score, adjusted_rand_score
)

# Silhouette Score ([-1, 1], plus haut = mieux)
silhouette = silhouette_score(X, clusters)
print(f"Silhouette Score: {silhouette:.4f}")

# Calinski-Harabasz Index (plus haut = mieux)
ch_score = calinski_harabasz_score(X, clusters)

# Davies-Bouldin Index (plus bas = mieux)
db_score = davies_bouldin_score(X, clusters)

# Adjusted Rand Index (avec vraies labels)
ari = adjusted_rand_score(true_labels, clusters)

# Normalized Mutual Information
from sklearn.metrics import normalized_mutual_info_score
nmi = normalized_mutual_info_score(true_labels, clusters)


[OK] HYPERPARAMETER TUNING

# === Grid Search ===

from sklearn.model_selection import GridSearchCV

# Définir grille de paramètres
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [3, 5, 7, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4]
}

# Grid Search
grid_search = GridSearchCV(
    estimator=RandomForestClassifier(random_state=42),
    param_grid=param_grid,
    cv=5,                    # Cross-validation folds
    scoring='accuracy',      # Métrique
    n_jobs=-1,              # Parallélisation
    verbose=2               # Afficher progression
)
grid_search.fit(X_train, y_train)

# Meilleurs paramètres
print(f"Best parameters: {grid_search.best_params_}")
print(f"Best score: {grid_search.best_score_:.4f}")

# Meilleur modèle
best_model = grid_search.best_estimator_

# Tous les résultats
results = pd.DataFrame(grid_search.cv_results_)
print(results[['params', 'mean_test_score', 'std_test_score']])

# === Random Search ===

from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform

# Distributions de paramètres
param_distributions = {
    'n_estimators': randint(50, 500),
    'max_depth': randint(3, 20),
    'min_samples_split': randint(2, 20),
    'min_samples_leaf': randint(1, 10),
    'max_features': uniform(0.1, 0.9)
}

# Random Search
random_search = RandomizedSearchCV(
    estimator=RandomForestClassifier(random_state=42),
    param_distributions=param_distributions,
    n_iter=100,              # Nombre d'itérations
    cv=5,
    scoring='accuracy',
    n_jobs=-1,
    random_state=42,
    verbose=2
)
random_search.fit(X_train, y_train)

print(f"Best parameters: {random_search.best_params_}")
print(f"Best score: {random_search.best_score_:.4f}")

# === Validation Curve ===

from sklearn.model_selection import validation_curve

# Analyser impact d'un paramètre
param_range = np.arange(1, 20)
train_scores, test_scores = validation_curve(
    RandomForestClassifier(random_state=42),
    X, y,
    param_name='max_depth',
    param_range=param_range,
    cv=5,
    scoring='accuracy'
)

# Visualiser
train_mean = np.mean(train_scores, axis=1)
test_mean = np.mean(test_scores, axis=1)
plt.plot(param_range, train_mean, label='Training score')
plt.plot(param_range, test_mean, label='Validation score')
plt.xlabel('max_depth')
plt.ylabel('Score')
plt.legend()
plt.show()

# === Learning Curve ===

from sklearn.model_selection import learning_curve

# Analyser si plus de données aiderait
train_sizes, train_scores, test_scores = learning_curve(
    RandomForestClassifier(random_state=42),
    X, y,
    cv=5,
    n_jobs=-1,
    train_sizes=np.linspace(0.1, 1.0, 10),
    scoring='accuracy'
)

# Visualiser
train_mean = np.mean(train_scores, axis=1)
test_mean = np.mean(test_scores, axis=1)
plt.plot(train_sizes, train_mean, label='Training score')
plt.plot(train_sizes, test_mean, label='Validation score')
plt.xlabel('Training Set Size')
plt.ylabel('Score')
plt.legend()
plt.show()


[OK] PIPELINES

# === Pipeline Simple ===

from sklearn.pipeline import Pipeline

# Créer pipeline
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('classifier', RandomForestClassifier(random_state=42))
])

# Fit et predict
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)

# === Pipeline avec Preprocessing ===

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

# Pipeline complet
pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='mean')),
    ('scaler', StandardScaler()),
    ('pca', PCA(n_components=10)),
    ('classifier', LogisticRegression(random_state=42))
])

pipeline.fit(X_train, y_train)

# Accéder aux étapes
scaler = pipeline.named_steps['scaler']
pca = pipeline.named_steps['pca']

# === ColumnTransformer ===

from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder

# Features numériques et catégorielles
numeric_features = ['age', 'salary']
categorical_features = ['city', 'department']

# Transformations différentes par type
preprocessor = ColumnTransformer(
    transformers=[
        ('num', StandardScaler(), numeric_features),
        ('cat', OneHotEncoder(), categorical_features)
    ]
)

# Pipeline avec ColumnTransformer
pipeline = Pipeline([
    ('preprocessor', preprocessor),
    ('classifier', RandomForestClassifier(random_state=42))
])

pipeline.fit(X_train, y_train)

# === FeatureUnion ===

from sklearn.pipeline import FeatureUnion

# Combiner plusieurs transformations
feature_union = FeatureUnion([
    ('pca', PCA(n_components=10)),
    ('select', SelectKBest(k=5))
])

# Dans un pipeline
pipeline = Pipeline([
    ('features', feature_union),
    ('classifier', LogisticRegression())
])

# === Pipeline avec Grid Search ===

# Paramètres avec préfixe du nom de l'étape
param_grid = {
    'pca__n_components': [5, 10, 15],
    'classifier__C': [0.1, 1.0, 10.0],
    'classifier__penalty': ['l1', 'l2']
}

pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('pca', PCA()),
    ('classifier', LogisticRegression(solver='saga'))
])

grid_search = GridSearchCV(pipeline, param_grid, cv=5)
grid_search.fit(X_train, y_train)


[OK] ENSEMBLE METHODS

# === Voting Classifier ===

from sklearn.ensemble import VotingClassifier

# Hard voting (majorité)
voting_clf = VotingClassifier(
    estimators=[
        ('lr', LogisticRegression()),
        ('rf', RandomForestClassifier()),
        ('svc', SVC(probability=True))
    ],
    voting='hard'  # 'hard' ou 'soft'
)
voting_clf.fit(X_train, y_train)

# Soft voting (moyenne des probabilités)
voting_clf = VotingClassifier(
    estimators=[
        ('lr', LogisticRegression()),
        ('rf', RandomForestClassifier()),
        ('svc', SVC(probability=True))
    ],
    voting='soft',
    weights=[1, 2, 1]  # Poids optionnels
)
voting_clf.fit(X_train, y_train)

# === Stacking Classifier ===

from sklearn.ensemble import StackingClassifier

# Base learners
estimators = [
    ('rf', RandomForestClassifier(n_estimators=10, random_state=42)),
    ('svc', SVC(probability=True, random_state=42)),
    ('lr', LogisticRegression(random_state=42))
]

# Meta-learner
stacking_clf = StackingClassifier(
    estimators=estimators,
    final_estimator=LogisticRegression(),
    cv=5
)
stacking_clf.fit(X_train, y_train)

# === Bagging Classifier ===

from sklearn.ensemble import BaggingClassifier

# Bagging avec Decision Tree
bagging_clf = BaggingClassifier(
    estimator=DecisionTreeClassifier(),
    n_estimators=50,
    max_samples=1.0,        # Fraction d'échantillons
    max_features=1.0,       # Fraction de features
    bootstrap=True,         # Bootstrap samples
    bootstrap_features=False,
    n_jobs=-1,
    random_state=42
)
bagging_clf.fit(X_train, y_train)

# === Extra Trees ===

from sklearn.ensemble import ExtraTreesClassifier

# Extra Trees (plus aléatoire que Random Forest)
extra_trees = ExtraTreesClassifier(
    n_estimators=100,
    max_depth=None,
    random_state=42
)
extra_trees.fit(X_train, y_train)


[OK] GESTION DES DONNÉES DÉSÉQUILIBRÉES

# === Class Weights ===

# Automatique
model = RandomForestClassifier(
    class_weight='balanced',
    random_state=42
)
model.fit(X_train, y_train)

# Manuel
class_weights = {0: 1, 1: 10}  # Classe 1 a poids 10x
model = LogisticRegression(class_weight=class_weights)

# === Resampling avec imbalanced-learn ===

# Installation: pip install imbalanced-learn
from imblearn.over_sampling import SMOTE, RandomOverSampler
from imblearn.under_sampling import RandomUnderSampler

# SMOTE (Synthetic Minority Over-sampling)
smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)

# Random Over-Sampling
ros = RandomOverSampler(random_state=42)
X_resampled, y_resampled = ros.fit_resample(X_train, y_train)

# Random Under-Sampling
rus = RandomUnderSampler(random_state=42)
X_resampled, y_resampled = rus.fit_resample(X_train, y_train)

# === Pipeline avec SMOTE ===

from imblearn.pipeline import Pipeline as ImbPipeline

pipeline = ImbPipeline([
    ('smote', SMOTE(random_state=42)),
    ('scaler', StandardScaler()),
    ('classifier', RandomForestClassifier(random_state=42))
])

pipeline.fit(X_train, y_train)


[OK] OUTLIER DETECTION

# === Isolation Forest ===

from sklearn.ensemble import IsolationForest

# Isolation Forest
iso_forest = IsolationForest(
    contamination=0.1,     # Pourcentage d'outliers attendu
    random_state=42
)
outliers = iso_forest.fit_predict(X)
# -1 = outlier, 1 = inlier

# === Local Outlier Factor ===

from sklearn.neighbors import LocalOutlierFactor

# LOF
lof = LocalOutlierFactor(
    n_neighbors=20,
    contamination=0.1
)
outliers = lof.fit_predict(X)

# === One-Class SVM ===

from sklearn.svm import OneClassSVM

# One-Class SVM
oc_svm = OneClassSVM(
    kernel='rbf',
    gamma='auto',
    nu=0.1  # Proportion d'outliers
)
outliers = oc_svm.fit_predict(X)

# === Elliptic Envelope ===

from sklearn.covariance import EllipticEnvelope

# Elliptic Envelope (assume données gaussiennes)
ee = EllipticEnvelope(
    contamination=0.1,
    random_state=42
)
outliers = ee.fit_predict(X)


[OK] MODEL PERSISTENCE

# === Sauvegarder avec Joblib ===

import joblib

# Sauvegarder modèle
joblib.dump(model, 'model.pkl')

# Charger modèle
model = joblib.load('model.pkl')

# Avec compression
joblib.dump(model, 'model.pkl', compress=3)

# === Sauvegarder avec Pickle ===

import pickle

# Sauvegarder
with open('model.pkl', 'wb') as f:
    pickle.dump(model, f)

# Charger
with open('model.pkl', 'rb') as f:
    model = pickle.load(f)

# === Sauvegarder Pipeline Complet ===

# Pipeline avec preprocessing
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('pca', PCA(n_components=10)),
    ('classifier', RandomForestClassifier())
])
pipeline.fit(X_train, y_train)

# Sauvegarder pipeline
joblib.dump(pipeline, 'pipeline.pkl')

# Charger et utiliser
pipeline = joblib.load('pipeline.pkl')
predictions = pipeline.predict(X_new)


[OK] TEXT PROCESSING

# === CountVectorizer ===

from sklearn.feature_extraction.text import CountVectorizer

# Bag of Words
vectorizer = CountVectorizer(
    max_features=1000,      # Top 1000 mots
    min_df=2,               # Minimum document frequency
    max_df=0.8,             # Maximum document frequency
    stop_words='english'    # Supprimer stop words
)
X = vectorizer.fit_transform(documents)

# Vocabulaire
vocab = vectorizer.get_feature_names_out()

# === TfidfVectorizer ===

from sklearn.feature_extraction.text import TfidfVectorizer

# TF-IDF
tfidf = TfidfVectorizer(
    max_features=1000,
    min_df=2,
    max_df=0.8,
    stop_words='english',
    ngram_range=(1, 2)      # Unigrams et bigrams
)
X = tfidf.fit_transform(documents)

# === HashingVectorizer ===

from sklearn.feature_extraction.text import HashingVectorizer

# Hashing (pour très grandes données)
hasher = HashingVectorizer(
    n_features=2**10,
    alternate_sign=False
)
X = hasher.transform(documents)

# === Pipeline Text Classification ===

from sklearn.pipeline import Pipeline
from sklearn.naive_bayes import MultinomialNB

# Pipeline complet
text_clf = Pipeline([
    ('vect', CountVectorizer()),
    ('tfidf', TfidfTransformer()),
    ('clf', MultinomialNB())
])

text_clf.fit(X_train, y_train)
predictions = text_clf.predict(X_test)


[OK] CALIBRATION

# === Probability Calibration ===

from sklearn.calibration import CalibratedClassifierCV

# Calibrer les probabilités
calibrated_clf = CalibratedClassifierCV(
    estimator=RandomForestClassifier(random_state=42),
    method='sigmoid',  # 'sigmoid' ou 'isotonic'
    cv=5
)
calibrated_clf.fit(X_train, y_train)

# Probabilités calibrées
proba_calibrated = calibrated_clf.predict_proba(X_test)

# === Calibration Curve ===

from sklearn.calibration import calibration_curve

# Analyser calibration
prob_true, prob_pred = calibration_curve(
    y_test, y_proba, n_bins=10
)

plt.plot(prob_pred, prob_true, marker='o')
plt.plot([0, 1], [0, 1], 'k--')
plt.xlabel('Predicted Probability')
plt.ylabel('True Probability')
plt.show()


[OK] MULTI-LABEL CLASSIFICATION

# === Multi-Label Setup ===

from sklearn.multioutput import MultiOutputClassifier

# Multi-output classifier
multi_clf = MultiOutputClassifier(
    RandomForestClassifier(random_state=42)
)
multi_clf.fit(X_train, y_train_multilabel)

# Prédire plusieurs labels
y_pred_multilabel = multi_clf.predict(X_test)

# === Chain Classifier ===

from sklearn.multioutput import ClassifierChain

# Classifier Chain (labels dépendants)
chain_clf = ClassifierChain(
    RandomForestClassifier(random_state=42),
    order='random',
    random_state=42
)
chain_clf.fit(X_train, y_train_multilabel)

# === Multi-Label Metrics ===

from sklearn.metrics import hamming_loss, jaccard_score

# Hamming Loss
hamming = hamming_loss(y_test, y_pred)

# Jaccard Score
jaccard = jaccard_score(y_test, y_pred, average='samples')


[OK] BONNES PRATIQUES

# 1. Toujours séparer train/test AVANT preprocessing
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

# 2. Fit sur train, transform sur test
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)  # PAS fit_transform!

# 3. Utiliser pipelines
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('model', RandomForestClassifier())
])

# 4. Cross-validation pour évaluation robuste
scores = cross_val_score(model, X, y, cv=5)

# 5. Grid search avec pipeline
param_grid = {'model__n_estimators': [50, 100, 200]}
grid = GridSearchCV(pipeline, param_grid, cv=5)

# 6. Fixer random_state pour reproductibilité
model = RandomForestClassifier(random_state=42)

# 7. Sauvegarder le pipeline complet
joblib.dump(pipeline, 'model.pkl')

# 8. Documenter les hyperparamètres
best_params = {
    'n_estimators': 100,
    'max_depth': 10,
    'min_samples_split': 5
}

# 9. Évaluer sur plusieurs métriques
from sklearn.metrics import classification_report
print(classification_report(y_test, y_pred))

# 10. Gérer les données déséquilibrées
model = RandomForestClassifier(class_weight='balanced')


[OK] EXEMPLES COMPLETS

# === Classification Binaire Complète ===

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, roc_auc_score
from sklearn.pipeline import Pipeline
import joblib

# Charger données
data = load_breast_cancer()
X, y = data.data, data.target

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

# Pipeline
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('classifier', RandomForestClassifier(random_state=42))
])

# Grid search
param_grid = {
    'classifier__n_estimators': [50, 100, 200],
    'classifier__max_depth': [5, 10, None],
    'classifier__min_samples_split': [2, 5]
}

grid_search = GridSearchCV(
    pipeline, param_grid, cv=5, scoring='roc_auc', n_jobs=-1
)
grid_search.fit(X_train, y_train)

# Évaluer
best_model = grid_search.best_estimator_
y_pred = best_model.predict(X_test)
y_proba = best_model.predict_proba(X_test)[:, 1]

print(f"Best parameters: {grid_search.best_params_}")
print(f"\nClassification Report:")
print(classification_report(y_test, y_pred))
print(f"ROC AUC: {roc_auc_score(y_test, y_proba):.4f}")

# Sauvegarder
joblib.dump(best_model, 'breast_cancer_model.pkl')

# === Régression Complète ===

from sklearn.datasets import fetch_california_housing
from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score
from sklearn.metrics import mean_squared_error, r2_score

# Charger données
data = fetch_california_housing()
X, y = data.data, data.target

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

# Pipeline
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('regressor', Ridge())
])

# Grid search
param_grid = {
    'regressor__alpha': [0.1, 1.0, 10.0, 100.0]
}

grid_search = GridSearchCV(
    pipeline, param_grid, cv=5, scoring='neg_mean_squared_error'
)
grid_search.fit(X_train, y_train)

# Évaluer
best_model = grid_search.best_estimator_
y_pred = best_model.predict(X_test)

print(f"Best alpha: {grid_search.best_params_}")
print(f"RMSE: {np.sqrt(mean_squared_error(y_test, y_pred)):.4f}")
print(f"R²: {r2_score(y_test, y_pred):.4f}")

# === Clustering Complet ===

from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
import matplotlib.pyplot as plt

# Générer données
X, y_true = make_blobs(n_samples=500, centers=4, random_state=42)

# Méthode du coude
inertias = []
silhouettes = []
K_range = range(2, 11)

for k in K_range:
    kmeans = KMeans(n_clusters=k, random_state=42)
    kmeans.fit(X)
    inertias.append(kmeans.inertia_)
    silhouettes.append(silhouette_score(X, kmeans.labels_))

# Visualiser
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
ax1.plot(K_range, inertias, 'bo-')
ax1.set_xlabel('Number of clusters')
ax1.set_ylabel('Inertia')
ax1.set_title('Elbow Method')

ax2.plot(K_range, silhouettes, 'ro-')
ax2.set_xlabel('Number of clusters')
ax2.set_ylabel('Silhouette Score')
ax2.set_title('Silhouette Analysis')
plt.tight_layout()
plt.show()

# Modèle final
kmeans = KMeans(n_clusters=4, random_state=42)
clusters = kmeans.fit_predict(X)

# Visualiser résultats
plt.scatter(X[:, 0], X[:, 1], c=clusters, cmap='viridis')
plt.scatter(
    kmeans.cluster_centers_[:, 0],
    kmeans.cluster_centers_[:, 1],
    marker='x', s=200, c='red', linewidths=3
)
plt.title('K-Means Clustering')
plt.show()


[OK] RESSOURCES

# Documentation officielle:
# https://scikit-learn.org/stable/

# Tutoriels:
# https://scikit-learn.org/stable/tutorial/

# API Reference:
# https://scikit-learn.org/stable/modules/classes.html

# User Guide:
# https://scikit-learn.org/stable/user_guide.html

# Exemples:
# https://scikit-learn.org/stable/auto_examples/

# Cheat Sheet officiel:
# https://scikit-learn.org/stable/tutorial/machine_learning_map/


[OK] ALGORITHMES AVANCÉS

# === SGD Classifier (Stochastic Gradient Descent) ===

from sklearn.linear_model import SGDClassifier

# SGD pour classification (scalable)
sgd = SGDClassifier(
    loss='hinge',           # 'hinge', 'log', 'modified_huber', 'squared_hinge'
    penalty='l2',           # 'l1', 'l2', 'elasticnet'
    alpha=0.0001,           # Régularisation
    max_iter=1000,
    tol=1e-3,
    random_state=42
)
sgd.fit(X_train, y_train)

# Partial fit (apprentissage en ligne)
for batch in batches:
    X_batch, y_batch = batch
    sgd.partial_fit(X_batch, y_batch, classes=np.unique(y))

# === SGD Regressor ===

from sklearn.linear_model import SGDRegressor

# SGD pour régression
sgd_reg = SGDRegressor(
    loss='squared_error',   # 'squared_error', 'huber', 'epsilon_insensitive'
    penalty='l2',
    alpha=0.0001,
    max_iter=1000,
    random_state=42
)
sgd_reg.fit(X_train, y_train)

# === Perceptron ===

from sklearn.linear_model import Perceptron

# Perceptron classique
perceptron = Perceptron(
    max_iter=1000,
    tol=1e-3,
    random_state=42
)
perceptron.fit(X_train, y_train)

# === Passive Aggressive Classifier ===

from sklearn.linear_model import PassiveAggressiveClassifier

# Passive Aggressive (bon pour stream de données)
pa = PassiveAggressiveClassifier(
    C=1.0,
    max_iter=1000,
    random_state=42
)
pa.fit(X_train, y_train)

# === Quadratic Discriminant Analysis ===

from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis

# QDA (assume covariances différentes par classe)
qda = QuadraticDiscriminantAnalysis()
qda.fit(X_train, y_train)

# === Gaussian Process Classifier ===

from sklearn.gaussian_process import GaussianProcessClassifier
from sklearn.gaussian_process.kernels import RBF

# Gaussian Process
kernel = 1.0 * RBF(1.0)
gpc = GaussianProcessClassifier(
    kernel=kernel,
    random_state=42
)
gpc.fit(X_train, y_train)

# Avec incertitude
y_pred, y_std = gpc.predict(X_test, return_std=True)

# === Gaussian Process Regressor ===

from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import ConstantKernel, RBF, WhiteKernel

# Kernel composé
kernel = ConstantKernel(1.0) * RBF(length_scale=1.0) + WhiteKernel(noise_level=1.0)

gpr = GaussianProcessRegressor(
    kernel=kernel,
    alpha=1e-10,
    random_state=42
)
gpr.fit(X_train, y_train)

# Prédictions avec incertitude
y_pred, y_std = gpr.predict(X_test, return_std=True)

# Visualiser incertitude
plt.plot(X_test, y_pred, 'b-', label='Prediction')
plt.fill_between(
    X_test.ravel(),
    y_pred - 1.96 * y_std,
    y_pred + 1.96 * y_std,
    alpha=0.2, color='blue'
)
plt.show()

# === Neural Network (MLP) ===

from sklearn.neural_network import MLPClassifier, MLPRegressor

# MLP Classifier
mlp = MLPClassifier(
    hidden_layer_sizes=(100, 50),  # 2 couches: 100 et 50 neurones
    activation='relu',              # 'relu', 'tanh', 'logistic'
    solver='adam',                  # 'adam', 'sgd', 'lbfgs'
    alpha=0.0001,                   # Régularisation L2
    batch_size='auto',
    learning_rate='constant',       # 'constant', 'invscaling', 'adaptive'
    learning_rate_init=0.001,
    max_iter=200,
    random_state=42
)
mlp.fit(X_train, y_train)

# MLP Regressor
mlp_reg = MLPRegressor(
    hidden_layer_sizes=(100, 50),
    activation='relu',
    solver='adam',
    alpha=0.0001,
    max_iter=200,
    random_state=42
)
mlp_reg.fit(X_train, y_train)

# Courbe de perte
plt.plot(mlp.loss_curve_)
plt.xlabel('Iteration')
plt.ylabel('Loss')
plt.title('Training Loss')
plt.show()


[OK] TRAITEMENT DE SÉRIES TEMPORELLES

# === TimeSeriesSplit ===

from sklearn.model_selection import TimeSeriesSplit

# Split pour séries temporelles
tscv = TimeSeriesSplit(n_splits=5)

for i, (train_idx, test_idx) in enumerate(tscv.split(X)):
    print(f"Fold {i}:")
    print(f"  Train: {train_idx.min()} to {train_idx.max()}")
    print(f"  Test: {test_idx.min()} to {test_idx.max()}")
    
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]

# === Lag Features ===

import pandas as pd

# Créer features de lag
def create_lag_features(df, target_col, n_lags=5):
    for i in range(1, n_lags + 1):
        df[f'lag_{i}'] = df[target_col].shift(i)
    return df

# Exemple
df = pd.DataFrame({'value': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})
df = create_lag_features(df, 'value', n_lags=3)

# === Rolling Features ===

# Rolling mean, std, etc.
df['rolling_mean_3'] = df['value'].rolling(window=3).mean()
df['rolling_std_3'] = df['value'].rolling(window=3).std()
df['rolling_min_3'] = df['value'].rolling(window=3).min()
df['rolling_max_3'] = df['value'].rolling(window=3).max()


[OK] FEATURE ENGINEERING AVANCÉ

# === Interaction Features ===

from sklearn.preprocessing import PolynomialFeatures

# Créer interactions entre features
poly = PolynomialFeatures(degree=2, include_bias=False, interaction_only=True)
X_interactions = poly.fit_transform(X)

# Noms des features
feature_names = poly.get_feature_names_out(['f1', 'f2', 'f3'])

# === Target Encoding ===

# Encoder catégories par moyenne de la target
def target_encode(df, cat_col, target_col, smoothing=1.0):
    # Moyenne globale
    global_mean = df[target_col].mean()
    
    # Moyennes par catégorie
    agg = df.groupby(cat_col)[target_col].agg(['mean', 'count'])
    
    # Smoothing
    smooth = (agg['count'] * agg['mean'] + smoothing * global_mean) / (agg['count'] + smoothing)
    
    # Map
    return df[cat_col].map(smooth)

# === Binning ===

from sklearn.preprocessing import KBinsDiscretizer

# Discrétiser features continues
discretizer = KBinsDiscretizer(
    n_bins=5,
    encode='ordinal',  # 'ordinal', 'onehot', 'onehot-dense'
    strategy='quantile'  # 'uniform', 'quantile', 'kmeans'
)
X_binned = discretizer.fit_transform(X)

# === Custom Transformer ===

from sklearn.base import BaseEstimator, TransformerMixin

class LogTransformer(BaseEstimator, TransformerMixin):
    def fit(self, X, y=None):
        return self
    
    def transform(self, X):
        return np.log1p(X)

# Utiliser dans pipeline
pipeline = Pipeline([
    ('log', LogTransformer()),
    ('scaler', StandardScaler()),
    ('model', RandomForestRegressor())
])

# === Feature Hashing ===

from sklearn.feature_extraction import FeatureHasher

# Hashing de features (pour haute dimensionnalité)
hasher = FeatureHasher(n_features=10, input_type='string')
X_hashed = hasher.transform(feature_list)


[OK] VALIDATION AVANCÉE

# === Nested Cross-Validation ===

from sklearn.model_selection import cross_val_score, GridSearchCV

# CV externe
outer_cv = KFold(n_splits=5, shuffle=True, random_state=42)

# CV interne pour hyperparameter tuning
inner_cv = KFold(n_splits=3, shuffle=True, random_state=42)

# Grid search
param_grid = {'n_estimators': [50, 100], 'max_depth': [3, 5]}

# Nested CV
nested_scores = []
for train_idx, test_idx in outer_cv.split(X):
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]
    
    # Inner CV
    grid = GridSearchCV(
        RandomForestClassifier(random_state=42),
        param_grid,
        cv=inner_cv
    )
    grid.fit(X_train, y_train)
    
    # Évaluer sur test externe
    score = grid.score(X_test, y_test)
    nested_scores.append(score)

print(f"Nested CV Score: {np.mean(nested_scores):.4f} (+/- {np.std(nested_scores):.4f})")

# === Stratified Group K-Fold ===

from sklearn.model_selection import StratifiedGroupKFold

# Pour données groupées avec classes déséquilibrées
sgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=42)

for train_idx, test_idx in sgkf.split(X, y, groups):
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]

# === Repeated Cross-Validation ===

from sklearn.model_selection import RepeatedKFold, RepeatedStratifiedKFold

# Répéter CV plusieurs fois
rcv = RepeatedKFold(n_splits=5, n_repeats=10, random_state=42)
scores = cross_val_score(model, X, y, cv=rcv)

print(f"Score: {scores.mean():.4f} (+/- {scores.std():.4f})")


[OK] INTERPRÉTABILITÉ DES MODÈLES

# === Feature Importance ===

# Random Forest / Gradient Boosting
model = RandomForestClassifier(random_state=42)
model.fit(X_train, y_train)

importances = model.feature_importances_
indices = np.argsort(importances)[::-1]

# Visualiser
plt.figure(figsize=(10, 6))
plt.title('Feature Importances')
plt.bar(range(X.shape[1]), importances[indices])
plt.xticks(range(X.shape[1]), [f'Feature {i}' for i in indices], rotation=90)
plt.tight_layout()
plt.show()

# === Permutation Importance ===

from sklearn.inspection import permutation_importance

# Importance par permutation (modèle-agnostique)
perm_importance = permutation_importance(
    model, X_test, y_test,
    n_repeats=10,
    random_state=42,
    n_jobs=-1
)

# Visualiser
sorted_idx = perm_importance.importances_mean.argsort()
plt.figure(figsize=(10, 6))
plt.boxplot(
    perm_importance.importances[sorted_idx].T,
    vert=False,
    labels=[f'Feature {i}' for i in sorted_idx]
)
plt.xlabel('Permutation Importance')
plt.tight_layout()
plt.show()

# === Partial Dependence Plot ===

from sklearn.inspection import partial_dependence, PartialDependenceDisplay

# PDP pour 1 ou 2 features
features = [0, 1, (0, 1)]  # Feature 0, 1, et leur interaction
PartialDependenceDisplay.from_estimator(
    model, X, features,
    feature_names=['Feature 0', 'Feature 1']
)
plt.tight_layout()
plt.show()

# === SHAP Values (nécessite shap) ===

# Installation: pip install shap
import shap

# Explainer
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test)

# Summary plot
shap.summary_plot(shap_values, X_test, feature_names=feature_names)

# Force plot (une prédiction)
shap.force_plot(
    explainer.expected_value[1],
    shap_values[1][0],
    X_test[0],
    feature_names=feature_names
)

# === LIME (nécessite lime) ===

# Installation: pip install lime
import lime
import lime.lime_tabular

# Explainer
explainer = lime.lime_tabular.LimeTabularExplainer(
    X_train,
    feature_names=feature_names,
    class_names=['Class 0', 'Class 1'],
    mode='classification'
)

# Expliquer une prédiction
exp = explainer.explain_instance(
    X_test[0],
    model.predict_proba,
    num_features=10
)
exp.show_in_notebook()


[OK] OPTIMISATION BAYÉSIENNE

# === Scikit-Optimize ===

# Installation: pip install scikit-optimize
from skopt import BayesSearchCV
from skopt.space import Real, Integer

# Espaces de recherche
param_space = {
    'n_estimators': Integer(50, 500),
    'max_depth': Integer(3, 20),
    'min_samples_split': Integer(2, 20),
    'min_samples_leaf': Integer(1, 10),
    'max_features': Real(0.1, 1.0)
}

# Bayesian Optimization
bayes_search = BayesSearchCV(
    estimator=RandomForestClassifier(random_state=42),
    search_spaces=param_space,
    n_iter=50,              # Nombre d'itérations
    cv=5,
    n_jobs=-1,
    random_state=42
)
bayes_search.fit(X_train, y_train)

print(f"Best parameters: {bayes_search.best_params_}")
print(f"Best score: {bayes_search.best_score_:.4f}")


[OK] APPRENTISSAGE SEMI-SUPERVISÉ

# === Self-Training ===

from sklearn.semi_supervised import SelfTrainingClassifier

# Self-training (avec données non-labelisées)
# y_train avec -1 pour données non-labelisées
y_train_semi = y_train.copy()
y_train_semi[100:] = -1  # Marquer comme non-labelisé

self_training = SelfTrainingClassifier(
    base_estimator=RandomForestClassifier(random_state=42),
    threshold=0.75,        # Seuil de confiance
    criterion='threshold'  # 'threshold' ou 'k_best'
)
self_training.fit(X_train, y_train_semi)

# === Label Propagation ===

from sklearn.semi_supervised import LabelPropagation

# Label Propagation
label_prop = LabelPropagation(
    kernel='rbf',          # 'rbf' ou 'knn'
    gamma=20,
    max_iter=1000
)
label_prop.fit(X_train, y_train_semi)

# === Label Spreading ===

from sklearn.semi_supervised import LabelSpreading

# Label Spreading (version plus robuste)
label_spread = LabelSpreading(
    kernel='rbf',
    alpha=0.2,            # Clamping factor
    max_iter=1000
)
label_spread.fit(X_train, y_train_semi)


[OK] ONLINE LEARNING

# === Incremental Learning ===

from sklearn.linear_model import SGDClassifier

# Modèle online
online_model = SGDClassifier(random_state=42)

# Classes possibles
classes = np.array([0, 1, 2])

# Apprentissage par batch
batch_size = 100
for i in range(0, len(X_train), batch_size):
    X_batch = X_train[i:i+batch_size]
    y_batch = y_train[i:i+batch_size]
    
    # Partial fit
    online_model.partial_fit(X_batch, y_batch, classes=classes)
    
    # Évaluer périodiquement
    if i % 500 == 0:
        score = online_model.score(X_test, y_test)
        print(f"Batch {i}: Score = {score:.4f}")

# === Out-of-Core Learning ===

from sklearn.datasets import load_svmlight_file

def load_in_batches(filename, batch_size=1000):
    """Charger données par batch"""
    with open(filename) as f:
        while True:
            batch = []
            for _ in range(batch_size):
                line = f.readline()
                if not line:
                    break
                batch.append(line)
            
            if not batch:
                break
                
            # Parser batch
            X_batch, y_batch = parse_batch(batch)
            yield X_batch, y_batch

# Apprentissage
online_model = SGDClassifier()
for X_batch, y_batch in load_in_batches('large_dataset.txt'):
    online_model.partial_fit(X_batch, y_batch, classes=classes)


[OK] APPRENTISSAGE PAR RENFORCEMENT

# === Multi-Armed Bandits ===

from sklearn.linear_model import Ridge

class ThompsonSampling:
    """Thompson Sampling pour bandits"""
    
    def __init__(self, n_arms):
        self.n_arms = n_arms
        self.successes = np.ones(n_arms)
        self.failures = np.ones(n_arms)
    
    def select_arm(self):
        samples = np.random.beta(self.successes, self.failures)
        return np.argmax(samples)
    
    def update(self, arm, reward):
        if reward > 0:
            self.successes[arm] += 1
        else:
            self.failures[arm] += 1

# Utilisation
bandit = ThompsonSampling(n_arms=5)
for _ in range(1000):
    arm = bandit.select_arm()
    reward = get_reward(arm)  # Fonction de reward
    bandit.update(arm, reward)


[OK] MÉTAMODÈLES ET AUTOML

# === AutoML Simple ===

def simple_automl(X_train, X_test, y_train, y_test):
    """Teste plusieurs modèles et retourne le meilleur"""
    
    models = {
        'Logistic Regression': LogisticRegression(max_iter=1000),
        'Random Forest': RandomForestClassifier(),
        'Gradient Boosting': GradientBoostingClassifier(),
        'SVM': SVC(probability=True),
        'KNN': KNeighborsClassifier()
    }
    
    results = {}
    for name, model in models.items():
        # Entraîner
        model.fit(X_train, y_train)
        
        # Évaluer
        y_pred = model.predict(X_test)
        score = accuracy_score(y_test, y_pred)
        
        results[name] = {
            'model': pipeline,
            'RMSE': rmse,
            'MAE': mae,
            'R2': r2
        }
        
        print(f"{name}:")
        print(f"  RMSE: {rmse:.4f}")
        print(f"  MAE: {mae:.4f}")
        print(f"  R²: {r2:.4f}\n")
    
    # Meilleur modèle
    best_name = min(results, key=lambda k: results[k]['RMSE'])
    best_model = results[best_name]['model']
    
    print(f"Best model: {best_name}")
    
    # Visualiser prédictions
    y_pred_best = best_model.predict(X_test)
    
    plt.figure(figsize=(12, 5))
    
    plt.subplot(1, 2, 1)
    plt.scatter(y_test, y_pred_best, alpha=0.6)
    plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2)
    plt.xlabel('True Values')
    plt.ylabel('Predictions')
    plt.title(f'{best_name}: Predictions vs True Values')
    plt.grid(True, alpha=0.3)
    
    plt.subplot(1, 2, 2)
    residuals = y_test - y_pred_best
    plt.scatter(y_pred_best, residuals, alpha=0.6)
    plt.axhline(y=0, color='r', linestyle='--', lw=2)
    plt.xlabel('Predicted Values')
    plt.ylabel('Residuals')
    plt.title(f'{best_name}: Residual Plot')
    plt.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()
    
    return best_model, results

# Utilisation
# model, results = regression_pipeline(X, y)


[OK] RACCOURCIS ET ASTUCES

# === 1. Quick Model Comparison ===

def quick_comparison(X, y, cv=5):
    """Comparer rapidement plusieurs modèles"""
    from sklearn.linear_model import LogisticRegression
    from sklearn.tree import DecisionTreeClassifier
    from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
    from sklearn.svm import SVC
    from sklearn.neighbors import KNeighborsClassifier
    
    models = [
        ('Logistic Regression', LogisticRegression(max_iter=1000)),
        ('Decision Tree', DecisionTreeClassifier()),
        ('Random Forest', RandomForestClassifier()),
        ('Gradient Boosting', GradientBoostingClassifier()),
        ('SVM', SVC()),
        ('KNN', KNeighborsClassifier())
    ]
    
    results = []
    for name, model in models:
        scores = cross_val_score(model, X, y, cv=cv, scoring='accuracy')
        results.append({
            'Model': name,
            'Mean Score': scores.mean(),
            'Std': scores.std()
        })
    
    df = pd.DataFrame(results).sort_values('Mean Score', ascending=False)
    print(df.to_string(index=False))
    return df

# === 2. Quick Feature Importance ===

def quick_feature_importance(X, y, feature_names=None, top_n=10):
    """Feature importance rapide"""
    model = RandomForestClassifier(n_estimators=100, random_state=42)
    model.fit(X, y)
    
    if feature_names is None:
        feature_names = [f'Feature_{i}' for i in range(X.shape[1])]
    
    importances = pd.DataFrame({
        'Feature': feature_names,
        'Importance': model.feature_importances_
    }).sort_values('Importance', ascending=False).head(top_n)
    
    plt.figure(figsize=(10, 6))
    plt.barh(range(len(importances)), importances['Importance'])
    plt.yticks(range(len(importances)), importances['Feature'])
    plt.xlabel('Importance')
    plt.title(f'Top {top_n} Feature Importances')
    plt.gca().invert_yaxis()
    plt.tight_layout()
    plt.show()
    
    return importances

# === 3. Quick Classification Report ===

def quick_report(model, X_train, X_test, y_train, y_test):
    """Rapport rapide et complet"""
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    
    print("="*50)
    print("CLASSIFICATION REPORT")
    print("="*50)
    print(classification_report(y_test, y_pred))
    
    print("\nCONFUSION MATRIX")
    print("="*50)
    cm = confusion_matrix(y_test, y_pred)
    print(cm)
    
    if hasattr(model, 'predict_proba'):
        y_proba = model.predict_proba(X_test)[:, 1]
        auc = roc_auc_score(y_test, y_proba)
        print(f"\nROC AUC Score: {auc:.4f}")
    
    # Visualiser
    plt.figure(figsize=(6, 5))
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
    plt.ylabel('True Label')
    plt.xlabel('Predicted Label')
    plt.title('Confusion Matrix')
    plt.tight_layout()
    plt.show()

# === 4. Save/Load Helper ===

def save_model(model, filename, metadata=None):
    """Sauvegarder modèle avec métadonnées"""
    from datetime import datetime
    
    data = {
        'model': model,
        'saved_at': datetime.now().isoformat(),
        'sklearn_version': sklearn.__version__
    }
    
    if metadata:
        data.update(metadata)
    
    joblib.dump(data, filename)
    print(f"[OK] Model saved to {filename}")

def load_model(filename):
    """Charger modèle avec métadonnées"""
    data = joblib.load(filename)
    
    print(f"Model loaded:")
    print(f"  Saved at: {data.get('saved_at', 'Unknown')}")
    print(f"  Scikit-learn version: {data.get('sklearn_version', 'Unknown')}")
    
    if sklearn.__version__ != data.get('sklearn_version'):
        print("[ATTENTION]  Warning: Version mismatch!")
    
    return data['model'], data

# === 5. Data Quality Check ===

def check_data_quality(X, y=None):
    """Vérifier qualité des données"""
    print("DATA QUALITY REPORT")
    print("="*50)
    
    # Convert to DataFrame if needed
    if not isinstance(X, pd.DataFrame):
        X = pd.DataFrame(X, columns=[f'Feature_{i}' for i in range(X.shape[1])])
    
    print(f"\nShape: {X.shape}")
    print(f"Memory usage: {X.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
    
    # Valeurs manquantes
    missing = X.isnull().sum()
    if missing.sum() > 0:
        print("\n[ATTENTION]  Missing values:")
        print(missing[missing > 0])
    else:
        print("\n[OK] No missing values")
    
    # Duplicates
    n_duplicates = X.duplicated().sum()
    if n_duplicates > 0:
        print(f"\n[ATTENTION]  {n_duplicates} duplicate rows found")
    else:
        print("\n[OK] No duplicate rows")
    
    # Types de données
    print("\nData types:")
    print(X.dtypes.value_counts())
    
    # Statistiques
    print("\nNumeric features statistics:")
    print(X.describe())
    
    # Distribution target
    if y is not None:
        print("\nTarget distribution:")
        unique, counts = np.unique(y, return_counts=True)
        for val, count in zip(unique, counts):
            pct = count / len(y) * 100
            print(f"  Class {val}: {count} ({pct:.1f}%)")
        
        # Check imbalance
        imbalance_ratio = counts.max() / counts.min()
        if imbalance_ratio > 3:
            print(f"\n[ATTENTION]  Class imbalance detected (ratio: {imbalance_ratio:.1f}:1)")
            print("Consider using stratified splits and class weights")
    
    # Outliers (pour features numériques)
    numeric_cols = X.select_dtypes(include=[np.number]).columns
    outliers_count = {}
    for col in numeric_cols:
        Q1 = X[col].quantile(0.25)
        Q3 = X[col].quantile(0.75)
        IQR = Q3 - Q1
        outliers = ((X[col] < (Q1 - 1.5 * IQR)) | (X[col] > (Q3 + 1.5 * IQR))).sum()
        if outliers > 0:
            outliers_count[col] = outliers
    
    if outliers_count:
        print("\n[ATTENTION]  Outliers detected (IQR method):")
        for col, count in outliers_count.items():
            pct = count / len(X) * 100
            print(f"  {col}: {count} ({pct:.1f}%)")

# Utilisation
check_data_quality(X, y)


[OK] FORMULES MATHÉMATIQUES IMPORTANTES

# === Métriques de Classification ===

# Accuracy = (TP + TN) / (TP + TN + FP + FN)
# Precision = TP / (TP + FP)
# Recall (Sensitivity) = TP / (TP + FN)
# Specificity = TN / (TN + FP)
# F1-Score = 2 * (Precision * Recall) / (Precision + Recall)
# F-beta = (1 + β²) * (Precision * Recall) / (β² * Precision + Recall)

# === Métriques de Régression ===

# MSE = (1/n) * Σ(y_true - y_pred)²
# RMSE = √MSE
# MAE = (1/n) * Σ|y_true - y_pred|
# R² = 1 - (SS_res / SS_tot)
#    = 1 - (Σ(y_true - y_pred)² / Σ(y_true - y_mean)²)

# === Régularisation ===

# Ridge (L2): Cost = MSE + α * Σw²
# Lasso (L1): Cost = MSE + α * Σ|w|
# ElasticNet: Cost = MSE + α₁ * Σ|w| + α₂ * Σw²

# === Distance Metrics ===

# Euclidean: √(Σ(x₁ - x₂)²)
# Manhattan: Σ|x₁ - x₂|
# Minkowski: (Σ|x₁ - x₂|ᵖ)^(1/p)
# Cosine Similarity: (x₁ · x₂) / (||x₁|| * ||x₂||)


[OK] COMMANDES UTILES

# Vérifier versions
import sklearn
print(f"Scikit-learn: {sklearn.__version__}")
print(f"NumPy: {np.__version__}")
print(f"Pandas: {pd.__version__}")

# Lister tous les estimators
from sklearn.utils import all_estimators
estimators = all_estimators(type_filter='classifier')
for name, EstimatorClass in estimators:
    print(name)

# Voir paramètres d'un modèle
model = RandomForestClassifier()
print(model.get_params())

# Configuration warnings
import warnings
warnings.filterwarnings('ignore', category=DeprecationWarning)
warnings.filterwarnings('ignore', category=FutureWarning)

# Seed global pour reproductibilité
np.random.seed(42)
import random
random.seed(42)

# Afficher tous les scores disponibles
from sklearn.metrics import SCORERS
print(sorted(SCORERS.keys()))

# Documentation rapide
from sklearn.ensemble import RandomForestClassifier
help(RandomForestClassifier)
# ou
print(RandomForestClassifier.__doc__)


[OK] RÉFÉRENCES RAPIDES

# === Quand utiliser quel algorithme? ===

# CLASSIFICATION:
# - Logistic Regression: Baseline, interprétable, rapide
# - Decision Tree: Interprétable, non-linéaire
# - Random Forest: Bon généraliste, robuste
# - Gradient Boosting: Haute performance, compétitions
# - SVM: Petites données, haute dimension
# - KNN: Simple, non-paramétrique
# - Naive Bayes: Text classification, probabiliste
# - Neural Network: Données complexes, non-linéaire

# RÉGRESSION:
# - Linear Regression: Baseline, interprétable
# - Ridge/Lasso: Régularisation, feature selection
# - Decision Tree: Non-linéaire, interprétable
# - Random Forest: Bon généraliste
# - Gradient Boosting: Haute performance
# - SVR: Robuste aux outliers

# CLUSTERING:
# - K-Means: Rapide, clusters sphériques
# - DBSCAN: Formes arbitraires, gère bruit
# - Hierarchical: Dendrogramme, pas besoin k
# - GMM: Clusters probabilistes

# === Tailles de données ===

# < 1000 samples: Simple models (Logistic, KNN, SVM)
# 1K - 100K: Random Forest, Gradient Boosting
# > 100K: SGD, Linear models, Deep Learning

# === Préprocessing par algorithme ===

# Scaling REQUIS: SVM, KNN, Neural Networks, Logistic Regression
# Scaling optionnel: Tree-based (RF, GB, DT)
# Encoding requis: Tous sauf arbres (supportent catégorielles)


[OK] GLOSSAIRE

# Accuracy: Proportion de prédictions correctes
# AUC: Area Under ROC Curve
# Bagging: Bootstrap Aggregating
# Bias: Erreur due à approximations du modèle
# Boosting: Ensemble séquentiel
# Cross-Validation: Validation croisée
# Ensemble: Combinaison de modèles
# Feature Engineering: Création de features
# Hyperparameter: Paramètre du modèle
# Imbalanced: Classes déséquilibrées
# Loss Function: Fonction de coût
# Overfitting: Sur-apprentissage
# Pipeline: Séquence de transformations
# Regularization: Pénalisation complexité
# Scaling: Normalisation des données
# Stratified: Préserve distribution classes
# Underfitting: Sous-apprentissage
# Variance: Sensibilité aux données d'entraînement


[OK] FOIRE AUX QUESTIONS (FAQ)

# Q: Quelle différence entre fit() et fit_transform()?
# A: fit() apprend les paramètres, fit_transform() apprend ET transforme
#    Toujours fit_transform() sur train, transform() sur test

# Q: Pourquoi mes scores sont différents à chaque run?
# A: Fixer random_state=42 dans les modèles et train_test_split

# Q: Comment gérer les valeurs manquantes?
# A: SimpleImputer (mean, median, most_frequent) ou supprimer

# Q: Faut-il toujours scaler les données?
# A: Oui pour SVM, KNN, régression. Optionnel pour arbres

# Q: Comment choisir entre RandomForest et GradientBoosting?
# A: RF = parallèle, rapide, moins overfit
#    GB = séquentiel, meilleure performance, plus overfit

# Q: Quelle métrique utiliser?
# A: Classification: accuracy si équilibré, F1 si déséquilibré
#    Régression: RMSE si outliers importent, MAE sinon

# Q: Comment éviter l'overfitting?
# A: Cross-validation, régularisation, plus de données,
#    feature selection, early stopping

# Q: Pipeline ou pas?
# A: Toujours utiliser pipelines pour éviter data leakage

# Q: Combien de folds pour cross-validation?
# A: 5 ou 10 généralement, 3 si données limitées

# Q: Comment gérer déséquilibre de classes?
# A: class_weight='balanced', SMOTE, sous-échantillonnage


[OK] MÉTHODES PAR DÉFAUT RECOMMANDÉES

# Pour commencer un projet:
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import classification_report

# Split stratifié
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

# Pipeline avec scaling
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('classifier', RandomForestClassifier(
        n_estimators=100,
        max_depth=10,
        random_state=42
    ))
])

# Cross-validation
scores = cross_val_score(pipeline, X_train, y_train, cv=5)
print(f"CV Score: {scores.mean():.3f} (+/- {scores.std():.3f})")

# Entraîner et évaluer
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)
print(classification_report(y_test, y_pred))


[OK] RESSOURCES SUPPLÉMENTAIRES

# Documentation officielle:
# https://scikit-learn.org/stable/

# Tutoriels interactifs:
# https://scikit-learn.org/stable/tutorial/

# Exemples de code:
# https://scikit-learn.org/stable/auto_examples/

# API Reference complète:
# https://scikit-learn.org/stable/modules/classes.html

# User Guide détaillé:
# https://scikit-learn.org/stable/user_guide.html

# Cheat Sheet visuel:
# https://scikit-learn.org/stable/tutorial/machine_learning_map/

# Cours recommandés:
# - Machine Learning by Andrew Ng (Coursera)
# - Fast.ai Practical Machine Learning
# - Kaggle Learn

# Livres:
# - Hands-On Machine Learning (Aurélien Géron)
# - Python Machine Learning (Sebastian Raschka)
# - Introduction to Machine Learning with Python (Andreas Müller)

# Communauté:
# - Stack Overflow: [scikit-learn] tag
# - GitHub: https://github.com/scikit-learn/scikit-learn
# - Gitter: https://gitter.im/scikit-learn/scikit-learn

# Compétitions pour pratiquer:
# - Kaggle: https://www.kaggle.com/
# - DrivenData: https://www.drivendata.org/
# - Zindi: https://zindi.africa/

# Datasets pour pratiquer:
# - UCI Machine Learning Repository
# - Kaggle Datasets
# - OpenML
# - sklearn.datasets (intégrés)


[OK] NOTES FINALES

# Cette cheatsheet couvre:
# [OK] Installation et configuration
# [OK] Preprocessing (scaling, encoding, imputation)
# [OK] Modèles de classification
# [OK] Modèles de régression
# [OK] Clustering
# [OK] Réduction de dimensionnalité
# [OK] Model selection et validation
# [OK] Hyperparameter tuning
# [OK] Pipelines
# [OK] Métriques d'évaluation
# [OK] Ensembles
# [OK] Gestion déséquilibre
# [OK] Détection d'anomalies
# [OK] Text processing
# [OK] Feature engineering
# [OK] Model persistence
# [OK] Interprétabilité
# [OK] Bonnes pratiques
# [OK] Debugging et diagnostics
# [OK] Exemples complets
# [OK] Templates réutilisables

# Pour aller plus loin:
# - Deep Learning: TensorFlow, PyTorch
# - Big Data: PySpark MLlib, Dask-ML
# - AutoML: TPOT, Auto-sklearn, H2O
# - Model serving: Flask, FastAPI, TensorFlow Serving
# - Monitoring: MLflow, Weights & Biases

# Bon apprentissage! [RAPIDE]] = {
            'model': model,
            'score': score
        }
        
        print(f"{name}: {score:.4f}")
    
    # Meilleur modèle
    best_name = max(results, key=lambda k: results[k]['score'])
    return results[best_name]['model'], results

# Utilisation
best_model, all_results = simple_automl(X_train, X_test, y_train, y_test)

# === TPOT (AutoML avancé) ===

# Installation: pip install tpot
from tpot import TPOTClassifier

# TPOT
tpot = TPOTClassifier(
    generations=5,
    population_size=20,
    cv=5,
    random_state=42,
    verbosity=2,
    n_jobs=-1
)
tpot.fit(X_train, y_train)

# Score
print(f"Score: {tpot.score(X_test, y_test):.4f}")

# Exporter pipeline
tpot.export('tpot_pipeline.py')


[OK] DÉPLOIEMENT ET PRODUCTION

# === Versionning de Modèles ===

import joblib
from datetime import datetime

# Sauvegarder avec métadonnées
metadata = {
    'model_name': 'RandomForest',
    'version': '1.0.0',
    'timestamp': datetime.now().isoformat(),
    'features': feature_names,
    'metrics': {
        'accuracy': 0.95,
        'f1_score': 0.94
    }
}

# Sauvegarder
model_data = {
    'model': model,
    'scaler': scaler,
    'metadata': metadata
}
joblib.dump(model_data, f'model_v{metadata["version"]}.pkl')

# === API de Prédiction ===

# Flask API simple
from flask import Flask, request, jsonify

app = Flask(__name__)

# Charger modèle
model_data = joblib.load('model.pkl')
model = model_data['model']
scaler = model_data['scaler']

@app.route('/predict', methods=['POST'])
def predict():
    # Récupérer données
    data = request.json
    X = np.array(data['features']).reshape(1, -1)
    
    # Prétraiter
    X_scaled = scaler.transform(X)
    
    # Prédire
    prediction = model.predict(X_scaled)[0]
    probability = model.predict_proba(X_scaled)[0].tolist()
    
    return jsonify({
        'prediction': int(prediction),
        'probability': probability
    })

if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=5000)

# === Monitoring ===

class ModelMonitor:
    """Monitorer performance du modèle en production"""
    
    def __init__(self, model):
        self.model = model
        self.predictions = []
        self.true_labels = []
    
    def predict(self, X):
        prediction = self.model.predict(X)
        self.predictions.extend(prediction)
        return prediction
    
    def add_true_labels(self, y_true):
        self.true_labels.extend(y_true)
    
    def get_metrics(self):
        if len(self.true_labels) > 0:
            accuracy = accuracy_score(self.true_labels, self.predictions[:len(self.true_labels)])
            return {'accuracy': accuracy}
        return {}
    
    def detect_drift(self, X_reference, X_current):
        """Détection de data drift"""
        from scipy.stats import ks_2samp
        
        drift_scores = []
        for i in range(X_reference.shape[1]):
            stat, pval = ks_2samp(X_reference[:, i], X_current[:, i])
            drift_scores.append(pval)
        
        # Drift si p-value < 0.05
        return np.array(drift_scores) < 0.05


[OK] ASTUCES ET TRICKS

# === 1. Gestion Mémoire ===

# Réduire taille des arrays
X = X.astype('float32')  # Au lieu de float64

# Utiliser sparse matrices
from scipy.sparse import csr_matrix
X_sparse = csr_matrix(X)

# === 2. Parallélisation ===

# Utiliser tous les CPU
model = RandomForestClassifier(n_jobs=-1)

# GridSearchCV parallèle
grid = GridSearchCV(model, param_grid, cv=5, n_jobs=-1)

# === 3. Early Stopping ===

# Gradient Boosting avec early stopping
gb = GradientBoostingClassifier(
    n_estimators=1000,
    validation_fraction=0.2,
    n_iter_no_change=10,
    random_state=42
)
gb.fit(X_train, y_train)
print(f"Best iteration: {gb.n_estimators_}")

# === 4. Warm Start ===

# Continuer entraînement
model = RandomForestClassifier(n_estimators=10, warm_start=True)
model.fit(X_train, y_train)

# Ajouter plus d'arbres
model.n_estimators = 20
model.fit(X_train, y_train)

# === 5. Sample Weights ===

# Donner poids différents aux échantillons
sample_weights = np.ones(len(X_train))
sample_weights[important_indices] = 2.0

model.fit(X_train, y_train, sample_weight=sample_weights)

# === 6. Threshold Tuning ===

# Ajuster seuil de décision
y_proba = model.predict_proba(X_test)[:, 1]

# Tester différents seuils
from sklearn.metrics import precision_recall_curve

precision, recall, thresholds = precision_recall_curve(y_test, y_proba)

# Trouver meilleur seuil
f1_scores = 2 * (precision * recall) / (precision + recall)
best_threshold = thresholds[np.argmax(f1_scores)]

# Prédire avec nouveau seuil
y_pred_adjusted = (y_proba >= best_threshold).astype(int)

# === 7. Voting avec Poids ===

# Ensemble avec poids personnalisés
voting = VotingClassifier(
    estimators=[
        ('rf', RandomForestClassifier()),
        ('gb', GradientBoostingClassifier()),
        ('lr', LogisticRegression())
    ],
    voting='soft',
    weights=[2, 2, 1]  # RF et GB ont plus de poids
)

# === 8. Pipeline Debugging ===

# Afficher étapes
pipeline.named_steps

# Accéder à une étape
scaler = pipeline.named_steps['scaler']
print(scaler.mean_)

# Transformer jusqu'à une étape
X_transformed = pipeline[:'pca'].transform(X)


[OK] ERREURS COURANTES À ÉVITER

# [X] ERREUR 1: Fit sur test set
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)  # Fuite de données!
X_train, X_test = train_test_split(X_scaled, test_size=0.2)

# [OK] CORRECT:
X_train, X_test = train_test_split(X, test_size=0.2)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# [X] ERREUR 2: Oublier stratify pour classes déséquilibrées
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

# [OK] CORRECT:
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y
)

# [X] ERREUR 3: Ne pas fixer random_state
model = RandomForestClassifier()  # Résultats non reproductibles

# [OK] CORRECT:
model = RandomForestClassifier(random_state=42)

# [X] ERREUR 4: Évaluer sur train set
model.fit(X_train, y_train)
score = model.score(X_train, y_train)  # Trop optimiste!

# [OK] CORRECT:
score = model.score(X_test, y_test)

# [X] ERREUR 5: Ignorer les warnings
import warnings
warnings.filterwarnings('ignore')  # Dangereux!

# [OK] CORRECT: Lire et corriger les warnings

# [X] ERREUR 6: Utiliser moyenne pour données déséquilibrées
accuracy = accuracy_score(y_test, y_pred)  # Pas assez!

# [OK] CORRECT: Utiliser plusieurs métriques
print(classification_report(y_test, y_pred))
print(confusion_matrix(y_test, y_pred))


[OK] CHECKLIST PROJET ML

# 1. [OK] Exploration des données
# - Statistiques descriptives
# - Visualisations
# - Valeurs manquantes
# - Outliers
# - Distribution des classes

# 2. [OK] Preprocessing
# - Imputation valeurs manquantes
# - Encoding catégories
# - Scaling/Normalization
# - Feature engineering

# 3. [OK] Split des données
# - Train/Test split (avec stratify si nécessaire)
# - Validation set ou CV

# 4. [OK] Baseline model
# - Modèle simple pour référence
# - Dummy classifier/regressor

# 5. [OK] Training
# - Plusieurs algorithmes
# - Cross-validation
# - Hyperparameter tuning

# 6. [OK] Évaluation
# - Métriques appropriées
# - Courbes (ROC, Precision-Recall)
# - Matrice de confusion
# - Validation croisée

# 7. [OK] Analyse des erreurs
# - Identifier patterns dans erreurs
# - Feature importance
# - Interprétabilité

# 8. [OK] Amélioration
# - Feature engineering avancé
# - Ensembles
# - Gestion déséquilibre

# 9. [OK] Production
# - Sauvegarder modèle et pipeline
# - Documentation
# - Tests
# - Monitoring


[OK] EXEMPLES PRATIQUES COMPLETS

# === Exemple 1: Classification avec Features Catégorielles ===

import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.metrics import classification_report
import joblib

# Données avec colonnes numériques et catégorielles
data = {
    'age': [25, 35, 45, 30, 50, 28, 40],
    'salary': [30000, 50000, 70000, 45000, 90000, 35000, 60000],
    'city': ['Paris', 'Lyon', 'Paris', 'Lyon', 'Paris', 'Lyon', 'Paris'],
    'education': ['Master', 'Bachelor', 'PhD', 'Master', 'PhD', 'Bachelor', 'Master'],
    'purchased': [0, 1, 1, 0, 1, 0, 1]
}
df = pd.DataFrame(data)

# Séparer features et target
X = df.drop('purchased', axis=1)
y = df['purchased']

# Identifier colonnes numériques et catégorielles
numeric_features = ['age', 'salary']
categorical_features = ['city', 'education']

# Créer transformers
numeric_transformer = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

categorical_transformer = Pipeline([
    ('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
    ('onehot', OneHotEncoder(handle_unknown='ignore'))
])

# Combiner transformers
preprocessor = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, numeric_features),
        ('cat', categorical_transformer, categorical_features)
    ])

# Pipeline complet
pipeline = Pipeline([
    ('preprocessor', preprocessor),
    ('classifier', RandomForestClassifier(random_state=42))
])

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

# Grid search
param_grid = {
    'classifier__n_estimators': [50, 100, 200],
    'classifier__max_depth': [None, 10, 20],
    'classifier__min_samples_split': [2, 5]
}

grid_search = GridSearchCV(
    pipeline, param_grid, cv=5, scoring='f1', n_jobs=-1
)
grid_search.fit(X_train, y_train)

# Évaluation
print(f"Best params: {grid_search.best_params_}")
print(f"Best score: {grid_search.best_score_:.4f}")

y_pred = grid_search.predict(X_test)
print("\nClassification Report:")
print(classification_report(y_test, y_pred))

# Sauvegarder
joblib.dump(grid_search.best_estimator_, 'model_pipeline.pkl')

# === Exemple 2: Prédiction Séries Temporelles ===

import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error
import matplotlib.pyplot as plt

# Générer données temporelles
dates = pd.date_range('2020-01-01', periods=365, freq='D')
values = np.sin(np.arange(365) * 2 * np.pi / 365) + np.random.normal(0, 0.1, 365)
df = pd.DataFrame({'date': dates, 'value': values})

# Feature engineering temporel
df['day_of_week'] = df['date'].dt.dayofweek
df['day_of_month'] = df['date'].dt.day
df['month'] = df['date'].dt.month
df['quarter'] = df['date'].dt.quarter

# Lag features
for i in range(1, 8):
    df[f'lag_{i}'] = df['value'].shift(i)

# Rolling features
df['rolling_mean_7'] = df['value'].rolling(window=7).mean()
df['rolling_std_7'] = df['value'].rolling(window=7).std()

# Supprimer NaN
df = df.dropna()

# Features et target
feature_cols = [col for col in df.columns if col not in ['date', 'value']]
X = df[feature_cols]
y = df['value']

# Split temporel (80% train, 20% test)
split_idx = int(len(df) * 0.8)
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]

# Modèle
model = RandomForestRegressor(
    n_estimators=100,
    max_depth=10,
    random_state=42
)
model.fit(X_train, y_train)

# Prédiction
y_pred = model.predict(X_test)

# Métriques
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
mae = mean_absolute_error(y_test, y_pred)
print(f"RMSE: {rmse:.4f}")
print(f"MAE: {mae:.4f}")

# Visualiser
plt.figure(figsize=(12, 6))
plt.plot(df['date'][:split_idx], y_train, label='Train', alpha=0.7)
plt.plot(df['date'][split_idx:], y_test, label='Test', alpha=0.7)
plt.plot(df['date'][split_idx:], y_pred, label='Predictions', alpha=0.7)
plt.legend()
plt.xlabel('Date')
plt.ylabel('Value')
plt.title('Time Series Prediction')
plt.tight_layout()
plt.show()

# Feature importance
importances = pd.DataFrame({
    'feature': feature_cols,
    'importance': model.feature_importances_
}).sort_values('importance', ascending=False)
print("\nTop 10 Features:")
print(importances.head(10))

# === Exemple 3: Détection d'Anomalies ===

from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

# Générer données avec anomalies
np.random.seed(42)
X_normal = np.random.randn(300, 2) * 2
X_anomalies = np.random.uniform(-10, 10, (20, 2))
X = np.vstack([X_normal, X_anomalies])

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

# Isolation Forest
iso_forest = IsolationForest(
    contamination=0.1,  # 10% anomalies attendues
    random_state=42
)
predictions = iso_forest.fit_predict(X_scaled)

# Scores d'anomalie
scores = iso_forest.score_samples(X_scaled)

# Visualiser
plt.figure(figsize=(12, 5))

# Plot 1: Données avec anomalies
plt.subplot(1, 2, 1)
plt.scatter(X[predictions == 1, 0], X[predictions == 1, 1], 
            c='blue', label='Normal', alpha=0.6)
plt.scatter(X[predictions == -1, 0], X[predictions == -1, 1], 
            c='red', label='Anomaly', alpha=0.6)
plt.legend()
plt.title('Anomaly Detection')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')

# Plot 2: Scores d'anomalie
plt.subplot(1, 2, 2)
plt.hist(scores[predictions == 1], bins=30, alpha=0.6, label='Normal')
plt.hist(scores[predictions == -1], bins=30, alpha=0.6, label='Anomaly')
plt.legend()
plt.xlabel('Anomaly Score')
plt.ylabel('Count')
plt.title('Anomaly Score Distribution')

plt.tight_layout()
plt.show()

# Statistiques
n_anomalies = np.sum(predictions == -1)
n_normal = np.sum(predictions == 1)
print(f"Normal: {n_normal}, Anomalies: {n_anomalies}")
print(f"Anomaly rate: {n_anomalies / len(X) * 100:.2f}%")

# === Exemple 4: Clustering avec Visualisation ===

from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering
from sklearn.metrics import silhouette_score
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt

# Générer données
from sklearn.datasets import make_blobs
X, y_true = make_blobs(n_samples=500, centers=4, n_features=10, random_state=42)

# Tester plusieurs algorithmes
algorithms = {
    'K-Means (k=4)': KMeans(n_clusters=4, random_state=42),
    'DBSCAN': DBSCAN(eps=1.5, min_samples=5),
    'Hierarchical': AgglomerativeClustering(n_clusters=4)
}

# Réduire dimensionnalité pour visualisation
tsne = TSNE(n_components=2, random_state=42)
X_tsne = tsne.fit_transform(X)

# Visualiser
fig, axes = plt.subplots(2, 2, figsize=(14, 12))
axes = axes.ravel()

# Plot 1: Données originales
axes[0].scatter(X_tsne[:, 0], X_tsne[:, 1], c=y_true, cmap='viridis', alpha=0.6)
axes[0].set_title('True Labels')

# Plots 2-4: Résultats clustering
for i, (name, algorithm) in enumerate(algorithms.items(), 1):
    labels = algorithm.fit_predict(X)
    
    # Silhouette score (pas pour DBSCAN avec bruit)
    if len(np.unique(labels)) > 1:
        score = silhouette_score(X, labels)
        title = f'{name}\nSilhouette: {score:.3f}'
    else:
        title = name
    
    axes[i].scatter(X_tsne[:, 0], X_tsne[:, 1], c=labels, cmap='viridis', alpha=0.6)
    axes[i].set_title(title)

plt.tight_layout()
plt.show()

# Méthode du coude détaillée
inertias = []
silhouettes = []
K_range = range(2, 11)

for k in K_range:
    kmeans = KMeans(n_clusters=k, random_state=42)
    labels = kmeans.fit_predict(X)
    inertias.append(kmeans.inertia_)
    silhouettes.append(silhouette_score(X, labels))

# Visualiser
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

ax1.plot(K_range, inertias, 'bo-', linewidth=2, markersize=8)
ax1.set_xlabel('Number of Clusters (k)', fontsize=12)
ax1.set_ylabel('Inertia', fontsize=12)
ax1.set_title('Elbow Method', fontsize=14)
ax1.grid(True, alpha=0.3)

ax2.plot(K_range, silhouettes, 'ro-', linewidth=2, markersize=8)
ax2.set_xlabel('Number of Clusters (k)', fontsize=12)
ax2.set_ylabel('Silhouette Score', fontsize=12)
ax2.set_title('Silhouette Analysis', fontsize=14)
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print("Optimal k based on silhouette:", K_range[np.argmax(silhouettes)])

# === Exemple 5: Text Classification ===

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix
import seaborn as sns

# Données textuelles (exemple)
texts = [
    "I love this product, it's amazing",
    "Terrible experience, waste of money",
    "Best purchase ever, highly recommend",
    "Not worth it, very disappointed",
    "Excellent quality and fast shipping",
    "Poor quality, broke after one use",
    "Great value for money",
    "Worst product I've ever bought",
]
labels = [1, 0, 1, 0, 1, 0, 1, 0]  # 1=positive, 0=negative

# Pipeline
text_clf = Pipeline([
    ('tfidf', TfidfVectorizer(
        max_features=1000,
        ngram_range=(1, 2),
        stop_words='english'
    )),
    ('clf', MultinomialNB(alpha=0.1))
])

# Si plus de données disponibles
# X_train, X_test, y_train, y_test = train_test_split(texts, labels, test_size=0.2)
# text_clf.fit(X_train, y_train)

# Pour démo avec petites données
text_clf.fit(texts, labels)

# Prédire
new_texts = [
    "This is fantastic!",
    "I hate this",
    "Could be better"
]
predictions = text_clf.predict(new_texts)
probabilities = text_clf.predict_proba(new_texts)

for text, pred, proba in zip(new_texts, predictions, probabilities):
    sentiment = "Positive" if pred == 1 else "Negative"
    confidence = proba[pred] * 100
    print(f"Text: '{text}'")
    print(f"Sentiment: {sentiment} (confidence: {confidence:.1f}%)\n")

# Feature importance (mots les plus informatifs)
vectorizer = text_clf.named_steps['tfidf']
classifier = text_clf.named_steps['clf']

feature_names = vectorizer.get_feature_names_out()
log_probs = classifier.feature_log_prob_

# Top mots pour chaque classe
n_top = 10
for i, class_name in enumerate(['Negative', 'Positive']):
    top_indices = log_probs[i].argsort()[-n_top:][::-1]
    top_features = [feature_names[idx] for idx in top_indices]
    print(f"\nTop {n_top} words for {class_name}:")
    print(", ".join(top_features))


[OK] OPTIMISATIONS AVANCÉES

# === 1. Feature Selection Avancée ===

from sklearn.feature_selection import (
    SelectFromModel, SequentialFeatureSelector
)

# SelectFromModel (basé sur importance)
selector = SelectFromModel(
    RandomForestClassifier(n_estimators=100, random_state=42),
    threshold='median'  # ou valeur spécifique
)
X_selected = selector.fit_transform(X_train, y_train)

# Features sélectionnées
selected_features = selector.get_support()
print(f"Selected {np.sum(selected_features)} features")

# Sequential Feature Selection
sfs = SequentialFeatureSelector(
    RandomForestClassifier(random_state=42),
    n_features_to_select=10,
    direction='forward',  # 'forward' ou 'backward'
    cv=5
)
X_selected = sfs.fit_transform(X_train, y_train)

# === 2. Custom Scoring ===

from sklearn.metrics import make_scorer, f1_score

# Scorer personnalisé
def custom_scorer(y_true, y_pred):
    # Exemple: pénaliser plus les faux négatifs
    tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
    return tp / (tp + 2 * fn)  # FN coûte 2x plus

custom_score = make_scorer(custom_scorer)

# Utiliser dans GridSearch
grid = GridSearchCV(model, param_grid, scoring=custom_score, cv=5)

# === 3. Pipeline avec Feature Union ===

from sklearn.pipeline import FeatureUnion
from sklearn.decomposition import PCA, TruncatedSVD

# Combiner différentes transformations
feature_union = FeatureUnion([
    ('pca', PCA(n_components=10)),
    ('svd', TruncatedSVD(n_components=10)),
    ('original', 'passthrough')  # Garder features originales
])

pipeline = Pipeline([
    ('union', feature_union),
    ('scaler', StandardScaler()),
    ('classifier', LogisticRegression())
])

# === 4. Calibration Multiple ===

from sklearn.calibration import CalibratedClassifierCV

# Calibrer plusieurs modèles
models = [
    ('rf', RandomForestClassifier(random_state=42)),
    ('gb', GradientBoostingClassifier(random_state=42)),
    ('svc', SVC(random_state=42))
]

calibrated_models = []
for name, model in models:
    calibrated = CalibratedClassifierCV(model, cv=5, method='isotonic')
    calibrated.fit(X_train, y_train)
    calibrated_models.append((name, calibrated))

# Ensemble des modèles calibrés
voting = VotingClassifier(
    estimators=calibrated_models,
    voting='soft'
)
voting.fit(X_train, y_train)


[OK] DEBUGGING ET DIAGNOSTICS

# === 1. Learning Curve Détaillée ===

from sklearn.model_selection import learning_curve

def plot_learning_curve(estimator, X, y, title="Learning Curve"):
    """Plot learning curve détaillée"""
    train_sizes, train_scores, val_scores = learning_curve(
        estimator, X, y,
        cv=5,
        n_jobs=-1,
        train_sizes=np.linspace(0.1, 1.0, 10),
        scoring='accuracy',
        shuffle=True,
        random_state=42
    )
    
    train_mean = np.mean(train_scores, axis=1)
    train_std = np.std(train_scores, axis=1)
    val_mean = np.mean(val_scores, axis=1)
    val_std = np.std(val_scores, axis=1)
    
    plt.figure(figsize=(10, 6))
    plt.plot(train_sizes, train_mean, label='Training score', color='blue', marker='o')
    plt.fill_between(train_sizes, train_mean - train_std, train_mean + train_std, 
                     alpha=0.15, color='blue')
    
    plt.plot(train_sizes, val_mean, label='Validation score', color='red', marker='s')
    plt.fill_between(train_sizes, val_mean - val_std, val_mean + val_std, 
                     alpha=0.15, color='red')
    
    plt.xlabel('Training Set Size')
    plt.ylabel('Score')
    plt.title(title)
    plt.legend(loc='best')
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.show()
    
    # Diagnostic
    final_gap = train_mean[-1] - val_mean[-1]
    if final_gap > 0.1:
        print("[ATTENTION]  High variance (overfitting): Consider regularization or more data")
    elif val_mean[-1] < 0.7:
        print("[ATTENTION]  High bias (underfitting): Consider more complex model or features")
    else:
        print("[OK] Model looks good!")

# Utilisation
plot_learning_curve(RandomForestClassifier(random_state=42), X, y)

# === 2. Feature Correlation Analysis ===

def analyze_feature_correlations(X, y, feature_names=None, threshold=0.9):
    """Analyser corrélations entre features"""
    import seaborn as sns
    
    # Convertir en DataFrame si nécessaire
    if not isinstance(X, pd.DataFrame):
        if feature_names is None:
            feature_names = [f'Feature_{i}' for i in range(X.shape[1])]
        X_df = pd.DataFrame(X, columns=feature_names)
    else:
        X_df = X
    
    # Matrice de corrélation
    corr_matrix = X_df.corr().abs()
    
    # Visualiser
    plt.figure(figsize=(12, 10))
    sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm', 
                square=True, linewidths=0.5)
    plt.title('Feature Correlation Matrix')
    plt.tight_layout()
    plt.show()
    
    # Trouver features hautement corrélées
    upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))
    high_corr = [(column, index, upper.loc[index, column]) 
                 for column in upper.columns 
                 for index in upper.index 
                 if upper.loc[index, column] > threshold]
    
    if high_corr:
        print(f"\n[ATTENTION]  Highly correlated features (|correlation| > {threshold}):")
        for feat1, feat2, corr in high_corr:
            print(f"  {feat1} <-> {feat2}: {corr:.3f}")
        print("\nConsider removing one feature from each pair.")
    else:
        print(f"\n[OK] No highly correlated features found (threshold={threshold})")
    
    # Corrélation avec target
    if y is not None:
        X_df_with_target = X_df.copy()
        X_df_with_target['target'] = y
        target_corr = X_df_with_target.corr()['target'].drop('target').abs().sort_values(ascending=False)
        
        print("\nTop 10 features correlated with target:")
        print(target_corr.head(10))
        
        # Visualiser
        plt.figure(figsize=(10, 6))
        target_corr.head(20).plot(kind='barh')
        plt.xlabel('Absolute Correlation with Target')
        plt.title('Feature-Target Correlation')
        plt.tight_layout()
        plt.show()

# Utilisation
analyze_feature_correlations(X_train, y_train, threshold=0.85)

# === 3. Prediction Analysis ===

def analyze_predictions(model, X_test, y_test, feature_names=None):
    """Analyser les prédictions en détail"""
    y_pred = model.predict(X_test)
    y_proba = model.predict_proba(X_test) if hasattr(model, 'predict_proba') else None
    
    # Identifier erreurs
    errors = y_test != y_pred
    n_errors = np.sum(errors)
    
    print(f"Total predictions: {len(y_test)}")
    print(f"Errors: {n_errors} ({n_errors/len(y_test)*100:.2f}%)")
    
    if y_proba is not None:
        # Analyser confiance
        max_proba = np.max(y_proba, axis=1)
        
        plt.figure(figsize=(12, 5))
        
        # Plot 1: Distribution de confiance
        plt.subplot(1, 2, 1)
        plt.hist(max_proba[~errors], bins=30, alpha=0.7, label='Correct', color='green')
        plt.hist(max_proba[errors], bins=30, alpha=0.7, label='Errors', color='red')
        plt.xlabel('Prediction Confidence')
        plt.ylabel('Count')
        plt.title('Confidence Distribution')
        plt.legend()
        
        # Plot 2: Erreurs par niveau de confiance
        plt.subplot(1, 2, 2)
        confidence_bins = np.linspace(0, 1, 11)
        error_rates = []
        for i in range(len(confidence_bins)-1):
            mask = (max_proba >= confidence_bins[i]) & (max_proba < confidence_bins[i+1])
            if np.sum(mask) > 0:
                error_rate = np.sum(errors[mask]) / np.sum(mask)
                error_rates.append(error_rate)
            else:
                error_rates.append(0)
        
        plt.plot(confidence_bins[:-1], error_rates, 'ro-', linewidth=2, markersize=8)
        plt.xlabel('Confidence Bin')
        plt.ylabel('Error Rate')
        plt.title('Error Rate by Confidence')
        plt.grid(True, alpha=0.3)
        
        plt.tight_layout()
        plt.show()
        
        # Exemples d'erreurs avec faible confiance
        low_confidence_errors = errors & (max_proba < 0.6)
        if np.sum(low_confidence_errors) > 0:
            print(f"\n[ATTENTION]  {np.sum(low_confidence_errors)} errors with low confidence (<0.6)")
            print("Consider reviewing these cases for data quality issues")

# Utilisation
analyze_predictions(model, X_test, y_test)


[OK] TEMPLATES RÉUTILISABLES

# === Template: Classification Binaire ===

def binary_classification_pipeline(X, y, test_size=0.2):
    """Template complet pour classification binaire"""
    
    # 1. Split
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=test_size, stratify=y, random_state=42
    )
    
    # 2. Pipeline
    pipeline = Pipeline([
        ('scaler', StandardScaler()),
        ('classifier', RandomForestClassifier(random_state=42))
    ])
    
    # 3. Grid search
    param_grid = {
        'classifier__n_estimators': [50, 100, 200],
        'classifier__max_depth': [5, 10, None],
        'classifier__min_samples_split': [2, 5]
    }
    
    grid = GridSearchCV(
        pipeline, param_grid,
        cv=5, scoring='roc_auc',
        n_jobs=-1, verbose=1
    )
    
    # 4. Entraîner
    print("Training...")
    grid.fit(X_train, y_train)
    
    # 5. Évaluer
    best_model = grid.best_estimator_
    y_pred = best_model.predict(X_test)
    y_proba = best_model.predict_proba(X_test)[:, 1]
    
    print(f"\nBest parameters: {grid.best_params_}")
    print(f"Best CV score: {grid.best_score_:.4f}")
    print(f"\nTest Results:")
    print(classification_report(y_test, y_pred))
    print(f"ROC AUC: {roc_auc_score(y_test, y_proba):.4f}")
    
    # 6. Visualisations
    fig, axes = plt.subplots(1, 2, figsize=(14, 5))
    
    # Confusion Matrix
    cm = confusion_matrix(y_test, y_pred)
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=axes[0])
    axes[0].set_title('Confusion Matrix')
    axes[0].set_ylabel('True')
    axes[0].set_xlabel('Predicted')
    
    # ROC Curve
    fpr, tpr, _ = roc_curve(y_test, y_proba)
    axes[1].plot(fpr, tpr, linewidth=2, label=f'AUC = {roc_auc_score(y_test, y_proba):.3f}')
    axes[1].plot([0, 1], [0, 1], 'k--')
    axes[1].set_xlabel('False Positive Rate')
    axes[1].set_ylabel('True Positive Rate')
    axes[1].set_title('ROC Curve')
    axes[1].legend()
    axes[1].grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()
    
    return best_model

# Utilisation
# model = binary_classification_pipeline(X, y)

# === Template: Régression ===

def regression_pipeline(X, y, test_size=0.2):
    """Template complet pour régression"""
    
    # Split
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=test_size, random_state=42
    )
    
    # Tester plusieurs modèles
    models = {
        'Linear': LinearRegression(),
        'Ridge': Ridge(alpha=1.0),
        'Lasso': Lasso(alpha=1.0),
        'Random Forest': RandomForestRegressor(random_state=42),
        'Gradient Boosting': GradientBoostingRegressor(random_state=42)
    }
    
    results = {}
    for name, model in models.items():
        # Pipeline
        pipeline = Pipeline([
            ('scaler', StandardScaler()),
            ('regressor', model)
        ])
        
        # Entraîner
        pipeline.fit(X_train, y_train)
        
        # Prédire
        y_pred = pipeline.predict(X_test)
        
        # Métriques
        mse = mean_squared_error(y_test, y_pred)
        rmse = np.sqrt(mse)
        mae = mean_absolute_error(y_test, y_pred)
        r2 = r2_score(y_test, y_pred)
        
        results[name