Fondamentaux
Le Machine Learning (apprentissage automatique) est le domaine de l’informatique qui permet aux ordinateurs d’apprendre à partir de données sans être explicitement programmés pour chaque tâche. C’est la branche de la science computationnelle qui se concentre sur l’analyse et l’interprétation de patterns et structures dans les données pour permettre l’apprentissage, le raisonnement et la prise de décision sans intervention humaine directe.
Contraste avec la programmation classique :
| Approche | Programmation classique | Machine Learning |
|---|---|---|
| Méthode | Règles explicites (if/else) | Apprentissage de patterns |
| Scalabilité | Limitée (trop de cas) | Excellente (généralisation) |
| Adaptation | Modification manuelle du code | Ré-entraînement automatique |
| Complexité | Difficile au-delà de quelques règles | Gère la haute dimensionnalité |
Évolution et dynamisme
Propriétés du domaine :
- Évolution constante : Nouveaux algorithmes chaque année
- Peu de standards figés : Best practices en évolution
- Interdisciplinarité : Mathématiques, statistiques, informatique
- Accessibilité croissante : Outils de plus en plus simples (sklearn, TensorFlow)
⚠️ Conseil : Restez à jour ! Suivez les conférences (NeurIPS, ICML), blogs (Towards Data Science), et implémentations (Papers with Code).
Taxonomie du Machine Learning
Machine Learning
|
┌─────────────────┴─────────────────┐
│ │
Supervisé Non-supervisé
│ │
┌────┴────┐ ┌─────────┼──────────┐
│ │ │ │ │
Regression Classification Clustering Dim. Anomaly
Reduction Detection
│ │ │ │ │
Linéaire Logistique K-Means PCA Isolation
Ridge SVM DBSCAN t-SNE Forest
Lasso Random Forest Hierarchical UMAP
Neural Networks
Apprentissage supervisé
Caractéristiques :
- Données étiquetées (labeled)
- Objectif : Apprendre une fonction $f: X \rightarrow y$
- Évaluation : Comparaison prédictions vs vraies valeurs
Regression vs Classification
Différence fondamentale : Nature de la variable cible.
| Aspect | Regression | Classification |
|---|---|---|
| Target | Continue (nombre réel) | Discrète (catégorie) |
| Question | “Combien ?” | “Quelle catégorie ?” |
| Exemple | Quelle sera la température ? | Fera-t-il froid ou chaud ? |
| Output | 23.5°C | “Chaud” |
| Métrique | MAE, RMSE, R² | Accuracy, Precision, Recall |
| ![[Pasted image 20251105093648.png | 500]] |
Autres tâches supervisées
Ranking : Ordonner des éléments par pertinence
- Moteurs de recherche
- Systèmes de recommandation
Apprentissage non-supervisé
Caractéristiques :
- Données non-étiquetées
- Objectif : Découvrir la structure cachée
- Pas de “bonne réponse” à comparer
Clustering (Regroupement)
Objectif : Trouver des groupes naturels dans les données.
Exemples :
- Segmentation de clients
- Identification de familles de molécules similaires
- Compression d’images (K-means)
Dimensionality Reduction (Réduction de dimension)
Objectif : Réduire le nombre de features tout en préservant l’information.
Usages :
- Visualisation de données haute-dimension
- Pré-traitement avant modélisation
- Compression
Méthodes courantes : PCA, t-SNE, UMAP
Anomaly Detection (Détection d’anomalies)
Objectif : Identifier les points atypiques.
Applications :
- Détection de fraudes
- Maintenance prédictive
- Contrôle qualité en production chimique
Terminologie
| Concept | Synonymes |
|---|---|
| Features | Input, X’s, Variables, Descripteurs, Predictors |
| Target | Output, y, Label, Class, Response |
| Samples | Rows, Observations, Instances, Examples |
| Model | Estimator, Predictor, Learner |
| Training | Fitting, Learning |
Notation standard :
- $X$ : Matrice de features (majuscule, gras)
- $y$ : Vecteur target (minuscule)
- $n$ : Nombre d’échantillons
- $p$ : Nombre de features
- $\hat{y}$ : Prédictions
Domaines et tâches

Tâches par domaine :
| Domaine | Tâches courantes |
|---|---|
| Computer Vision | Classification d’images, détection d’objets, segmentation |
| NLP | Classification de texte, traduction, génération |
| Time Series | Prévision, détection d’anomalies |
| Recommandation | Collaborative filtering, content-based |
| Reinforcement Learning | Jeux, robotique, optimisation |
Pipeline Machine Learning
Dataset
↓
Data Retrieval (Collecte)
↓
Data Preparation
├─ Data Processing & Wrangling
├─ Feature Extraction & Engineering
└─ Feature Scaling & Selection
↓
Modeling ←─── Machine Learning Algorithms
↓
Model Evaluation & Tuning
↓ (si insatisfaisant, retour à Data Preparation)
↓
Deployment & Monitoring
Étape 1 : Data Retrieval
Collecter et charger les données.
Sources courantes :
- Bases de données (SQL)
- APIs (REST, GraphQL)
- Fichiers (CSV, JSON, Parquet)
- Web scraping
- Instruments de mesure (spectroscopie, chromatographie)
Étape 2 : Data Preparation
2.1 Data Processing & Wrangling
Nettoyer et structurer les données.
Tâches courantes :
- Gestion des valeurs manquantes
- Suppression des doublons
- Correction des erreurs de saisie
- Conversion de types
- Fusion de datasets
2.2 Feature Extraction & Engineering
Créer de nouvelles features à partir des données brutes.
Exemples :
- Extraire année, mois, jour d’une date
- Créer des interactions (produit de deux features)
- Calculer des descripteurs moléculaires (à partir de SMILES)
- One-hot encoding de variables catégorielles
2.3 Feature Scaling & Selection
Scaling : Normaliser les échelles des features
- StandardScaler : $(x - \mu) / \sigma$
- MinMaxScaler : $(x - x_{min}) / (x_{max} - x_{min})$
Selection : Choisir les features les plus informatives
- Variance threshold
- Corrélation avec la target
- Importance dans un modèle
Étape 3 : Modeling
Entraîner un modèle sur les données préparées.
Choix du modèle : Dépend de :
- Type de problème (regression, classification)
- Taille du dataset
- Interprétabilité requise
- Contraintes computationnelles
Étape 4 : Model Evaluation & Tuning
Évaluer et améliorer le modèle.
Processus itératif :
- Évaluer les performances
- Identifier les faiblesses
- Ajuster hyperparamètres ou revenir au preprocessing
- Répéter jusqu’à satisfaction
Étape 5 : Deployment & Monitoring
Deployment : Mettre le modèle en production
- API REST (Flask, FastAPI)
- Batch predictions
- Edge deployment (mobile, IoT)
Monitoring : Surveiller les performances
- Drift de données
- Dégradation du modèle
- Latence, erreurs
Modélisations linéaires avec Scikit-learn
Généralités
- Open-source et très maintenu
- API cohérente entre tous les modèles
- Documentation excellente
- Intégration NumPy/Pandas
- Performance : Implémentations optimisées (C/Cython)
Structure de Scikit-learn
Organisation :
- Sklearn est organisé en modules
- Chaque module contient des outils sous forme de classes
Principaux modules :
| Module | Contenu |
|---|---|
linear_model |
Modèles linéaires (Regression, Logistic, Ridge, Lasso) |
ensemble |
Méthodes d’ensemble (Random Forest, Gradient Boosting) |
tree |
Arbres de décision |
svm |
Support Vector Machines |
preprocessing |
Transformations de données |
model_selection |
Split, cross-validation, grid search |
metrics |
Métriques d’évaluation |
Imports : Bonnes pratiques
Exemple avec linear_model :
# ❌ NE FONCTIONNE PAS avec sklearn
import sklearn
model = sklearn.linear_model.LinearRegression()
# ❌ Import du module entier (verbeux)
import sklearn.linear_model
model = sklearn.linear_model.LinearRegression()
# ❌ Import du module (toujours verbeux)
from sklearn import linear_model
model = linear_model.LinearRegression()
# ❌ Import wildcard (non-explicite)
from sklearn.linear_model import *
model = LinearRegression() # D'où vient-il ?
# ✅ BONNE PRATIQUE : Import explicite de la classe
from sklearn.linear_model import LinearRegression
model = LinearRegression() # Clair et concis
Workflow et exemple
Les 5 étapes standard :
# 1. Import du modèle
from sklearn.linear_model import LinearRegression
# 2. Instanciation du modèle
model = LinearRegression()
# 3. Entraînement du modèle
model.fit(X_train, y_train)
# 4. Évaluation du modèle
score = model.score(X_test, y_test)
# 5. Prédictions
predictions = model.predict(X_new)
Dataset
import pandas as pd
data = pd.read_csv('houses.csv')
# Mélanger les données (important !)
data = data.sample(frac=1)
data.head()
| Id | MSSubClass | LotArea | GrLivArea | SalePrice | |
|---|---|---|---|---|---|
| 1557 | 358 | 120 | 4224 | 1142 | 134000 |
| 1004 | 1005 | 120 | 3182 | 1504 | 181000 |
| 1518 | 179 | 20 | 17423 | 2234 | 501837 |
Objectif : Prédire SalePrice (y) en fonction de GrLivArea (X).
# Sélectionner les colonnes pertinentes
livecode_data = data[['GrLivArea', 'SalePrice']]
Exploration visuelle
import matplotlib.pyplot as plt
plt.scatter(data['GrLivArea'], data['SalePrice'])
plt.xlabel("Surface habitable (ft²)")
plt.ylabel("Prix de vente ($)")
plt.title("Relation surface-prix")
plt.show()

Observation : Tendance linéaire positive (plus grand → plus cher).
Entraînement
from sklearn.linear_model import LinearRegression
# 1. Instancier le modèle (aussi appelé "estimator")
model = LinearRegression()
# 2. Définir X et y
X = data[['GrLivArea']] # ⚠️ Double crochets → DataFrame
y = data['SalePrice'] # ⚠️ Simple crochet → Series
# 3. Entraîner le modèle
model.fit(X, y)
Attributs du modèle
Après entraînement, les paramètres optimaux sont stockés comme attributs (suffixe _).
# Pente (a)
print(f"Pente : {model.coef_[0]:.2f}")
Pente : 105.01
# Intercept (b) print(f"Intercept : {model.intercept_:.2f}")Intercept : 22104.12
Interprétation :
- Chaque ft² supplémentaire ajoute ~105$ au prix
- Une maison de 0 ft² aurait un “prix de base” de ~22,104$ (n’a pas de sens physiquement, mais nécessaire mathématiquement)
Évaluation (Score)
Chaque algorithme sklearn a une métrique par défaut.
Pour LinearRegression : Coefficient de détermination ($R^2$)
Définition : $R^2$ représente la proportion de la variance de $y$ expliquée par $X$.
\[R^2 = 1 - \frac{SS_{res}}{SS_{tot}} = 1 - \frac{\sum(y_i - \hat{y}_i)^2}{\sum(y_i - \bar{y})^2}\]Propriétés :
- Varie typiquement entre 0 et 1
- 0 : Le modèle n’explique rien
- 1 : Le modèle explique parfaitement
- Plus haut = meilleur
# Évaluer la performance score = model.score(X, y) print(f"R² : {score:.3f}")R² : 0.490
Interprétation : Le modèle explique ~49% de la variance des prix.
💡 Métrique par défaut : Consultez la documentation de .score() pour chaque modèle.
Exemples :
LogisticRegression: AccuracySVC: Accuracy- Régresseurs : Généralement $R^2$
Prédictions
# Prédire sur de nouvelles données
new_data = pd.DataFrame({'GrLivArea': [1000]})
prediction = model.predict(new_data)
print(f"Prix prédit : ${prediction[0]:,.0f}")
Prix prédit : $127,113
Interprétation : Un appartement de 1000 ft² est estimé à ~127K$.
Généralisation
The Holdout Method
Méthode d’évaluation consistant à diviser le dataset en deux ensembles :
- Training set (~70%) : Pour entraîner le modèle
- Test set (~30%) : Pour évaluer la généralisation
- Le modèle apprend sur le training set
- On mesure sa performance sur le test set (données inédites)
- Le score sur le test set estime la performance en production
Implémentation
from sklearn.model_selection import train_test_split
# Préparer X et y
X = livecode_data[['GrLivArea']]
y = livecode_data['SalePrice']
# Split en Train/Test (70/30)
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.3 # 30% pour le test
)
Résultat : 4 objets créés
X_train → Features d'entraînement (70% des lignes)
y_train → Target d'entraînement (70% des lignes)
X_test → Features de test (30% des lignes)
y_test → Target de test (30% des lignes)
Alternative : Split directement le DataFrame
# Split le DataFrame complet
train_data, test_data = train_test_split(livecode_data, test_size=0.3)
# Puis extraire X et y
X_train = train_data[['GrLivArea']]
y_train = train_data['SalePrice']
X_test = test_data[['GrLivArea']]
y_test = test_data['SalePrice']
Entraînement et évaluation
# Instancier
model = LinearRegression()
# Entraîner sur le Training set
model.fit(X_train, y_train)
# Évaluer sur le Test set
test_score = model.score(X_test, y_test)
print(f"Score sur test : {test_score:.3f}")
Score sur test : 0.482
Comparaison :
- Score sur toutes les données : 0.490
- Score sur test set : 0.482
Interprétation : Légère baisse acceptable (le modèle généralise bien).
Limitations
K-Fold Cross Validation
Processus
- Le dataset est divisé en K “folds” (plis)
- Pour chaque fold :
- Il sert de test set
- Les K-1 autres servent de training set
- Un sous-modèle est entraîné et évalué
- Le score final = moyenne des K scores
|  |
Vue DataFrame :
|  |
Propriété clé : On n’entraîne pas de modèle final, on estime simplement le score d’un modèle hypothétique entraîné sur tout le dataset.
Implémentation
from sklearn.model_selection import cross_validate
# Instancier le modèle
model = LinearRegression()
# Cross-validation à 5 folds
cv_results = cross_validate(model, X, y, cv=5)
# Afficher les 5 scores
print("Scores des 5 folds :")
print(cv_results['test_score'])
# Moyenne des scores
mean_score = cv_results['test_score'].mean()
print(f"\nScore moyen : {mean_score:.3f}")
Scores des 5 folds : [0.300 0.434 0.553 0.592 0.515]
Score moyen : 0.479
Interprétation :
- Score plus robuste (moyenne de 5 évaluations)
- Variabilité visible entre folds
- Utilise toutes les données
Choisir K
Compromis : Fiabilité vs Coût computationnel
| K | Avantages | Inconvénients |
|---|---|---|
| 2-3 | Rapide | Peu représentatif |
| 5-10 | ✅ Bon équilibre | Coût modéré |
| n (LOOCV) | Maximum de données | Très coûteux |
Règle empirique : K=5 ou K=10
Propriétés :
- Plus K est grand → Score plus représentatif
- Plus K est grand → Temps de calcul plus long
- K=n (Leave-One-Out) : Maximum d’information, mais très lent
Le compromis Biais-Variance

No Free Lunch Theorem
Il n’existe pas de modèle universel optimal pour tous les problèmes.
C’est à nous, data scientists, de :
- Faire des hypothèses sur les données
- Tester plusieurs modèles
- Évaluer leur généralisation
Donc par exemples :
- Données linéaires → Regression linéaire
- Données non-linéaires → Polynomial, Random Forest
- Relations complexes → Neural Networks
Learning Curves
On augmente progressivement la taille du training set et on observe l’évolution des scores.

Comportement typique :
À mesure que la taille d’entraînement augmente :
- Le score d’entraînement diminue (plus difficile de “mémoriser”)
- Le score de test augmente (meilleure généralisation)
- Les courbes convergent généralement

High Bias (Underfitting)

Interprétation : Le modèle est trop simple pour capturer les patterns.
Solutions :
- Modèle plus complexe
- Plus de features
- Feature engineering
High Variance (Overfitting)

Interprétation : Le modèle mémorise le bruit des données d’entraînement.
Solutions :
- Plus de données d’entraînement
- Modèle plus simple
- Régularisation
- Réduction de features
Courbes idéales

Interprétation : Le modèle généralise parfaitement !
Implémentation avec Scikit-learn
import numpy as np
from sklearn.model_selection import learning_curve
import matplotlib.pyplot as plt
# Définir les tailles d'entraînement à tester
train_sizes = [25, 50, 75, 100, 250, 500, 750, 1000, 1150]
# Calculer les courbes d'apprentissage
train_sizes, train_scores, test_scores = learning_curve(
estimator=LinearRegression(),
X=X,
y=y,
train_sizes=train_sizes,
cv=5 # 5-fold pour chaque taille
)
# Moyennes des scores (sur les 5 folds)
train_scores_mean = np.mean(train_scores, axis=1)
test_scores_mean = np.mean(test_scores, axis=1)
# Tracer les courbes
plt.figure(figsize=(10, 6))
plt.plot(train_sizes, train_scores_mean, label='Score entraînement', marker='o')
plt.plot(train_sizes, test_scores_mean, label='Score test', marker='o')
plt.xlabel('Taille du training set', fontsize=14)
plt.ylabel('R² score', fontsize=14)
plt.title('Courbes d\'apprentissage', fontsize=18)
plt.legend()
plt.grid(alpha=0.3)
plt.show()
|  |
Observation 1 : Les courbes ont convergé et plafonné
- Le modèle fait de son mieux avec les patterns disponibles
- Ajouter plus de données n’améliorera pas la performance
Observation 2 : Le score reste relativement bas (R² ~ 0.5)
- Suggère qu’on pourrait améliorer le modèle
- Solutions : features supplémentaires, modèle non-linéaire, feature engineering
Classification Multi-classes
Exemples :
- Reconnaître des chiffres manuscrits (0-9) : 10 classes
- Identifier des espèces de fleurs (Iris) : 3 classes
- Classifier des types de réactions chimiques : n classes
- Prédire des espèces de pingouins : 3 classes
On peut se demander si la généralisation du problème binaire à n classes ne constitue pas un défi algorithmique fondamental en Machine Learning ?
Dataset d’exemple : Palmer Penguins
Source : Dataset intégré à Seaborn, collecté par Dr. Kristen Gorman à la station Palmer en Antarctique.
Objectif : Classifier trois espèces de pingouins en fonction de leurs mesures physiques.
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
# Charger le dataset
penguins = sns.load_dataset('penguins')
# Aperçu
penguins.head()
| species | island | bill_length_mm | bill_depth_mm | flipper_length_mm | body_mass_g | sex | |
|---|---|---|---|---|---|---|---|
| 0 | Adelie | Torgersen | 39.1 | 18.7 | 181.0 | 3750.0 | Male |
| 1 | Adelie | Torgersen | 39.5 | 17.4 | 186.0 | 3800.0 | Female |
| 2 | Adelie | Torgersen | 40.3 | 18.0 | 195.0 | 3250.0 | Female |
| 3 | Adelie | Torgersen | NaN | NaN | NaN | NaN | NaN |
| 4 | Adelie | Torgersen | 36.7 | 19.3 | 193.0 | 3450.0 | Female |
Les trois espèces :
- Adelie (Pygoscelis adeliae)
- Chinstrap (Pygoscelis antarcticus)
- Gentoo (Pygoscelis papua)
# Distribution des espèces penguins['species'].value_counts()Adelie 152 Gentoo 124 Chinstrap 68 Name: species, dtype: int64
Exploration visuelle
# Visualisation des features par espèce
sns.pairplot(penguins, hue='species',
vars=['bill_length_mm', 'bill_depth_mm',
'flipper_length_mm', 'body_mass_g'])
plt.suptitle('Caractéristiques physiques des pingouins par espèce', y=1.02)
plt.show()
Observations :
- Gentoo : Nageoires plus longues, bec moins profond
- Chinstrap : Bec plus long
- Adelie : Bec plus court et profond
- Séparabilité visible mais pas parfaite
```python
Scatter plot de deux features clés
plt.figure(figsize=(10, 6)) for species in penguins[‘species’].unique(): subset = penguins[penguins[‘species’] == species] plt.scatter(subset[‘bill_length_mm’], subset[‘bill_depth_mm’], label=species, alpha=0.6, s=100)
plt.xlabel(‘Longueur du bec (mm)’, fontsize=12) plt.ylabel(‘Profondeur du bec (mm)’, fontsize=12) plt.title(‘Séparation des espèces par dimensions du bec’, fontsize=14) plt.legend() plt.grid(alpha=0.3) plt.show()
**Propriété importante** : Les classes ne sont pas parfaitement linéairement séparables, mais des frontières peuvent être établies.
### Préparation des données
```python
# Nettoyer les données
penguins_clean = penguins.dropna()
# Séparer features et target
X = penguins_clean[['bill_length_mm', 'bill_depth_mm',
'flipper_length_mm', 'body_mass_g']]
y = penguins_clean['species']
# Vérifier les dimensions
print(f"X shape : {X.shape}")
print(f"y shape : {y.shape}")
print(f"Nombre de classes : {y.nunique()}")
X shape : (333, 4) y shape : (333,) Nombre de classes : 3 ```python from sklearn.model_selection import train_test_split
Split Train/Test
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y # Préserve les proportions de classes )
print(f”Training set : {len(X_train)} échantillons”) print(f”Test set : {len(X_test)} échantillons”)
> Training set : 233 échantillons
> Test set : 100 échantillons
**Propriété du paramètre `stratify`** : Assure que les proportions de chaque classe sont maintenues dans train et test.
### Stratégies pour la classification multi-classes
La plupart des algorithmes de classification sont naturellement **binaires** (deux classes). Comment gérer n classes ?
Deux stratégies principales :
| Stratégie | Principe | Nombre de modèles | Décision finale |
|-----------|----------|-------------------|-----------------|
| **One-vs-Rest (OvR)** | Une classe vs toutes les autres | n | Score le plus élevé |
| **One-vs-One (OvO)** | Chaque paire de classes | n(n-1)/2 | Vote majoritaire |
#### One-vs-Rest (OvR) / One-vs-All (OvA)
**Définition** : Entraîner un classificateur binaire pour chaque classe contre toutes les autres.
**Processus** :
1. Pour la classe 1 : Entraîner un modèle "Classe 1 vs (Classe 2 + Classe 3)"
2. Pour la classe 2 : Entraîner un modèle "Classe 2 vs (Classe 1 + Classe 3)"
3. Pour la classe 3 : Entraîner un modèle "Classe 3 vs (Classe 1 + Classe 2)"
**Prédiction** : Choisir la classe avec le **score le plus élevé** parmi les n modèles.
┌──────────────────────────────────────────────┐ │ Classificateur 1: Adelie vs (Chinstrap+Gentoo) │ │ Score: 0.85 │ ├──────────────────────────────────────────────┤ │ Classificateur 2: Chinstrap vs (Adelie+Gentoo) │ │ Score: 0.32 │ ├──────────────────────────────────────────────┤ │ Classificateur 3: Gentoo vs (Adelie+Chinstrap) │ │ Score: 0.21 │ └──────────────────────────────────────────────┘ ↓ Prédiction: Adelie (score max)
**Propriétés** :
- ✅ Simple à implémenter
- ✅ Efficace computationnellement (n modèles)
- ❌ Classes déséquilibrées dans chaque modèle binaire
#### One-vs-One (OvO)
**Définition** : Entraîner un classificateur binaire pour **chaque paire** de classes.
**Nombre de modèles** : $\frac{n(n-1)}{2}$ où n = nombre de classes
Pour 3 classes : $\frac{3 \times 2}{2} = 3$ modèles
**Processus** :
1. Modèle 1 : Adelie vs Chinstrap
2. Modèle 2 : Adelie vs Gentoo
3. Modèle 3 : Chinstrap vs Gentoo
**Prédiction** : Chaque modèle vote pour une classe, la classe avec le **plus de votes** gagne.
┌────────────────────────────────┐ │ Modèle 1: Adelie vs Chinstrap │ │ Vote: Adelie │ ├────────────────────────────────┤ │ Modèle 2: Adelie vs Gentoo │ │ Vote: Adelie │ ├────────────────────────────────┤ │ Modèle 3: Chinstrap vs Gentoo │ │ Vote: Gentoo │ └────────────────────────────────┘ ↓ Votes: Adelie=2, Chinstrap=0, Gentoo=1 ↓ Prédiction: Adelie (majorité)
**Propriétés** :
- ✅ Classes équilibrées dans chaque modèle binaire
- ✅ Souvent plus précis pour petits datasets
- ❌ Coût computationnel élevé ($O(n^2)$ modèles)
- ❌ Temps de prédiction plus long
#### Comparaison OvR vs OvO
| Critère | One-vs-Rest | One-vs-One |
|---------|-------------|------------|
| **Nb de modèles (3 classes)** | 3 | 3 |
| **Nb de modèles (10 classes)** | 10 | 45 |
| **Temps d'entraînement** | Rapide | Lent pour beaucoup de classes |
| **Équilibre des classes** | Déséquilibré (1 vs n-1) | Équilibré (1 vs 1) |
| **Précision** | Bonne | Souvent meilleure |
| **Sklearn défaut** | Oui (pour la plupart) | Non |
### Implémentation avec Scikit-learn
#### Logistic Regression Multi-classes
**Propriété** : `LogisticRegression` de sklearn gère automatiquement le multi-classes avec OvR par défaut.
```python
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
# Instancier le modèle
model_ovr = LogisticRegression(multi_class='ovr', max_iter=1000)
# Entraîner
model_ovr.fit(X_train, y_train)
# Prédire
y_pred_ovr = model_ovr.predict(X_test)
# Évaluer
accuracy_ovr = accuracy_score(y_test, y_pred_ovr)
print(f"Accuracy (OvR) : {accuracy_ovr:.3f}")
Accuracy (OvR) : 0.970
Rapport de classification détaillé :
print(classification_report(y_test, y_pred_ovr))
precision recall f1-score support Adelie 0.98 0.98 0.98 46 Chinstrap 0.95 0.95 0.95 19 Gentoo 0.97 0.97 0.97 35 accuracy 0.97 100 macro avg 0.97 0.97 0.97 100 weighted avg 0.97 0.97 0.97 100
Interprétation :
- Precision : Parmi les prédictions “Adelie”, 98% sont correctes
- Recall : Parmi les vrais “Adelie”, 98% sont détectés
- F1-score : Moyenne harmonique de precision et recall
- Support : Nombre d’échantillons par classe
One-vs-One explicite
# Forcer la stratégie OvO
model_ovo = LogisticRegression(multi_class='ovr', max_iter=1000)
# Alternative : Wrapper explicite
from sklearn.multiclass import OneVsOneClassifier
from sklearn.svm import SVC
model_ovo_svm = OneVsOneClassifier(SVC(kernel='linear'))
model_ovo_svm.fit(X_train, y_train)
y_pred_ovo = model_ovo_svm.predict(X_test)
accuracy_ovo = accuracy_score(y_test, y_pred_ovo)
print(f"Accuracy (OvO avec SVM) : {accuracy_ovo:.3f}")
Accuracy (OvO avec SVM) : 0.980
Propriété : OneVsOneClassifier est un wrapper qui peut s’appliquer à n’importe quel classificateur binaire.
Visualisation des frontières de décision
Objectif : Visualiser comment le modèle sépare les trois espèces dans l’espace des features.
Simplification : Utiliser seulement 2 features pour la visualisation 2D.
from sklearn.preprocessing import StandardScaler
import numpy as np
# Sélectionner 2 features pour visualisation
X_2d = penguins_clean[['bill_length_mm', 'bill_depth_mm']]
y = penguins_clean['species']
# Standardiser (important pour SVM)
scaler = StandardScaler()
X_2d_scaled = scaler.fit_transform(X_2d)
# Split
X_train_2d, X_test_2d, y_train_2d, y_test_2d = train_test_split(
X_2d_scaled, y, test_size=0.3, random_state=42, stratify=y
)
# Entraîner un SVM
from sklearn.svm import SVC
model_2d = SVC(kernel='rbf', gamma='auto')
model_2d.fit(X_train_2d, y_train_2d)
# Créer une grille pour visualiser les frontières
h = 0.02 # Pas de la grille
x_min, x_max = X_2d_scaled[:, 0].min() - 1, X_2d_scaled[:, 0].max() + 1
y_min, y_max = X_2d_scaled[:, 1].min() - 1, X_2d_scaled[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
# Prédire sur la grille
Z = model_2d.predict(np.c_[xx.ravel(), yy.ravel()])
Z = pd.Categorical(Z).codes # Convertir en codes numériques
Z = Z.reshape(xx.shape)
# Tracer
plt.figure(figsize=(12, 8))
plt.contourf(xx, yy, Z, alpha=0.3, cmap='viridis')
# Tracer les points d'entraînement
colors = {'Adelie': 'red', 'Chinstrap': 'blue', 'Gentoo': 'green'}
for species in penguins_clean['species'].unique():
subset = penguins_clean[penguins_clean['species'] == species]
subset_scaled = scaler.transform(subset[['bill_length_mm', 'bill_depth_mm']])
plt.scatter(subset_scaled[:, 0], subset_scaled[:, 1],
c=colors[species], label=species,
edgecolors='k', s=100, alpha=0.7)
plt.xlabel('Longueur du bec (standardisée)', fontsize=12)
plt.ylabel('Profondeur du bec (standardisée)', fontsize=12)
plt.title('Frontières de décision - Classification des pingouins', fontsize=14)
plt.legend()
plt.show()
Interprétation :
- Les zones colorées représentent les régions où le modèle prédit chaque classe
- Les points sont les données d’entraînement
- Les frontières séparent les régions de décision
Matrice de confusion
Définition : Tableau croisé montrant les prédictions vs les vraies classes.
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
# Calculer la matrice de confusion
cm = confusion_matrix(y_test, y_pred_ovr, labels=model_ovr.classes_)
# Visualiser
disp = ConfusionMatrixDisplay(confusion_matrix=cm,
display_labels=model_ovr.classes_)
disp.plot(cmap='Blues', values_format='d')
plt.title('Matrice de confusion - Pingouins')
plt.show()
Lecture :
Prédictions
Adelie Chinstrap Gentoo
Vraies Adelie 45 1 0
Chinstrap 0 18 1
Gentoo 1 0 34
Interprétation :
- Diagonale : Prédictions correctes
- Hors diagonale : Erreurs
- 1 Adelie prédit comme Chinstrap
- 1 Chinstrap prédit comme Gentoo
- 1 Gentoo prédit comme Adelie
Probabilités de prédiction
Propriété : Les classificateurs sklearn peuvent retourner des probabilités d’appartenance à chaque classe.
# Probabilités pour les 5 premiers échantillons du test set
probas = model_ovr.predict_proba(X_test[:5])
# Créer un DataFrame pour affichage
proba_df = pd.DataFrame(
probas,
columns=model_ovr.classes_,
index=[f"Échantillon {i+1}" for i in range(5)]
)
print(proba_df.round(3))
Adelie Chinstrap Gentoo Échantillon 1 0.981 0.018 0.001 Échantillon 2 0.002 0.125 0.873 Échantillon 3 0.003 0.989 0.008 Échantillon 4 0.975 0.024 0.001 Échantillon 5 0.001 0.005 0.994
Propriétés :
- Chaque ligne somme à 1.0
- Plus la probabilité est proche de 1, plus le modèle est confiant
- Utile pour définir des seuils de décision personnalisés
# Prédictions avec les vraies classes for i in range(5): predicted_class = model_ovr.classes_[probas[i].argmax()] true_class = y_test.iloc[i] confidence = probas[i].max() print(f"Échantillon {i+1}: Prédit={predicted_class} (confiance={confidence:.1%}), " f"Vrai={true_class}")Échantillon 1: Prédit=Adelie (confiance=98.1%), Vrai=Adelie Échantillon 2: Prédit=Gentoo (confiance=87.3%), Vrai=Gentoo Échantillon 3: Prédit=Chinstrap (confiance=98.9%), Vrai=Chinstrap Échantillon 4: Prédit=Adelie (confiance=97.5%), Vrai=Adelie Échantillon 5: Prédit=Gentoo (confiance=99.4%), Vrai=Gentoo
Algorithmes natifs multi-classes
Certains algorithmes gèrent naturellement le multi-classes sans stratégie OvR/OvO :
| Algorithme | Multi-classes natif | Stratégie par défaut |
|---|---|---|
| Logistic Regression | ❌ | OvR (sklearn) |
| SVM | ❌ | OvR (sklearn) |
| Decision Trees | ✅ | Native |
| Random Forest | ✅ | Native |
| Naive Bayes | ✅ | Native |
| K-Nearest Neighbors | ✅ | Native |
| Neural Networks | ✅ | Native (softmax) |
Exemple avec Random Forest :
from sklearn.ensemble import RandomForestClassifier
# Random Forest gère nativement le multi-classes
rf_model = RandomForestClassifier(n_estimators=100, random_state=42)
rf_model.fit(X_train, y_train)
y_pred_rf = rf_model.predict(X_test)
accuracy_rf = accuracy_score(y_test, y_pred_rf)
print(f"Accuracy (Random Forest) : {accuracy_rf:.3f}")
Accuracy (Random Forest) : 0.990
Avantage : Pas besoin de wrapper, plus efficace computationnellement.
Application en chimie computationnelle
- Classification de familles de molécules
- Classes : Alcanes, Alcènes, Aromatiques, Hétérocycles
- Features : Descripteurs moléculaires (MW, LogP, HBD, HBA)
- Prédiction de types de réactions
- Classes : Substitution, Addition, Élimination, Réarrangement
- Features : Groupes fonctionnels, électronégativité, encombrement stérique
- Classification de spectres
- Classes : Composés purs identifiés
- Features : Pics d’absorption, intensités
- Prédiction de toxicité
- Classes : Non-toxique, Faible, Modéré, Élevé
- Features : Structure moléculaire, propriétés physico-chimiques
Code template pour classification chimique :
from rdkit import Chem
from rdkit.Chem import Descriptors
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_validate
# Calculer descripteurs depuis SMILES
def compute_descriptors(smiles):
mol = Chem.MolFromSmiles(smiles)
if mol is None:
return None
return {
'MW': Descriptors.MolWt(mol),
'LogP': Descriptors.MolLogP(mol),
'HBD': Descriptors.NumHDonors(mol),
'HBA': Descriptors.NumHAcceptors(mol),
'TPSA': Descriptors.TPSA(mol),
'RotBonds': Descriptors.NumRotatableBonds(mol)
}
# Exemple de workflow
# df_molecules['descriptors'] = df_molecules['SMILES'].apply(compute_descriptors)
# X = pd.DataFrame(df_molecules['descriptors'].tolist())
# y = df_molecules['famille_chimique']
#
# model = RandomForestClassifier(n_estimators=200, random_state=42)
# cv_results = cross_validate(model, X, y, cv=5)
# print(f"Accuracy moyenne : {cv_results['test_score'].mean():.3f}")
Récapitulatif
Concepts clés :
- Classification multi-classes : n > 2 classes
- One-vs-Rest : n modèles binaires, score maximal
- One-vs-One : n(n-1)/2 modèles, vote majoritaire
- Algorithmes natifs multi-classes : plus efficaces
Dataset Palmer Penguins :
- 3 espèces (Adelie, Chinstrap, Gentoo)
- 4 features morphologiques
- Excellent exemple pédagogique
Métriques :
- Accuracy globale
- Precision/Recall/F1 par classe
- Matrice de confusion
- Probabilités de prédiction
Sklearn :
multi_class='ovr'ou'multinomial'OneVsOneClassifierwrapperpredict_proba()pour probabilitésclassification_report()détaillé
Préparation des données
- 📚 Sklearn Preprocessing Guide
- 📚 Imbalanced-learn Documentation
- 📖 Feature Engineering for Machine Learning (A. Zheng & A. Casari)
La préparation des données est souvent considérée comme l’étape la plus chronophage du Machine Learning, représentant 60-80% du temps total d’un projet.
Trois raisons fondamentales au data pré-processing :
- Les données b rutes peuvent être sales et bruitées
- Valeurs manquantes
- Doublons
- Erreurs de saisie
- Outliers
- Contraintes algorithmiques
- Certains algorithmes n’acceptent que des valeurs numériques,
- sont sensibles aux échelles,
- ou supposent des distributions spécifiques
- Amélioration des performances
- Transformations appropriées → Meilleur apprentissage
- Feature engineering → Signaux plus forts
- Sélection de features → Réduction du bruit
Dataset d’exemple utilisé dans cette partie
| GrLivArea | BedroomAbvGr | KitchenAbvGr | OverallCond | Pesos | Alley | Street | WallMat | SalePrice | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1710 | 3 | 1 | 5 | 4170000.0 | NaN | Pave | Concrete | 208500 |
| 1 | 1262 | 3 | 1 | 8 | 3630000.0 | NaN | Pave | Wood | 181500 |
| 2 | 1786 | 3 | 1 | 5 | 4470000.0 | NaN | Pave | Wood | 223500 |
| 3 | 1717 | 3 | 1 | 5 | 2800000.0 | NaN | Pave | Concrete | 140000 |
| 4 | 2198 | 4 | 1 | 5 | 5000000.0 | NaN | Pave | Concrete | 250000 |
Pipeline de préparation
Workflow complet :
Data Exploration
↓
Deduplicate (Supprimer doublons)
↓
Missing Data (Gérer valeurs manquantes)
↓
Outliers (Traiter valeurs aberrantes)
↓
Scaling (Normaliser les échelles)
↓
Balancing (Équilibrer les classes)
↓
Encoding / Discretizing / Feature Creation
↓
Feature Correlation (Analyse)
↓
Modeling
↓
Feature Selection (Permutation)
↓
Remodel (Itération)
Duplicates
Une ligne dupliquée est une copie exacte d’une autre ligne dans le dataset.
Impact sur l’évaluation :
- Fausse l’évaluation de la généralisation
- Peut créer du data leakage si présent dans train ET test
- Gonfle artificiellement les scores
Exemple :
data[["GrLivArea", "SalePrice"]].head(10)
| GrLivArea | SalePrice | |
|---|---|---|
| 0 | 1710 | 208500 |
| 1 | 1262 | 181500 |
| 2 | 1786 | 223500 |
| 3 | 1717 | 140000 |
| 4 | 2198 | 250000 |
| 5 | 1362 | 143000 |
| 6 | 1694 | 307000 |
| 7 | 2090 | 200000 |
| 8 | 1774 | 129900 |
| 9 | 1077 | 118000 |
Data Leakage
Scénario problématique :
- Dataset contient des doublons
- Split train/test aléatoire
- Même observation dans train ET test
- Le modèle a “déjà vu” les données de test !
- Score artificiellement élevé
Propriété : Pour évaluer la généralisation, les données du test set doivent être totalement inédites pendant l’entraînement.
Détection et suppression
Détection :
# Nombre de lignes avant
print(f"Nombre de lignes : {len(data)}")
Nombre de lignes : 1760
# Vérifier si chaque ligne est un doublon print(data.duplicated())0 False 1 False 2 False 3 False … 1755 True 1756 True 1757 True 1758 True 1759 True Length: 1760, dtype: bool
# Compter le nombre total de doublons print(f"Nombre de doublons : {data.duplicated().sum()}")Nombre de doublons : 303
Suppression :
# Supprimer les doublons
data = data.drop_duplicates()
# Vérifier le nouveau nombre de lignes
print(f"Nombre de lignes après nettoyage : {len(data)}")
Nombre de lignes après nettoyage : 1457
Propriété : drop_duplicates() garde la première occurrence et supprime les suivantes.
Missing Data
Causes et représentations courantes
Raisons des valeurs manquantes :
| Cause | Exemple |
|---|---|
| Erreur de programmation | Bug dans la collecte |
| Échec de mesure | Patient absent à un rendez-vous clinique |
| Événement aléatoire | Capteur météo sans batterie |
| Saisie incorrecte | Champ laissé vide dans un formulaire |
| Valeur non applicable | “Revenu du conjoint” pour célibataire |
Valeurs manquantes déguisées :
NaN(Not a Number) - Standard999ou-999(Nombres suspects)?(Caractère interrogation)±∞(Valeurs infinies)""(Chaîne vide)NULL,None,NA
⚠️ Toujours explorer les données pour détecter ces représentations !
Détection
Comptage par colonne :
# Nombre de NaN par colonne (ordre décroissant)
missing_counts = data.isnull().sum().sort_values(ascending=False)
print(missing_counts)
WallMat 1452 Alley 1367 Pesos 10 GrLivArea 0 BedroomAbvGr 0 KitchenAbvGr 0 OverallCond 0 Street 0 SalePrice 0 dtype: int64
Pourcentage de valeurs manquantes :
# Pourcentage par colonne
missing_pct = (data.isnull().sum() / len(data)).sort_values(ascending=False)
print(missing_pct)
WallMat 0.997 (99.7% !) Alley 0.938 (93.8%) Pesos 0.007 (0.7%) GrLivArea 0.000 … dtype: float64
Stratégies de traitement
Questions à se poser :
- Quelle est la cause des valeurs manquantes ?
- Les valeurs manquantes racontent-elles une histoire ?
- Puis-je les remplacer par une autre valeur ?
- Puis-je me permettre de perdre ces données ?
⚠️ Connaissance du domaine : Essentielle pour choisir la bonne stratégie !
Cas 1 : WallMat (>99% manquant)
# Pourcentage de valeurs manquantes
pct_missing = data.WallMat.isnull().sum() / len(data)
print(f"WallMat : {pct_missing:.1%} manquant")
WallMat : 99.7% manquant
Décision : Supprimer la feature (trop de valeurs manquantes)
# Supprimer la colonne
data = data.drop(columns='WallMat')
Règle empirique : >30% manquant → Considérer suppression
Cas 2 : Alley (~94% manquant)
# Pourcentage de valeurs manquantes
pct_missing = data.Alley.isnull().sum() / len(data)
print(f"Alley : {pct_missing:.1%} manquant")
Alley : 93.8% manquant
Attention : Valeur manquante ≠ Absence d’information !
Interprétation : NaN signifie ici “la maison n’a pas d’allée”.
Solution : Remplacer par une catégorie explicite
# Remplacer NaN par "NoAlley"
data.Alley = data.Alley.replace(np.nan, "NoAlley")
# Vérifier les catégories
print(data.Alley.value_counts())
NoAlley 1367 Grvl 50 Pave 40 Name: Alley, dtype: int64
Propriété : Le NaN devient une information utile !
Cas 3 : Pesos (<1% manquant)
# Pourcentage de valeurs manquantes
pct_missing = data.Pesos.isnull().sum() / len(data)
print(f"Pesos : {pct_missing:.1%} manquant")
Pesos : 0.7% manquant
Deux options :
Option 1 : Supprimer les lignes
# Supprimer les lignes où Pesos est manquant
data_clean = data.dropna(subset=['Pesos'])
Option 2 : Imputer (remplacer) les valeurs
# Remplacer par la moyenne
data.Pesos = data.Pesos.replace(np.nan, data.Pesos.mean())
Comparaison des options :
| Critère | Option 1 (Supprimer) | Option 2 (Imputer) |
|---|---|---|
| Perte de données | Oui (10 lignes) | Non |
| Biais introduit | Non | Potentiel (moyenne ≠ vraie valeur) |
| Simplicité | ✅ Simple | Choix de stratégie nécessaire |
Règles empiriques
| Pourcentage manquant | Action recommandée |
|---|---|
| > 30% | Potentiellement supprimer feature/lignes |
| < 30% | Considérer imputation |
| < 5% | Imputation sans risque majeur |
==Imputer = Approximation → Peut introduire bruit et biais==
SimpleImputer de Scikit-learn
Outil pour remplacer les valeurs manquantes selon une stratégie.
Stratégies disponibles :
| Stratégie | Description | Usage typique |
|---|---|---|
'mean' |
Moyenne | Features continues normales |
'median' |
Médiane | Features avec outliers |
'most_frequent' |
Mode | Features catégorielles |
'constant' |
Valeur fixe | Remplissage par 0 ou catégorie |
Implémentation :
from sklearn.impute import SimpleImputer
# 1. Instancier avec stratégie
imputer = SimpleImputer(strategy="mean")
# 2. Fit : Calculer la moyenne
imputer.fit(data[['Pesos']])
# 3. Transform : Appliquer la transformation
data['Pesos'] = imputer.transform(data[['Pesos']])
# Valeur stockée (moyenne)
print(f"Moyenne calculée : {imputer.statistics_[0]:,.0f}")
Moyenne calculée : 3,608,796
Fonctionnement interne :
.fit() :
- Calcule la statistique (moyenne, médiane, etc.)
- Stocke la valeur comme attribut
.transform() :
- Identifie les valeurs manquantes
- Les remplace par la valeur calculée dans
.fit()
Propriété importante : Pattern fit/transform = Standard sklearn pour les transformateurs
Outliers
Points de données qui dévient significativement du reste des données.

Causes courantes :
| Cause | Exemple |
|---|---|
| ⌨️ Erreur de saisie | Age = 999 ans |
| 📐 Erreur de mesure | Balance déréglée |
| 🧑🏻🔬 Erreur de preprocessing | Unité incorrecte (km au lieu de m) |
| 🆕 Nouveauté légitime | Vente exceptionnelle d’un château |
Impact des outliers
Effets :
- Distributions : Distorsion des patterns
- Tendance centrale : Moyenne biaisée
- Dispersion : Écart-type gonflé
- Performance ML : Modèle perturbé
Exemple d’impact :
# Dataset sans outlier
data_normal = [10, 12, 11, 13, 12, 11, 10, 13]
print(f"Moyenne : {np.mean(data_normal):.1f}")
Moyenne : 11.5
# Même dataset avec un outlier data_with_outlier = [10, 12, 11, 13, 12, 11, 10, 1000] print(f"Moyenne : {np.mean(data_with_outlier):.1f}")Moyenne : 135.9
Détection visuelle : Boxplot
Outil principal : Le boxplot (boîte à moustaches)
import matplotlib.pyplot as plt
# Boxplot de GrLivArea
data[['GrLivArea']].boxplot()
plt.title('Distribution de la surface habitable')
plt.ylabel('Surface (ft²)')
plt.show()

Lecture d’un boxplot :
│
○ │ ← Outlier extrême
│
● │ ← Outlier léger
┬────┤ ← Moustache supérieure (Q3 + 1.5×IQR)
│ │
├────┤ ← Q3 (75e percentile)
│////│ ← Médiane (Q2, 50e percentile)
├────┤ ← Q1 (25e percentile)
│ │
┴────┤ ← Moustache inférieure (Q1 - 1.5×IQR)
│
● │ ← Outlier
IQR (Interquartile Range) : $IQR = Q3 - Q1$
Ressources :
Investigation des outliers
Question clé : Tous les outliers sont-ils de “vrais” outliers ?
# Valeur minimale
print(f"Surface minimale : {data['GrLivArea'].min()} ft²")
Surface minimale : -1 ft²
Une surface de -1 ft² est physiquement impossible !
Traitement des outliers
Questions à se poser :
- L’outlier est-il évidemment faux ?
- Pourrait-il être une nouveauté légitime ?
- Pourrait-il être utilisé comme feature ?
→ Les outliers peuvent être subjectifs. Comprendre avant de supprimer !
Cas 1 : Valeur manifestement fausse
# Identifier la ligne problématique
print(data['GrLivArea'] == -1)
0 False 1 False … 1455 False dtype: bool
Cas 2 : Valeurs extrêmes mais possibles
→ Définir des seuils raisonnables
# Créer un masque booléen
# Garder uniquement : 0 < surface < 5000 ft²
mask = (data['GrLivArea'] > 0) & (data['GrLivArea'] < 5000)
# Appliquer le filtre
data = data[mask].reset_index(drop=True)
# Vérifier le résultat
data[['GrLivArea']].boxplot()
plt.title('Distribution après nettoyage')
plt.show()

Propriété : reset_index(drop=True) réinitialise les indices après filtrage.
Scaling
Le feature scaling est le processus de transformation des features numériques vers une échelle commune plus petite.
Pourquoi scaler ?
| Raison | Explication |
|---|---|
| ❗️ Dominance par magnitude | Features à grande échelle dominent injustement |
| ⚡️ Efficacité computationnelle | Convergence plus rapide des algorithmes |
| 🕵🏻♂️ Interprétabilité | Comparaison de l’impact de chaque feature |
Exemple de problème :
| House | GrLivArea (ft²) | Pesos |
|---|---|---|
| 1 | 1,710 | 4,170,000 |
| 2 | 1,262 | 3,630,000 |
→ Pesos a des valeurs ~1000x plus grandes que GrLivArea.
→ Un algorithme basé sur les distances (KNN, SVM) sera biaisé vers Pesos.
Features numériques du dataset
data.head(3)
| GrLivArea | BedroomAbvGr | KitchenAbvGr | OverallCond | Pesos | Alley | Street | SalePrice | |
|---|---|---|---|---|---|---|---|---|
| 0 | 1710 | 3 | 1 | 5 | 4170000.0 | NoAlley | Pave | 208500 |
| 1 | 1262 | 3 | 1 | 8 | 3630000.0 | NoAlley | Pave | 181500 |
| 2 | 1786 | 3 | 1 | 5 | 4470000.0 | NoAlley | Pave | 223500 |
Features numériques : GrLivArea, BedroomAbvGr, KitchenAbvGr, OverallCond, Pesos
Conversion : 1 USD ≈ 20 Pesos (exemple de taux utilisé)

Les trois scalers principaux
| Scaler | Formule | Résultat |
|---|---|---|
| StandardScaler | $(x - \mu) / \sigma$ | μ=0, σ=1 |
| MinMaxScaler | $(x - x_{min}) / (x_{max} - x_{min})$ | [0, 1] |
| RobustScaler | $(x - median) / IQR$ | Centré sur médiane |
StandardScaler
\[z = \frac{x - \mu}{\sigma}\]→ Distribution centrée sur 0 avec écart-type de 1.
Efficace pour features normalement distribuées Pas d’échelle exacte commune, mais ~99% dans [-3, +3] Sensible aux outliers (moyenne et std affectés) Distorsion possible des distances relatives
Quand l’utiliser :
- Features gaussiennes
- Régression linéaire, réseaux de neurones
- Par défaut pour débuter rapidement
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Pour retrouver les moyennes et écarts-types
scaler.mean_, scaler.scale_
Effet visuel :

MinMaxScaler
Méthode aussi appelée “Normalisation”.
\[X' = \frac{X - X_{min}}{X_{max} - X_{min}}\]→ Toutes les valeurs compressées dans [0, 1].
Échelle fixe garantie [0, 1] Ne réduit pas l’effet des outliers Préserve la sparsité (0 reste 0)
Quand l’utiliser :
- Features ordinales (ex: scores 1-5)
- Features positives ou matrices creuses (ex: pixels RGB 0-255)
- Algorithme KNN (K-Nearest Neighbors)
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler(feature_range=(0, 1))
X_scaled = scaler.fit_transform(X)
# Pour retrouver les bornes utilisées
scaler.data_min_, scaler.data_max_
Effet visuel :

RobustScaler
\[X' = \frac{x - \text{median}}{IQR}\]Où $IQR = Q3 - Q1$ (Interquartile Range)
Résistant aux outliers (médiane et IQR moins sensibles) Préserve les patterns sans être influencé par les extrêmes
Quand l’utiliser :
- Features avec outliers qu’on ne veut/peut pas supprimer
- Features très asymétriques (skewed)
from sklearn.preprocessing import RobustScaler
scaler = RobustScaler()
X_scaled = scaler.fit_transform(X)
# Pour retrouver la médiane et l'IQR
scaler.center_, scaler.scale_
Tableau comparatif et règles de décision des scalers
| Critère | StandardScaler | MinMaxScaler | RobustScaler |
|---|---|---|---|
| Métrique centrale | Moyenne (μ) | Min | Médiane |
| Métrique dispersion | Std (σ) | Max - Min | IQR (Q3-Q1) |
| Résultat | μ=0, σ=1 | [0, 1] | Centré médiane |
| Sensibilité outliers | ❌ Élevée | ❌ Élevée | ✅ Faible |
| Préserve sparsité | ❌ Non | ✅ Oui | ❌ Non |
| Usage typique | Features gaussiennes | KNN, images, ordinal | Features skewed |
Arbre de décision pour le scaling :
Feature très asymétrique (skewed) ?
↓ OUI
Feature Engineering d'abord (ex: log)
↓ Si toujours problème
RobustScaler
↓
Sinon (distribution raisonnable)
↓
Feature positive / Matrice creuse ?
↓ OUI
MinMaxScaler
↓ NON
StandardScaler (défaut sûr)
Résumé des règles :
| Situation | Scaler recommandé |
|---|---|
| Feature très skewed → Engineering | log(feature), sqrt(feature) |
| Si Engineering insuffisant | RobustScaler |
| Feature positive/creuse (ex: pixels) | MinMaxScaler |
| Feature ordinale (ex: score 1-5) | MinMaxScaler |
| Outliers légitimes à préserver | MinMaxScaler |
| Par défaut / Features gaussiennes | StandardScaler |
| Modèles zéro-centrés (LinReg, NN) | StandardScaler |
⚠️ Toute règle a des exceptions ! Toujours expérimenter.
Application dans l’exemple : GrLivArea
Exploration de la distribution
import seaborn as sns
import matplotlib.pyplot as plt
# Distribution de GrLivArea
sns.histplot(data['GrLivArea'], bins=200, kde=True)
plt.title('Distribution de la surface habitable')
plt.xlabel('Surface (ft²)')
plt.show()

→ Distribution légèrement asymétrique avec quelques valeurs élevées.
Détection des outliers
# Boxplot
sns.boxplot(data=data, x='GrLivArea')
plt.title('Outliers dans GrLivArea')
plt.show()

Observation : Présence de quelques outliers (points au-delà des moustaches).
Choix du scaler
Quel scaler appliquer à GrLivArea ?
Analyse :
- Distribution pas parfaitement gaussienne ❌ StandardScaler non optimal
- Présence d’outliers ✅ RobustScaler recommandé
- Pas de contrainte [0,1] nécessaire ❌ MinMaxScaler pas prioritaire
→ RobustScaler
Implémentation
from sklearn.preprocessing import RobustScaler
# Étape 0 : Instancier
rb_scaler = RobustScaler()
# Étape 1 : Fit (apprendre médiane et IQR)
rb_scaler.fit(data[['GrLivArea']])
# Afficher les valeurs apprises
print(f"Médiane : {rb_scaler.center_[0]:.0f}")
print(f"IQR : {rb_scaler.scale_[0]:.0f}")
# Étape 2 : Transform (appliquer la transformation)
data['GrLivArea'] = rb_scaler.transform(data[['GrLivArea']])
data.head()
Médiane : 1464 IQR : 555
| GrLivArea | BedroomAbvGr | KitchenAbvGr | OverallCond | Pesos | … | |
|---|---|---|---|---|---|---|
| 0 | 0.380 | 3 | 1 | 5 | 4170000.0 | … |
| 1 | -0.312 | 3 | 1 | 8 | 3630000.0 | … |
| 2 | 0.498 | 3 | 1 | 5 | 4470000.0 | … |
Interprétation :
- Ligne 0 : Surface 0.38 IQR au-dessus de la médiane
- Ligne 1 : Surface 0.31 IQR en-dessous de la médiane
Dataset Balancing
Un dataset est déséquilibré (imbalanced) quand certaines classes sont sur-représentées ou sous-représentées.
Exemples :
| Domaine | Contexte | Ratio typique |
|---|---|---|
| 🦠 Médecine | Prévalence de maladie rare | 1:99 |
| 💳 Banque | Détection de fraude | 1:1000 |
| 🛍️ E-commerce | Taux de conversion (clics sur pub) | 1:50 |
| 🙋🏿♂️ Démographie | Représentation ethnique | Variable |
| 💃🏼 Égalité | Parité homme-femme dans études | Variable |
Ressources :
Pourquoi équilibrer ?
Les algorithmes ML apprennent par l’exemple.
Problème :
- Classe minoritaire sous-représentée → Modèle apprend mal
- Modèle biaisé vers la classe majoritaire
- Prédictions erronées sur la classe minoritaire
Seuil de déséquilibre : Un ratio 70/30 peut déjà être considéré comme problématique en classification binaire.
Exemple :
# Dataset déséquilibré
class_counts = pd.Series([90, 10], index=['Classe A', 'Classe B'])
print(class_counts)
Classe A 90 Classe B 10
Conséquence : Un modèle “stupide” qui prédit toujours “Classe A” aura 90% d’accuracy !
Stratégies d’équilibrage
Deux approches principales :
| Stratégie | Principe | Action |
|---|---|---|
| Oversampling | Augmenter minorité | Dupliquer ou créer instances |
| Undersampling | Réduire majorité | Échantillonner la majorité |

🔗 Source
Comparaison :
| Aspect | Oversampling | Undersampling |
|---|---|---|
| Taille dataset | Augmente | Diminue |
| Perte d’information | Non | Oui (majorité) |
| Risque overfitting | Oui (duplication) | Non |
| Temps d’entraînement | Augmente | Diminue |
Protocole d’oversampling
⚠️ ATTENTION : Ordre des opérations crucial pour éviter le data leakage !
Protocole correct :
1. Split Train/Test AVANT oversampling
2. Oversample UNIQUEMENT le train set
3. Évaluer sur le test set SANS oversampling
- Train : Le modèle doit apprendre la classe minoritaire → Oversampling OK
- Test : Doit refléter la vraie distribution → Pas d’oversampling
Erreur commune :
# ❌ MAUVAIS : Oversampling avant split
X_oversampled, y_oversampled = oversample(X, y)
X_train, X_test, y_train, y_test = train_test_split(X_oversampled, y_oversampled)
# Problème : Test set contient des duplicata → Data leakage !
Bonne pratique :
# ✅ BON : Split puis oversampling
X_train, X_test, y_train, y_test = train_test_split(X, y)
X_train_over, y_train_over = oversample(X_train, y_train)
# Test set reste authentique
SMOTE (Synthetic Minority Oversampling Technique)
SMOTE est un algorithme d’oversampling qui génère de nouvelles instances de la classe minoritaire par interpolation linéaire.
Principe :
- Choisir un point minoritaire $x_i$
- Trouver ses $k$ plus proches voisins (même classe)
- Choisir aléatoirement un voisin $x_{nn}$
- Créer un nouveau point entre $x_i$ et $x_{nn}$ :
où $\lambda \in [0, 1]$ est aléatoire.
Visualisation :


🔗 Source
Avantages :
- Crée des instances synthétiques (pas de duplication exacte)
- Réduit le risque d’overfitting vs simple duplication
- Points créés dans des régions “plausibles”
Implémentation :
# Installation nécessaire
# !pip install imbalanced-learn
from imblearn.over_sampling import SMOTE
from sklearn.model_selection import train_test_split
# Split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
# Distribution avant SMOTE
print(f"Train avant SMOTE: {pd.Series(y_train).value_counts()}")
# Instancier SMOTE
smote = SMOTE(random_state=42)
# Appliquer SMOTE sur train uniquement
X_train_balanced, y_train_balanced = smote.fit_resample(X_train, y_train)
# Distribution après SMOTE
print(f"Train après SMOTE: {pd.Series(y_train_balanced).value_counts()}")
Train avant SMOTE: 0 630 1 70
Train après SMOTE: 0 630 1 630
Propriété : La bibliothèque imbalanced-learn (import imblearn) s’appuie sur scikit-learn.
Encoding
L’encodage consiste à transformer des données non-numériques en une forme numérique équivalente.
Pourquoi encoder ?
| Raison | Explication |
|---|---|
| 🔠 Données textuelles | Mots, lettres, symboles |
| 🤖 Contrainte algorithmique | La plupart des algos ML n’acceptent que des nombres |
| 🧮 Opérations mathématiques | Impossible de faire $\text{“chat”} + \text{“chien”}$ |
OrdinalEncoder
OrdinalEncoder assigne un nombre à chaque catégorie.
Exemple : Feature “qualité”
example = pd.DataFrame({
"classes": ["bad", "average", "average", "good", "good", "bad", "good"]
})
print(example)
| classes | |
|---|---|
| 0 | bad |
| 1 | average |
| 2 | average |
| 3 | good |
| 4 | good |
| 5 | bad |
| 6 | good |
Encodage par défaut (ordre alphabétique) :
from sklearn.preprocessing import OrdinalEncoder
# Instancier
ordinal_encoder = OrdinalEncoder()
# Fit
ordinal_encoder.fit(example[["classes"]])
# Catégories détectées
print(f"Catégories : {ordinal_encoder.categories_}")
# Transform
example["encoded_classes"] = ordinal_encoder.transform(example[["classes"]])
print(example)
Catégories : [array([‘average’, ‘bad’, ‘good’], dtype=object)]
| classes | encoded_classes | |
|---|---|---|
| 0 | bad | 1.0 |
| 1 | average | 0.0 |
| 2 | average | 0.0 |
| 3 | good | 2.0 |
| 4 | good | 2.0 |
| 5 | bad | 1.0 |
| 6 | good | 2.0 |
Problème : Ordre alphabétique ! On voudrait bad=0, average=1, good=2.
Encodage avec ordre spécifié :
# Spécifier l'ordre logique
ordinal_encoder = OrdinalEncoder(categories=[["bad", "average", "good"]])
ordinal_encoder.fit(example[["classes"]])
example["encoded_classes"] = ordinal_encoder.transform(example[["classes"]])
print(example)
| classes | encoded_classes | |
|---|---|---|
| 0 | bad | 0.0 |
| 1 | average | 1.0 |
| 2 | average | 1.0 |
| 3 | good | 2.0 |
| 4 | good | 2.0 |
| 5 | bad | 0.0 |
| 6 | good | 2.0 |
Quand utiliser OrdinalEncoder :
✅ Variables ordinales (ordre intrinsèque)
- Qualité : Mauvais < Moyen < Bon
- Éducation : Primaire < Secondaire < Supérieur
- Taille : S < M < L < XL
❌ Variables nominales (pas d’ordre)
- Types d’arbres : Chêne, Pin, Érable
- Couleurs : Rouge, Vert, Bleu
- Pays : France, USA, Japon
Problème avec les nominales :

Si on encode Chêne=0, Pin=1, Érable=2, on crée une fausse relation d’ordre :
- Le modèle pense que
Érable>Pin>Chêne - Aucun sens !
Solution : OneHotEncoder
OneHotEncoder
Créer une colonne binaire pour chaque catégorie possible.
Principe :

Avantages :
- Pas de relation d’ordre artificielle
- Chaque catégorie est indépendante
- Fonctionne pour variables nominales
Application : Alley (3 catégories)
from sklearn.preprocessing import OneHotEncoder
# Valeurs uniques
print(f"Valeurs uniques : {data.Alley.unique()}")
Valeurs uniques : [‘NoAlley’ ‘Grvl’ ‘Pave’] ```python
Instancier
ohe = OneHotEncoder(sparse_output=False)
Fit
ohe.fit(data[[‘Alley’]])
Catégories détectées
print(f”Catégories : {ohe.categories_}”)
> Catégories : [array(['Grvl', 'NoAlley', 'Pave'], dtype=object)]
```python
# Noms des nouvelles colonnes
print(f"Colonnes générées : {ohe.get_feature_names_out()}")
Colonnes générées : [‘Alley_Grvl’ ‘Alley_NoAlley’ ‘Alley_Pave’] ```python
Transform et ajouter au DataFrame
data[ohe.get_feature_names_out()] = ohe.transform(data[[‘Alley’]])
Supprimer la colonne originale
data = data.drop(columns=[“Alley”])
data.head(3)
| | GrLivArea | ... | SalePrice | Alley_Grvl | Alley_NoAlley | Alley_Pave |
|---|---|---|---|---|---|---|
| 0 | 0.380 | ... | 208500 | 0.0 | 1.0 | 0.0 |
| 1 | -0.312 | ... | 181500 | 0.0 | 1.0 | 0.0 |
| 2 | 0.498 | ... | 223500 | 0.0 | 1.0 | 0.0 |
**Interprétation** :
- Ligne 0 : `Alley_NoAlley=1` → Cette maison n'a pas d'allée
- Chaque ligne a exactement un `1` et deux `0` (exclusif)
#### Application : Street (2 catégories - Binaire)
```python
# Valeurs uniques
print(f"Valeurs uniques : {data.Street.unique()}")
Valeurs uniques : [‘Pave’ ‘Grvl’]
Optimisation pour binaire : drop='if_binary'
Propriété : Pour une variable binaire, une seule colonne suffit !
Street_Pave = 0→ GrvlStreet_Pave = 1→ Pave ```pythonInstancier avec drop
ohe_binary = OneHotEncoder(sparse_output=False, drop=”if_binary”)
ohe_binary.fit(data[[‘Street’]]) print(f”Colonnes générées : {ohe_binary.get_feature_names_out()}”)
> Colonnes générées : ['Street_Pave']
```python
# Transform
data[ohe_binary.get_feature_names_out()] = ohe_binary.transform(data[['Street']])
data = data.drop(columns=["Street"])
data.head(3)
| GrLivArea | … | Alley_Grvl | Alley_NoAlley | Alley_Pave | Street_Pave | |
|---|---|---|---|---|---|---|
| 0 | 0.380 | … | 0.0 | 1.0 | 0.0 | 1.0 |
| 1 | -0.312 | … | 0.0 | 1.0 | 0.0 | 1.0 |
| 2 | 0.498 | … | 0.0 | 1.0 | 0.0 | 1.0 |
Économie : 1 colonne au lieu de 2 pour variable binaire !
LabelEncoder (Pour la target)
Encoder une target catégorielle (pas les features).
Exemple : Prédire l’espèce de pingouins
import seaborn as sns
# Charger dataset
penguins = sns.load_dataset("penguins")
target = penguins["species"]
print(target.value_counts())
Adelie 152 Gentoo 124 Chinstrap 68 Name: species, dtype: int64
Encodage :
from sklearn.preprocessing import LabelEncoder
# Instancier
label_encoder = LabelEncoder()
# Fit
label_encoder.fit(target)
# Classes encodées
print(f"Classes : {label_encoder.classes_}")
Classes : [‘Adelie’ ‘Chinstrap’ ‘Gentoo’] ```python
Transform
encoded_target = label_encoder.transform(target)
Comparaison
comparison = pd.DataFrame({ “target”: target, “encoded_target”: encoded_target }).sample(10) print(comparison)
| | target | encoded_target |
|---|---|---|
| 128 | Adelie | 0 |
| 275 | Gentoo | 2 |
| 215 | Chinstrap | 1 |
| 141 | Adelie | 0 |
**Décodage (inverse)** :
```python
# Revenir à l'original
original_target = label_encoder.inverse_transform(encoded_target)
pd.DataFrame({
"encoded_target": encoded_target,
"original_target": original_target,
"target": target
}).sample(5)
💡 Important : Dans la plupart des cas, pas besoin d’encoder la target !
La plupart des modèles sklearn gèrent automatiquement l’encodage interne des targets catégorielles.
Règle : Essayez d’abord sans encoder la target. Encodez seulement si le modèle l’exige explicitement.
Discretizing
La discrétisation est le processus de transformation de données continues en données discrètes via des bins (intervalles).
Usages :
| Usage | Description |
|---|---|
| Feature Engineering | Créer des catégories à partir de continu |
| Régression → Classification | Transformer un problème de prédiction |
Exemple : SalePrice
Objectif : Au lieu de prédire le prix exact (régression), classifier comme “Cheap” ou “Expensive” (classification).
Stratégie : Utiliser la moyenne comme seuil.
# Créer bins basés sur la moyenne
data['SalePriceBinary'] = pd.cut(
x=data['SalePrice'],
bins=[
data['SalePrice'].min() - 1, # Début (inclus minimum)
data['SalePrice'].mean(), # Seuil
data['SalePrice'].max() + 1 # Fin (inclus maximum)
],
labels=['cheap', 'expensive']
)
data.head()
| GrLivArea | … | SalePrice | SalePriceBinary | |
|---|---|---|---|---|
| 0 | 0.380 | … | 208500 | expensive |
| 1 | -0.312 | … | 181500 | expensive |
| 2 | 0.498 | … | 223500 | expensive |
| 3 | 0.391 | … | 140000 | cheap |
| 4 | 1.134 | … | 250000 | expensive |
Interprétation :
- Prix > Moyenne →
expensive - Prix ≤ Moyenne →
cheap
Variantes possibles :
Discrétisation en quartiles :
# Créer 4 catégories (quartiles)
data['SalePriceQuartile'] = pd.qcut(
x=data['SalePrice'],
q=4,
labels=['Q1', 'Q2', 'Q3', 'Q4']
)
Discrétisation personnalisée :
# Seuils personnalisés
data['SalePriceCustom'] = pd.cut(
x=data['SalePrice'],
bins=[0, 100000, 200000, 300000, np.inf],
labels=['Budget', 'Standard', 'Premium', 'Luxe']
)
Feature Engineering
La création de features consiste à introduire de la connaissance du domaine pour générer de nouveaux signaux.
Objectif :
- Créer de l’information supplémentaire
- Améliorer potentiellement les performances du modèle
Exemples de création
Domaine médical :
# Body Mass Index
df['BMI'] = df['weight'] / (df['height'] ** 2)
Domaine temporel :
# Délai entre événements
df['lag_time'] = df['delivered_date'] - df['dispatch_date']
# Extraction de composantes
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day_of_week'] = df['date'].dt.dayofweek
# Catégorisation
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
Domaine chimique :
# Descripteurs moléculaires
from rdkit import Chem
from rdkit.Chem import Descriptors
def compute_descriptors(smiles):
mol = Chem.MolFromSmiles(smiles)
return {
'MW': Descriptors.MolWt(mol),
'LogP': Descriptors.MolLogP(mol),
'HBD': Descriptors.NumHDonors(mol),
'HBA': Descriptors.NumHAcceptors(mol),
'TPSA': Descriptors.TPSA(mol),
'RotBonds': Descriptors.NumRotatableBonds(mol)
}
# Appliquer
df_desc = df['SMILES'].apply(compute_descriptors)
Catégories de feature engineering
| Catégorie | Techniques |
|---|---|
| Transformation | Log, sqrt, polynomial |
| Agrégation | Sum, mean, max par groupe |
| Interaction | Produits, ratios entre features |
| Extraction temporelle | Year, month, day, hour |
| Binning | Discrétisation |
| Encodage | Target encoding, frequency encoding |
| Domaine-spécifique | BMI, descripteurs moléculaires |
📚 sklearn.preprocessing - Plus d’outils
Sélection de features
La sélection de features est le processus d’élimination des features “non-informatives”.
Deux types principaux :
| Type | Focus | Méthode |
|---|---|---|
| Univariée | Corrélation individuelle avec target | Feature Correlation |
| Multivariée | Interactions entre features | Multicollinearity, Permutation |
Pourquoi sélectionner ?
Garbage In → Garbage Out
Des features de mauvaise qualité produisent :
- Bruit dans l’apprentissage
- Instabilité du modèle
- Sortie inutilisable
Malédiction de la dimensionnalité
Ne pas observer assez de données pour supporter une relation significative.

🔗 Source

Observation : À mesure que le nombre de dimensions augmente, la quantité de données nécessaire croît exponentiellement : $5^1, 5^2, 5^3, …, 5^n$.
| Dimensions | Points nécessaires (densité 5 par dim) |
|---|---|
| 1D | 5 |
| 2D | 25 |
| 3D | 125 |
| 10D | 9,765,625 |
🚨 Attention au OneHotEncoder : Une feature catégorielle avec beaucoup de catégories génère beaucoup de colonnes !
Exemple :
- Feature “Ville” avec 1000 villes → 1000 colonnes binaires
- Nécessite beaucoup plus de données pour généraliser
Réduction de complexité
Avantages d’un modèle simple :
| Bénéfice | Description |
|---|---|
| Interprétabilité | Plus facile à expliquer |
| Rapidité | Entraînement plus rapide |
| Déploiement | Plus facile à maintenir en production |
| Loi de Pareto | 20% des features → 80% de la performance |
Feature Correlation
Supprimer une des deux features fortement corrélées entre elles.
Justification : Corrélation élevée entre A et B → Information redondante
Matrice de corrélation (Pearson)
import seaborn as sns
# Calculer la matrice de corrélation
correlation_matrix = data.select_dtypes('number').corr()
# Heatmap
sns.heatmap(
correlation_matrix,
xticklabels=correlation_matrix.columns,
yticklabels=correlation_matrix.columns,
cmap="bwr", # Blue-White-Red
center=0,
vmin=-1,
vmax=1
)
plt.title('Matrice de corrélation')
plt.show()
Analyse détaillée des corrélations
# Convertir en DataFrame
corr_df = correlation_matrix.stack().reset_index()
corr_df.columns = ['feature_1', 'feature_2', 'correlation']
# Supprimer les auto-corrélations (diagonale)
no_self_corr = (corr_df['feature_1'] != corr_df['feature_2'])
corr_df = corr_df[no_self_corr]
# Valeur absolue
corr_df['absolute_correlation'] = np.abs(corr_df['correlation'])
# Top 10 paires les plus corrélées
top_corr = corr_df.sort_values(
by="absolute_correlation",
ascending=False
).head(10)
print(top_corr)
| feature_1 | feature_2 | correlation | absolute_correlation | |
|---|---|---|---|---|
| 54 | SalePrice | Pesos | 0.990 | 0.990 |
| 45 | Pesos | SalePrice | 0.990 | 0.990 |
| 76 | Alley_NoAlley | Alley_Grvl | -0.735 | 0.735 |
| 67 | Alley_Grvl | Alley_NoAlley | -0.735 | 0.735 |
| 50 | SalePrice | GrLivArea | 0.726 | 0.726 |
Observation critique : Pesos et SalePrice corrélés à 0.99 !
Data Leakage : Pesos
Analyse :
# Vérifier le taux de change
print(f"Ratio moyen : {(data['Pesos'] / data['SalePrice']).mean():.1f}")
Ratio moyen : 20.0
Explication : Pesos = 20 × SalePrice (taux de change USD → MXN)
🚨 Data Leakage : Pesos contient directement l’information de SalePrice !
Solution : Supprimer Pesos
data = data.drop(columns=['Pesos'])
Modélisation
Maintenant que les données sont préparées, entraînons un modèle de classification.
data.head(3)
| GrLivArea | BedroomAbvGr | … | Street_Pave | SalePriceBinary | |
|---|---|---|---|---|---|
| 0 | 0.380 | 3 | … | 1.0 | expensive |
| 1 | -0.312 | 3 | … | 1.0 | expensive |
| 2 | 0.498 | 3 | … | 1.0 | expensive |
Préparation :
from sklearn.preprocessing import LabelEncoder, MinMaxScaler
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
# Encoder la target
target_encoder = LabelEncoder()
y = target_encoder.fit_transform(data['SalePriceBinary'])
# Features
X = data.drop(columns=['SalePrice', 'SalePriceBinary'])
# Scaler les features numériques (sauf GrLivArea déjà scalée)
minmax_scaler = MinMaxScaler()
X[["BedroomAbvGr", "KitchenAbvGr", "OverallCond"]] = minmax_scaler.fit_transform(
X[["BedroomAbvGr", "KitchenAbvGr", "OverallCond"]]
)
# Modèle
log_reg = LogisticRegression(max_iter=1000)
# Cross-validation
scores = cross_val_score(log_reg, X, y, cv=10)
print(f"Accuracy moyenne : {scores.mean():.3f}")
Accuracy moyenne : 0.831
Interprétation : ~83% de précision → Semble bon… mais on a encore du Data Leakage !
On a scalé avant le split train/test dans la cross-validation, donc le scaler a “vu” les données du test set → Information leakée
Schéma du problème :

Pourquoi c’est grave ?
- Scaler calcule min/max ou mean/std sur toutes les données
- Ces statistiques incluent les données de test
- Le modèle a indirectement “vu” le test set
- Performance surestimée
Mauvaise approche : Fit le scaler deux fois

Problème : Statistiques différentes → Échelles incompatibles
Bonne approche :

Solutions :
Option 1 : K-Fold manuelle avec scaling dans la boucle
from sklearn.model_selection import KFold
kf = KFold(n_splits=5)
scores = []
for train_idx, val_idx in kf.split(X):
# Split
X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
y_train, y_val = y[train_idx], y[val_idx]
# Scaler sur train uniquement
scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_val_scaled = scaler.transform(X_val) # Même scaler !
# Entraîner et évaluer
model = LogisticRegression(max_iter=1000)
model.fit(X_train_scaled, y_train)
score = model.score(X_val_scaled, y_val)
scores.append(score)
print(f"Accuracy moyenne : {np.mean(scores):.3f}")
Option 2 : Pipelines (recommandé)
→ Voir la partie suivante [[Machine Learning#Workflow]]
Feature Permutation
La permutation de features évalue l’importance de chaque feature en mesurant la dégradation de performance quand on mélange ses valeurs.
Workflow :
- Entraîner le modèle avec toutes les features
- Évaluer et enregistrer le score de base
- Pour chaque feature :
- Mélanger aléatoirement cette feature dans le test set
- Évaluer le nouveau score
- Calculer la baisse de score
- Features avec grande baisse = importantes
Si mélanger une feature dégrade beaucoup le score, c’est qu’elle est cruciale !
Implémentation
Score de référence :
from sklearn.model_selection import cross_val_score
# Modèle baseline
log_model = LogisticRegression(max_iter=1000)
# Score de référence (cross-validation)
base_score = np.mean(cross_val_score(log_model, X, y, cv=5))
print(f"Score de référence : {base_score:.3f}")
Score de référence : 0.830
Permutation :
from sklearn.inspection import permutation_importance
# Entraîner le modèle
log_model = LogisticRegression(max_iter=1000)
log_model.fit(X, y)
# Permutation (répétée 10 fois pour robustesse)
permutation_score = permutation_importance(
log_model, X, y,
n_repeats=10,
random_state=42
)
# Créer DataFrame des résultats
importance_df = pd.DataFrame({
'feature': X.columns,
'score_decrease': permutation_score.importances_mean
})
# Trier par importance
importance_df = importance_df.sort_values(
by="score_decrease",
ascending=False
)
print(importance_df)
| feature | score_decrease | |
|---|---|---|
| 0 | GrLivArea | 0.300 |
| 1 | BedroomAbvGr | 0.026 |
| 2 | KitchenAbvGr | 0.013 |
| 5 | Alley_NoAlley | 0.010 |
| 4 | Alley_Grvl | 0.005 |
| 3 | OverallCond | 0.004 |
| 7 | Street_Pave | 0.001 |
| 6 | Alley_Pave | -0.001 |
Interprétation :
GrLivArea: Feature la plus importante (baisse de 30% quand mélangée)Alley_Pave: Négligeable (score_decrease négatif = bruit)
Modèle simplifié
# Sélectionner les 2 features les plus importantes
strongest_features = X[["GrLivArea", "BedroomAbvGr"]]
# Ré-entraîner
log_reg_simple = LogisticRegression(max_iter=1000)
simple_score = np.mean(cross_val_score(log_reg_simple, strongest_features, y, cv=10))
print(f"Score avec 2 features : {simple_score:.3f}")
print(f"Score avec toutes features : {base_score:.3f}")
print(f"Différence : {base_score - simple_score:.3f}")
Score avec 2 features : 0.814 Score avec toutes features : 0.830 Différence : 0.016
Analyse :
- Avec 2 features au lieu de 8 → Perte de seulement 1.6%
- Modèle 4x plus simple !
Récapitulatif
| Étape | Actions | Outils |
|---|---|---|
| 1. Doublons | Détecter et supprimer | drop_duplicates() |
| 2. Valeurs manquantes | Analyser cause, imputer ou supprimer | SimpleImputer, dropna() |
| 3. Outliers | Visualiser, comprendre, traiter | Boxplot, masque booléen |
| 4. Scaling | Choisir scaler approprié | StandardScaler, MinMaxScaler, RobustScaler |
| 5. Balancing | Évaluer déséquilibre, SMOTE si besoin | SMOTE |
| 6. Encoding | OneHot pour nominal, Ordinal pour ordinal | OneHotEncoder, OrdinalEncoder |
| 7. Feature Creation | Créer features domaine-spécifiques | Pandas, domain knowledge |
| 8. Feature Selection | Corrélation, permutation | corr(), permutation_importance |
Règles d’or
🚨 Règle 1 : Appliquer les mêmes transformations au train et test set
# ✅ BON
transformer = Transformer()
transformer.fit(X_train)
X_train_transformed = transformer.transform(X_train)
X_test_transformed = transformer.transform(X_test)
# ❌ MAUVAIS
transformer.fit(X_train)
X_train_transformed = transformer.transform(X_train)
transformer.fit(X_test) # ⚠️ Re-fit !
X_test_transformed = transformer.transform(X_test)
🚨 Règle 2 : Ne jamais fit les transformateurs sur le test set !
🚨 Règle 3 : Attention aux biais humains introduits pendant le preprocessing
Exemples de biais :
- Choix subjectifs de seuils
- Suppression discriminatoire de données
- Encodage qui favorise certains groupes
- Balancing qui distord la réalité
Principe : Les décisions de preprocessing peuvent être nuisibles et non-inclusives même sans mauvaise intention.
Principaux modèles
{insérer ici les modèles de regression et KNN}
Modèles de régressions
[[Statistiques et probabilités#Régression simple|Régression linéaire]]
→ Optimiser la pente $a$ et l’intercept $b$ en minimisant les résidus entre $y$ réel et $\hat{y}$ prédit.

Méthode d’optimisation : Moindres carrés \(\min_{a,b} \sum_{i=1}^{n} (y_i - (ax_i + b))^2\)
[[Statistiques et probabilités#Régression Logistique|Régression logistique]]
| ![[Pasted image 20251105093639.png | 500]] |
Sortie : Probabilité entre 0 et 1
- Si $P > 0.5$ → Classe 1
- Si $P \leq 0.5$ → Classe 0
K-Nearest Neighbors (KNN)
Propriétés :
- Non-paramétrique : Pas d’hypothèse sur la distribution des données
- Lazy learning : Pas d’entraînement, calcul à la prédiction
- Basé sur les distances : Euclidienne, Manhattan, Minkowski
Fonctionnement
Pour la classification
Processus :
- Calculer la distance entre le point à prédire et tous les points d’entraînement
- Sélectionner les K plus proches voisins
- Vote majoritaire : La classe la plus fréquente parmi les K voisins
Pour la régression
Processus : 1 et 2. Identique à la classification
- Moyenne : Calculer la moyenne des valeurs y des K voisins
Choix de K
La valeur optimale de K varie selon le dataset.
Trade-off :
| K | Avantages | Inconvénients |
|---|---|---|
| Petit (1-3) | Capture patterns locaux | Sensible au bruit, overfitting |
| Grand (>10) | Lisse les prédictions | Signal dilué, underfitting |
Règle empirique : $K = \sqrt{n}$ où n = nombre d’échantillons
Note : K impair évite les égalités en classification binaire
Implémentation sklearn
Classes disponibles :
| Classe | Usage |
|---|---|
KNeighborsClassifier |
Classification |
KNeighborsRegressor |
Régression |
NearestNeighbors |
Clustering, similarité |
KNNImputer |
Imputation de valeurs manquantes |
from sklearn.neighbors import KNeighborsClassifier
# Instancier
knn = KNeighborsClassifier(n_neighbors=5)
# Entraîner (mémoriser les données)
knn.fit(X_train, y_train)
# Prédire
predictions = knn.predict(X_test)
Paramètres importants :
n_neighbors: Nombre de voisins Kmetric: Distance (‘euclidean’, ‘manhattan’, ‘minkowski’)weights: ‘uniform’ ou ‘distance’ (pondération par distance)
Métriques de performance
3.4. Metrics and scoring: quantifying the quality of predictions — scikit-learn 1.7.2 documentation
Pourquoi plusieurs métriques ?
Principe fondamental : Une seule métrique ne raconte qu’une partie de l’histoire. Il est essentiel d’investiguer l’efficacité d’un modèle sous plusieurs angles.
Raisons :
- Perspectives différentes : Chaque métrique capture un aspect spécifique
- Contexte business : Différents problèmes nécessitent différentes priorités
- Trade-offs : Optimiser une métrique peut dégrader une autre
- Robustesse : Une vue d’ensemble évite les conclusions hâtives
Cycle de vie d’un modèle
Étapes du cycle
Workflow standard :
1. Définir le problème
↓
2. Collecter les données
↓
3. Explorer et nettoyer (EDA)
↓
4. Preprocessing
↓
5. Baseline (modèle simple)
↓
6. Modélisation itérative
↓
7. Évaluation (métriques)
↓
8. Analyse d'erreurs
↓
9. Amélioration (retour à 4 ou 6)
↓
10. Déploiement
↓
11. Monitoring
Importance de l’évaluation
Comment savoir si l’idée actuelle est meilleure que la précédente ? → Via des métriques d’évaluation objectives et reproductibles.
Propriété : Les métriques permettent de :
- Comparer différents modèles
- Suivre les progrès
- Identifier les faiblesses
- Justifier les décisions
Attention, une métrique ne reflète qu’une partie des résultats ! Il faut regarder l’ensemble des métriques du modèle pour évaluer sa performance.
Baseline Score
Le but est d’établir un seuil minimal que tout modèle sophistiqué doit surpasser: Si un modèle complexe ne bat pas le baseline, c’est qu’il y a un problème.
| Type | Stratégie baseline |
|---|---|
| Classification | Prédire la classe la plus fréquente ou aléatoire |
| Régression | Prédire la moyenne ou la médiane de la target |
Avantages :
- Identification rapide d’obstacles
- Validation du pipeline (avant modèle complexe)
- Point de référence objectif
- Détection de problèmes (data leakage, etc.)
→ Un R² proche de 0 (ou négatif) indique que le baseline n’a aucun pouvoir prédictif.
Règle : Toujours commencer par un baseline avant d’investir dans des modèles sophistiqués.
DummyRegressor
from sklearn.dummy import DummyRegressor
# Baseline : Prédire toujours la moyenne
baseline = DummyRegressor(strategy="mean")
baseline.fit(X_train, y_train)
# Score (R²)
baseline_score = baseline.score(X_test, y_test)
print(f"Baseline R² : {baseline_score:.3f}")
Stratégies disponibles :
'mean': Moyenne de y_train (défaut)'median': Médiane de y_train'constant': Valeur fixe (à spécifier)'quantile': Quantile spécifique
DummyClassifier
from sklearn.dummy import DummyClassifier
# Baseline : Prédire la classe majoritaire
baseline = DummyClassifier(strategy="most_frequent")
baseline.fit(X_train, y_train)
# Accuracy
baseline_score = baseline.score(X_test, y_test)
Stratégies :
'most_frequent': Classe la plus fréquente'stratified': Respecte distribution des classes'uniform': Aléatoire uniforme'constant': Classe fixe
Métriques de régression
En régression, on prédit des valeurs continues. Les métriques mesurent l’écart entre prédictions et vraies valeurs.
- $y_i$ : Vraie valeur
- $\hat{y}_i$ : Valeur prédite
- $n$ : Nombre d’échantillons
- $\bar{y}$ : Moyenne des vraies valeurs
Coefficient de détermination (R²)
Quand utiliser R² :
| Contexte | Justification |
|---|---|
| Comparaison générale | Sans unité |
| Goodness-of-fit | Variance expliquée |
| Datasets différents | Normalisé |
Propriétés :
- Sans unité (entre 0 et 1 typiquement)
- Comparable entre datasets différents
- 1 = parfait, 0 = aussi bon que la moyenne
- Peut être négatif si pire que baseline
| R² | Signification |
|---|---|
| 1.0 | Prédictions parfaites |
| 0.8 | 80% de variance expliquée |
| 0.0 | Aussi bon que prédire la moyenne |
| < 0 | Pire que prédire la moyenne ! |
from sklearn.metrics import r2_score
r2 = r2_score(y_true, y_pred)
print(f"R² : {r2:.3f}")
Mean Squared Error (MSE)
Propriétés :
- Sensible aux outliers (erreurs au carré)
- Pénalise fortement les grandes erreurs
- Pas dans la même unité que la target
- Pas de sens de direction (positif/négatif)
- Différentiable partout (optimisation)
Quand utiliser MSE :
| Contexte | Justification |
|---|---|
| Grandes erreurs coûteuses | Pénalisation quadratique |
| Optimisation | Dérivable facilement |
| Comparaison modèles | Sensibilité aux outliers |
Exemple : Essais cliniques où une erreur de 4mg est beaucoup plus grave qu’une erreur de 2mg (pas juste 2× pire, mais 4× pire).
from sklearn.metrics import mean_squared_error
mse = mean_squared_error(y_true, y_pred)
print(f"MSE : {mse:.2f}")
Root Mean Squared Error (RMSE)
Propriétés :
- Même unité que la target (interprétable)
- Toujours sensible aux outliers
- Intuitive : Erreur “typique” en unités originales
import math
from sklearn.metrics import root_mean_squared_error
mse = mean_squared_error(y_true, y_pred)
rmse = math.sqrt(mse)
# Ou directement :
rmse = root_mean_squared_error(y_true, y_pred)
Mean Absolute Error (MAE)
Quand utiliser MAE :
| Contexte | Justification |
|---|---|
| Erreurs proportionnelles | Toutes les erreurs comptent également |
| Outliers légitimes | Ne pas sur-pénaliser |
| Interprétabilité | Facile à expliquer |
Propriétés :
- Moins sensible aux outliers (linéaire)
- Même unité que la target
- Interprétation simple : Erreur moyenne
- Non différentiable en 0 (optimisation moins lisse)
from sklearn.metrics import mean_absolute_error
mae = mean_absolute_error(y_true, y_pred)
print(f"MAE : {mae:.2f}")
Max Error
Propriétés :
- Identifie le pire cas
- Très sensible à un seul outlier
- Critique pour contraintes strictes
from sklearn.metrics import max_error
me = max_error(y_true, y_pred)
print(f"Max Error : {me:.2f}")
Tableau comparatif
| Métrique | Sensibilité outliers | Unité | Interprétabilité | Usage principal |
|---|---|---|---|---|
| R² | Modérée | Sans | Bonne | Comparaison générale |
| MSE | Très élevée | Target² | Faible | Optimisation, pénaliser grandes erreurs |
| RMSE | Élevée | Target | Bonne | Comme MSE mais interprétable |
| MAE | Faible | Target | Excellente | Toutes erreurs égales |
| Max Error | Extrême | Target | Bonne | Contraintes strictes |
Métriques de classification
En classification, on prédit des catégories discrètes. L’évaluation se base sur la matrice de confusion.
Matrice de confusion
Structure (classification binaire) :
Prédictions
Négatif Positif
Vraies Nég. TN FP
Classes Pos. FN TP
Implémentation :
from sklearn.metrics import confusion_matrix
import pandas as pd
# Calculer
cm = confusion_matrix(y_true, y_pred)
# Avec pandas (plus lisible)
cm_df = pd.crosstab(
y_true,
y_pred,
rownames=['Actual'],
colnames=['Predicted']
)
Accuracy
Propriétés :
- Simple et intuitive
- Trompeuse pour classes déséquilibrées
- Bonne si toutes les classes ont même importance
Quand utiliser Accuracy :
| Critère | Condition |
|---|---|
| Classes équilibrées | Ratio proche 50/50 |
| Égale importance | Pas de classe prioritaire |
| Vue globale | Performance générale |
from sklearn.metrics import accuracy_score
acc = accuracy_score(y_true, y_pred)
print(f"Accuracy : {acc:.3f}")
Recall
Propriétés :
- Mesure la couverture de la classe positive
- N’indique pas le nombre de fausses alertes
- Crucial quand manquer un positif est grave
Interprétation :
| Recall | Signification |
|---|---|
| 1.0 | Tous les positifs détectés (0 FN) |
| 0.8 | 80% des positifs détectés |
| 0.0 | Aucun positif détecté |
Quand utiliser Recall :
| Contexte | Justification |
|---|---|
| Détection de fraude | Ne pas manquer de fraudeur |
| Dépistage médical | Ne pas manquer de malade |
| Spam | Ne pas manquer de spam (même si FP) |
Problème potentiel : Un modèle qui prédit toujours positif a Recall = 1.0 mais est inutile !
from sklearn.metrics import recall_score
recall = recall_score(y_true, y_pred)
print(f"Recall : {recall:.3f}")
Precision
Propriétés :
- Mesure la fiabilité des prédictions positives
- N’indique pas combien de positifs sont manqués
- Crucial quand faux positif est coûteux
Interprétation :
| Precision | Signification |
|---|---|
| 1.0 | Toutes les prédictions positives sont vraies (0 FP) |
| 0.8 | 80% des prédictions positives sont vraies |
| 0.0 | Aucune prédiction positive n’est vraie |
Quand utiliser Precision :
| Contexte | Justification |
|---|---|
| Publicité ciblée | Éviter de déranger clients non-intéressés |
| Sécurité alimentaire | Être sûr qu’un médicament est sûr |
| Alertes critiques | Éviter les fausses alarmes |
from sklearn.metrics import precision_score
precision = precision_score(y_true, y_pred)
print(f"Precision : {precision:.3f}")
Precision-Recall Trade-off
Il y a une relation inverse entre Precision et Recall.
- ↑ Seuil de classification → ↑ Precision, ↓ Recall
- ↓ Seuil de classification → ↓ Precision, ↑ Recall
Seuil = 0.9 (strict)
→ Peu de prédictions positives
→ Haute Precision (celles prédites sont fiables)
→ Basse Recall (beaucoup de positifs manqués)
Seuil = 0.3 (lâche)
→ Beaucoup de prédictions positives
→ Basse Precision (beaucoup de faux positifs)
→ Haute Recall (peu de positifs manqués)
F1 Score
Propriétés :
- Équilibre entre Precision et Recall
- Sensible à la plus basse des deux métriques
- Métrique unique pour comparaisons
- Peut masquer le détail du trade-off
Quand utiliser F1 :
| Contexte | Justification |
|---|---|
| Compromis | Ni precision ni recall ne domine |
| Comparaison générale | Métrique unique |
| Classes déséquilibrées | Mieux que accuracy |
from sklearn.metrics import f1_score
f1 = f1_score(y_true, y_pred)
print(f"F1 Score : {f1:.3f}")
ROC Curve (Receiver Operating Characteristic)
Propriétés :
- Indépendant du seuil (vue d’ensemble)
- Robuste au déséquilibre de classes
- Visualise tous les trade-offs possibles
Une courbe idéale monte rapidement vers (0, 1) = coin supérieur gauche
from sklearn.metrics import roc_curve
import matplotlib.pyplot as plt
# Calculer courbe
fpr, tpr, thresholds = roc_curve(y_true, y_proba)
# Tracer
plt.plot(fpr, tpr)
plt.plot([0, 1], [0, 1], 'k--') # Diagonale (aléatoire)
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate (Recall)')
plt.title('ROC Curve')
plt.show()
AUC (Area Under Curve)
Interprétation :
| AUC | Performance |
|---|---|
| 1.0 | Parfait (sépare parfaitement les classes) |
| 0.9-1.0 | Excellent |
| 0.8-0.9 | Bon |
| 0.7-0.8 | Acceptable |
| 0.5-0.7 | Faible |
| 0.5 | Aléatoire (diagonale) |
| < 0.5 | Pire que l’aléatoire ! |
Propriétés :
- Métrique unique pour performance globale
- Indépendant du seuil choisi
- Comparable entre modèles
- Moins informatif que courbe complète
Quand utiliser AUC :
| Contexte | Justification |
|---|---|
| Comparaison modèles | Métrique unique robuste |
| Seuil non fixé | Performance tous seuils |
| Classes déséquilibrées | Robuste |
from sklearn.metrics import roc_auc_score
auc = roc_auc_score(y_true, y_proba)
print(f"AUC : {auc:.3f}")
Tableau récapitulatif des métriques de classification
| Métrique | Formule | Correspondance | Quand utiliser | Limitations |
|---|---|---|---|---|
| Accuracy | $\frac{TP+TN}{Total}$ | Proportion de prédictions correctes. | Classes équilibrées, égale importance | Trompeuse si déséquilibre |
| Recall | $\frac{TP}{TP+FN}$ | Parmi les vrais positifs, combien ont été détectés ? | Ne pas manquer de positifs (cancer, fraude) | Ignore les faux positifs |
| Precision | $\frac{TP}{TP+FP}$ | Parmi les prédictions positives, combien sont correctes ? | Éviter fausses alertes (spam, pub) | Ignore les faux négatifs |
| F1 Score | $2 \cdot \frac{P \times R}{P + R}$ | Moyenne harmonique | Équilibre Precision/Recall | Masque détails |
| ROC-AUC | Aire sous courbe ROC | Performance générale, comparaison | Moins informatif que courbe |
Classification Report
classification_report fournit toutes les métriques par classe.
from sklearn.metrics import classification_report
report = classification_report(y_true, y_pred)
print(report)
Output exemple :
precision recall f1-score support
0 0.92 0.88 0.90 100
1 0.85 0.90 0.87 80
accuracy 0.89 180
macro avg 0.89 0.89 0.89 180
weighted avg 0.89 0.89 0.89 180
Interprétation :
- support : Nombre d’échantillons par classe
- macro avg : Moyenne simple (toutes classes égales)
- weighted avg : Moyenne pondérée par support
Cross-validation avec métriques
cross_validate accepte plusieurs métriques simultanément.
from sklearn.model_selection import cross_validate
from sklearn.linear_model import LinearRegression
model = LinearRegression()
# Spécifier plusieurs métriques
cv_results = cross_validate(
model, X, y,
cv=5,
scoring=['r2', 'neg_mean_absolute_error', 'neg_mean_squared_error', 'max_error']
)
# Extraire résultats
print(f"R² moyen : {cv_results['test_r2'].mean():.3f}")
print(f"MAE moyen : {-cv_results['test_neg_mean_absolute_error'].mean():.2f}")
Note : sklearn retourne les erreurs en négatif (convention : plus haut = meilleur).
Analyse d’erreurs
L’analyse d’erreurs est un processus itératif d’identification de patterns dans les erreurs du modèle. Le but est de comprendre pourquoi le modèle se trompe pour l’améliorer.
Questions à se poser
Analyse par cohorte :
- Certains sous-groupes de X performent-ils mieux/pire ?
- Y a-t-il des patterns démographiques dans les erreurs ?
Analyse par classe :
- Une classe est-elle mieux identifiée que les autres ?
- Confusion systématique entre deux classes spécifiques ?
Analyse par magnitude :
- Certaines erreurs sont-elles extrêmement grandes ?
- Y a-t-il des outliers qui tirent la performance vers le bas ?
Techniques d’analyse
Visualisation des erreurs
import matplotlib.pyplot as plt
# Régression : Résidus
residuals = y_true - y_pred
plt.scatter(y_pred, residuals)
plt.axhline(y=0, color='r', linestyle='--')
plt.xlabel('Prédictions')
plt.ylabel('Résidus')
plt.title('Analyse des résidus')
plt.show()
Matrice de confusion détaillée
from sklearn.metrics import ConfusionMatrixDisplay
# Affichage interactif
disp = ConfusionMatrixDisplay.from_predictions(y_true, y_pred)
plt.title('Matrice de confusion')
plt.show()
Analyse par feature
# Identifier features où modèle échoue
errors = (y_true != y_pred)
X_errors = X[errors]
# Analyser distribution des features pour erreurs
X_errors.describe()
Actions à entreprendre
Résultats de l’analyse → Actions :
| Observation | Action possible |
|---|---|
| Cohorte sous-performe | Collecter plus de données pour ce groupe |
| Features corrélées aux erreurs | Feature engineering, transformations |
| Classe confondue | Features distinctives, rééquilibrage |
| Outliers problématiques | Robustesse, traitement outliers |
| Pattern systématique | Biais du modèle, choix algorithme |
Processus itératif
Modèle initial
↓
Évaluation (métriques)
↓
Analyse d'erreurs
↓
Hypothèses d'amélioration
↓
Modifications (data/features/modèle)
↓
Retour à Évaluation
Principe : L’analyse d’erreurs est un cycle continu jusqu’à performance satisfaisante.
Conclusion
On peut se demander si la maîtrise des métriques de performance ne constitue pas la différence entre un data scientist qui applique des recettes et celui qui optimise vraiment ses modèles pour l’objectif business ?
Récapitulatif des concepts clés
Baseline :
- Point de référence essentiel
- DummyRegressor, DummyClassifier
- Validation du pipeline
Métriques de régression :
- MSE/RMSE : Pénalisation quadratique
- MAE : Linéaire, robuste
- R² : Variance expliquée
- Max Error : Contraintes strictes
Métriques de classification :
- Accuracy : Vue globale (attention déséquilibre)
- Precision : Fiabilité prédictions positives
- Recall : Couverture classe positive
- F1 : Équilibre harmonique
- ROC-AUC : Performance générale
Analyse d’erreurs :
- Processus itératif
- Identification de patterns
- Guide pour amélioration
Guide de sélection des métriques
Pour la régression :
| Priorité | Métrique recommandée |
|---|---|
| Pénaliser grandes erreurs | MSE ou RMSE |
| Interprétabilité | MAE ou RMSE |
| Comparaison générale | R² |
| Contraintes strictes | Max Error |
Pour la classification :
| Priorité | Métrique recommandée |
|---|---|
| Ne pas manquer positifs | Recall (+ seuil bas) |
| Éviter fausses alertes | Precision (+ seuil haut) |
| Équilibre | F1 Score |
| Vue d’ensemble | ROC-AUC |
| Classes équilibrées | Accuracy |
Principes généraux
- Toujours commencer par un baseline
- Utiliser plusieurs métriques (vue complète)
- Aligner métriques avec objectif business
- Analyser les erreurs (pas juste le score global)
- Itérer basé sur l’analyse
Ressources complémentaires
Under the Hood
Que se passe-t-il derrière .fit() ?
1.1 Contexte : entraînement d’un modèle
Considérons un ensemble de données simple :
| masse_moléculaire | solubilité |
|---|---|
| 150.2 | 2.5 |
| 340.8 | 1.8 |
| 520.5 | 3.2 |
from sklearn.linear_model import LinearRegression
# Instancier le modèle linéaire
model = LinearRegression()
# Entraîner le modèle
model.fit(data[['masse_moléculaire']], data['solubilité'])
# Accéder aux paramètres optimaux
print('β₀ (intercept) =', model.intercept_)
print('β₁ (slope) =', model.coef_[0])
1.2 La fonction hypothèse
1.3 Le rôle de .fit()
1.4 La fonction de perte L
Notation compacte :
\[\underset{\beta}{\text{argmin}} , L(\beta)\]1.5 Les méthodes de résolution (solvers)
Il existe plusieurs approches pour minimiser $L(\beta)$ :
| Méthode | Description | Exemple d’usage |
|---|---|---|
| Résolution exacte | Inversion matricielle (coûteuse) | SVD en régression linéaire |
| Approches itératives | Descente de gradient et variantes | SGD, Adam, Newton |
# Dans sklearn, les méthodes sont appelées "solvers"
from sklearn.linear_model import LogisticRegression
model = LogisticRegression(solver='newton-cg')
1.6 Intuition : optimisation 1D
Scénario simplifié : Imaginons que nous connaissions déjà la pente optimale ($\beta_1$) et cherchions uniquement l’ordonnée à l’origine ($\beta_0$).
Procédure :
- Initialiser aléatoirement $\beta_0$ (par exemple à 0)
- Calculer la perte (SSR - Sum of Squared Residuals)
- Modifier $\beta_0$ et répéter jusqu’à trouver le minimum
Observation : La fonction de perte a une forme convexe.
Problèmes potentiels :
- Pas trop grands → on peut manquer le minimum exact
- Point de départ arbitraire → convergence non garantie
- En réalité, on doit ajuster $\beta_0$ et $\beta_1$ simultanément
On peut se demander si une méthode itérative plus sophistiquée pourrait résoudre ces problèmes ? C’est là qu’intervient la descente de gradient.
2. Descente de gradient (Gradient Descent)
2.1 Principe fondamental
Définition mathématique :
La pente est égale à la dérivée partielle de la fonction de perte par rapport au paramètre d’intérêt :
\[\frac{\partial L}{\partial \beta_0}(\beta_0)\]2.2 Descente 1D : pas à pas
Étape 1 : Initialisation et calcul du gradient
# Initialiser un paramètre aléatoire
β₀ = 0
# Calculer la dérivée de la fonction de perte en ce point
∂L/∂β₀ (β₀)
Étape 2 : Mise à jour du paramètre
Caractéristique importante : Plus on approche du minimum, plus la dérivée diminue, et donc plus les pas deviennent petits. Cela rend la descente de gradient efficace computationnellement.
Répétition du processus
La valeur mise à jour $\beta_0^{(k+1)}$ est réinjectée dans le calcul du gradient, et le processus est répété.
Question : Quand s’arrête-t-on ?
2.3 Critères d’arrêt
| Critère | Description | Exemple |
|---|---|---|
| Taille minimale du pas | Arrêt quand le pas devient négligeable | < 0.001 |
| Nombre maximal d’itérations | Limite fixe d’epochs | max_iter=1000 |
2.4 Récapitulatif : Descente 1D
2.5 Solution analytique pour la régression linéaire
Pour la régression OLS (Ordinary Least Squares), on peut calculer explicitement le gradient :
2.6 Implémentation Python : Descente 1D
import numpy as np
# Données
X = data['masse_moléculaire']
y = data['solubilité']
# Paramètres
b1 = 0.64 # pente fixée pour cet exemple
eta = 0.1 # Learning rate
# Fonction hypothèse
def h(x, b0):
return b0 + b1 * x
# Initialisation
b0 = 0
# Epoch 0 : calcul de la perte initiale
loss_epoch0 = np.sum((y - h(X, b0)) ** 2)
print(f'Loss epoch 0: {loss_epoch0}')
# Epoch 1 : première itération
derivative = np.sum(-2 * (y - h(X, b0)))
b0_new = b0 - (eta * derivative)
print(f'b0 epoch 1: {b0_new}')
# Epoch 2 : deuxième itération
derivative = np.sum(-2 * (y - h(X, b0_new)))
b0_newer = b0_new - (eta * derivative)
print(f'b0 epoch 2: {b0_newer}')
# Répéter jusqu'à convergence...
2.7 Descente 2D : co-optimisation de β₀ et β₁
Dans un problème réaliste, on doit optimiser tous les paramètres simultanément.
Procédure itérative : On applique la même logique pour chaque paramètre, en suivant le gradient multidimensionnel.
2.8 Extension à N dimensions
Pour un problème avec $p$ features (donc $p+1$ paramètres incluant l’intercept), la fonction de perte est dans un espace à $(p+2)$ dimensions.
Exemple : Un modèle à 3 features → espace 4D (impossible à visualiser directement).
2.9 Formulation vectorielle
Interprétation géométrique : Les flèches bleues sur le paysage énergétique indiquent la direction et l’intensité du gradient. Au minimum, le gradient est nul.
2.10 Effet du taux d’apprentissage η
| Taux d’apprentissage | Avantages | Inconvénients |
|---|---|---|
| Petit η | Chemin plus direct vers le minimum | Nécessite plus d’epochs |
| Moins de risque de divergence | Peut rester bloqué dans un minimum local | |
| Grand η | Convergence rapide (moins d’epochs) | Risque de ne jamais converger (oscillations) |
2.11 Gradient analytique pour l’OLS
Remarque : Cette formule simple rend la descente de gradient très efficace pour les régressions OLS.
2.12 (Optionnel) Intuition géométrique
On peut se demander si une interprétation géométrique pourrait éclairer ce résultat ?
3. Autres solveurs
3.1 Limites de la descente de gradient classique
Question : Ne pourrait-on pas utiliser moins de $n$ observations pour calculer un gradient “approximatif” ?
3.2 Mini-Batch Gradient Descent
3.3 Stochastic Gradient Descent (SGD)
3.4 Implémentation Python : SGD
# Initialisation
b0 = 0
eta = 0.1
n_epoch = 5
for epoch in range(n_epoch):
# Boucle aléatoire sur toutes les observations
for i in np.random.permutation(len(X)):
# Mini-batch de taille 1
X_mini = X[i]
y_true = y[i]
# Calcul du gradient
y_pred = h(X_mini, b0)
derivative = -2 * (y_true - y_pred)
# Mise à jour
b0 = b0 - eta * derivative
print(f'b0 epoch {epoch}: {b0}')
Observation : En raison du bruit induit par l’échantillonnage, la perte fluctue davantage d’epoch en epoch et ne décroît pas nécessairement de manière monotone.
3.5 SGD : Avantages et inconvénients
| Aspect | Détails |
|---|---|
| Avantages | ✓ Plus rapide sur très grands datasets |
| ✓ Peut sortir des minima locaux (grâce au bruit) | |
| ✓ Réduit considérablement la charge RAM | |
| Inconvénients | ✗ Nécessite plus d’epochs |
| ✗ Ne converge jamais exactement (attention aux critères d’arrêt) | |
| ✗ Peut être plus lent sur petits datasets avec beaucoup de features |
Cas d’usage recommandés :
- Nombre d’observations > 100 000
- Besoin de sortir d’un minimum local
- Par défaut pour les réseaux de neurones (Deep Learning)
3.6 SGDRegressor et SGDClassifier dans sklearn
from sklearn.linear_model import SGDRegressor, LinearRegression
# OLS résolu par inversion matricielle
lin_reg = LinearRegression()
# OLS résolu par SGD
lin_reg_sgd = SGDRegressor(loss='squared_error')
3.7 Comparaison de performance
from sklearn.datasets import make_regression
# Créer un problème test
X, y = make_regression(n_samples=10000, n_features=1000)
# LinearRegression (inversion matricielle)
%time lin_reg.fit(X, y)
# CPU times: Wall time: 2.07 s
# SGDRegressor (descente de gradient)
%time lin_reg_sgd.fit(X, y)
# CPU times: Wall time: 189 ms
Conclusion : La descente de gradient surpasse l’inversion matricielle lorsque le nombre de features est grand. SGD est encore plus performant sur de très grands datasets.
3.8 Variantes avancées de la descente de gradient
| Méthode | Description | Caractéristique clé |
|---|---|---|
| Momentum | Ajoute de l’inertie au gradient | Accélère la convergence |
| AdaGrad | Taux d’apprentissage adaptatif par feature | Priorité aux features faiblement mises à jour |
| RMSProp | Ajout d’un facteur de décroissance | Seuls les gradients récents comptent |
| Adam | Combinaison de Momentum + RMSProp | Optimiseur par défaut en Deep Learning |
3.9 Méthodes du second ordre (Matrice Hessienne)
Usage : Problèmes ML “simples”. Solveur par défaut de LogisticRegression dans sklearn.
from sklearn.linear_model import LogisticRegression
# Utilise newton-cg par défaut
model = LogisticRegression(solver='lbfgs')
4. Fonctions de perte (Loss Functions)
4.1 Diversité des fonctions de perte
La perte quadratique (MSE) n’est pas la seule option :
from sklearn.linear_model import SGDRegressor, SGDClassifier
# Régression avec différentes pertes
SGDRegressor(loss='squared_error') # MSE
SGDRegressor(loss='huber') # Huber
# Classification avec différentes pertes
SGDClassifier(loss='log') # Log-loss (logistic)
SGDClassifier(loss='hinge') # Hinge loss (SVM)
Observation : Une même classe de modèle sklearn peut être instanciée avec différentes fonctions de perte.
Distinction : Les problèmes de classification et de régression ont des fonctions de perte différentes par nature.
4.2 Perte ≠ Métrique de performance
4.3 Fonctions de perte en régression
L1 Loss (MAE) vs L2 Loss (MSE)
| Critère | L1 (MAE) | L2 (MSE) |
|---|---|---|
| Sensibilité aux outliers | Faible (robuste) | Forte (pénalise fortement) |
| Learning rate | Nécessite un η décroissant | η constant possible |
| Gradient | Constant en magnitude | Proportionnel à l’erreur |
Huber Loss (Smooth L1 Loss)
Avantages :
- Robuste aux outliers (comme L1)
- Pente utilisable comme indicateur de convergence (comme L2)
4.4 Fonctions de perte en classification
Contexte : Classifieurs logistiques
Objectif : Prédire un vecteur binaire $y = [0, 0, 1, 0, \ldots, 1]$ de taille $n$.
Approche : On modélise par un vecteur de probabilités $\hat{y} = [0.1, 0.3, \ldots, 0.8] = h(X, \beta)$.
Question : Quelle fonction de perte utiliser entre $\hat{y}$ et $y$ ?
Maximisation de la vraisemblance (Likelihood)
Astuce : Il est plus facile de maximiser le logarithme (log-likelihood).
Log-Loss (Cross-Entropy Loss)
Origine du nom “Cross-Entropy” : Théorie de l’information de Shannon (mesure de divergence entre deux distributions).
Gradient de la Log-Loss
Pensée vectorielle : Les formules matricielles permettent d’implémenter efficacement la descente de gradient pour OLS et Logit.
Attention : Les valeurs numériques des gradients diffèrent car les fonctions hypothèses sont différentes ($h$ linéaire vs sigmoïde).
4.5 Autres classifieurs et leurs pertes
D’autres modèles de classification existent, chacun avec sa propre fonction de perte :
| Modèle | Fonction de perte | Caractéristique |
|---|---|---|
| Naive Bayes | Negative log-likelihood | Approche probabiliste bayésienne |
| Support Vector Machine (SVC) | Hinge loss | Maximisation de la marge |
| Neural Networks | Diverses (softmax cross-entropy, etc.) | Selon l’architecture |
5. Résumé
5.1 Configuration du problème
5.2 Paramètres du modèle
Calculés automatiquement durant .fit() :
- $\beta$ (coefficients du modèle)
- Optimisés en minimisant $L(\beta)$
5.3 Hyperparamètres du modèle
Choisis manuellement avant .fit() :
| Catégorie | Exemples | Rôle |
|---|---|---|
| Fonction de perte | MSE, MAE, Huber, Log-loss, Hinge | Définit ce qu’on optimise |
| Solveur | ‘newton-cg’, ‘lbfgs’, ‘sgd’ | Méthode d’optimisation |
| Paramètres du solveur | learning_rate, max_iter, tol |
Contrôle de la convergence |
| Spécificités du modèle | n_neighbors, C, gamma |
Selon le type de modèle |
5.4 Terminologie : “Modèle”
Régresseurs :
LinearRegression() # Régression OLS
KNeighborsRegressor() # KNN
SVR() # Support Vector Regressor
Classifieurs :
LogisticRegression() # Régression logistique
KNeighborsClassifier() # KNN
SVC() # Support Vector Classifier
5.5 Exception : SGDRegressor et SGDClassifier
Ces classes font référence à la méthode d’optimisation (SGD) plutôt qu’à une fonction hypothèse spécifique :
# Régression
SGDRegressor(loss='squared_error') # ≡ OLS
SGDRegressor(loss='huber') # Régression linéaire non-OLS
# Classification
SGDClassifier(loss='log') # ≡ Logistic Regression
SGDClassifier(loss='hinge') # ≡ SVC
5.6 Fine-tuning des hyperparamètres
Une fois le modèle sélectionné, on peut affiner ses hyperparamètres :
SGDRegressor(
loss='squared_error',
learning_rate='constant',
eta0=0.01,
max_iter=1000
)
KNeighborsRegressor(n_neighbors=5)
LogisticRegression(solver='newton-cg', C=1.0)
Puis entraînement via .fit().
Bibliographie
| Ressource | Type | Lien/Référence |
|---|---|---|
| StatQuest - Gradient Descent | Vidéo pédagogique | YouTube |
| Hands-On ML with Scikit-Learn | Livre (Chapitre 4) | Aurélien Géron |
| Andrew Ng - Linear Models | Notes de cours (CS229) | Stanford University |
| Derivative of log-loss function | Article technique | Diverses sources en ligne |
Model Tuning
Récapitulatif : Under the Hood
Configuration du problème
Plan du cours
- Complexité du modèle
- Régularisation
- Tuning de modèle : Grid Search & Random Search
- Support Vector Machines (Classifieurs à marge)
- Kernel Tricks
1. Complexité du modèle
1.1 Régression linéaire : limites
Rappel : La régression linéaire simple cherche une relation de la forme :
\[h_\beta(x) = \beta_0 + \beta_1 x\]Problème : Que faire si le comportement est non-linéaire ?
Solution naïve : Ajouter des features transformées :
\[h_\beta(x) = \beta_0 + \beta_1 x + \beta_2 x^2\]Le fit s’améliore, mais…
1.2 Ingénierie de features complexes
On pourrait créer des features très complexes :
\[h_\beta(x) = \beta_0 + \beta_1 x + \beta_2 x^2 + \beta_3 x^3 + \cdots + \beta_n x^n\]Observation : Le $R^2$ continue d’augmenter avec chaque feature additionnelle !
Illusion : Aurions-nous résolu la Data Science ?
def train_best_model_ever(X, y):
while True:
model = LinearRegression()
model.fit(X, y)
if calculate_r2(model, X, y) < 0.999999:
X = add_more_crazy_features(X)
else:
return model
1.3 Overfitting et Underfitting
Visualisation :
- Modèle 1 (trop simple) : underfitting
- Modèle 2 (équilibré) : optimal
- Modèle 3 (trop complexe) : overfitting
1.4 Le compromis Biais-Variance
La complexité optimale minimise l’erreur sur le test set.
1.5 Trouver la complexité optimale
Méthode idéale : Tester plusieurs modèles et sélectionner celui qui minimise l’erreur sur un dataset non vu.
Méthode pragmatique : Utiliser les courbes d’apprentissage (learning curves) pour diagnostiquer au moins le modèle qu’on a !
1.6 ❗ Rappel : Data Leakage ❗
1.7 Solutions contre l’overfitting
| Solution | Description | Application |
|---|---|---|
| Collecter plus de données | Augmente le ratio données/paramètres | Synthèse chimique, screening HTS |
| Sélection de features | Manuelle ou automatisée | Éliminer descripteurs redondants |
| Réduction de dimensionnalité | PCA, t-SNE, UMAP | Unsupervised Learning |
| Early stopping | Arrêt précoce de l’entraînement | Deep Learning |
| Régularisation | Pénalité sur la fonction de perte | Ridge, Lasso, ElasticNet |
On peut se demander si la régularisation ne serait pas la solution la plus élégante et pratique ? Explorons cette approche !
2. Régularisation
2.1 Principe de la régularisation
2.2 Les deux pénalités principales
Avertissement : Lors de la régularisation Ridge ou Lasso :
- On régularise les coefficients $\beta_1, \beta_2, \ldots, \beta_p$
- On ne régularise pas l’intercept $\beta_0$ (qui n’est associé à aucune feature)
2.3 L’hyperparamètre α
2.4 Comparaison Ridge vs Lasso
from sklearn import datasets
from sklearn.linear_model import Ridge, Lasso, LinearRegression
# Dataset : prédiction de la progression du diabète
X, y = datasets.load_diabetes(return_X_y=True, as_frame=True)
# Entraîner les 3 modèles
linreg = LinearRegression().fit(X, y)
ridge = Ridge(alpha=0.2).fit(X, y)
lasso = Lasso(alpha=0.2).fit(X, y)
# Comparer les coefficients
coefs = pd.DataFrame({
"coef_linreg": pd.Series(linreg.coef_, index=X.columns),
"coef_ridge": pd.Series(ridge.coef_, index=X.columns),
"coef_lasso": pd.Series(lasso.coef_, index=X.columns)
})
| Feature | coef_linreg | coef_ridge | coef_lasso |
|---|---|---|---|
| age | -10 | 7 | 0 |
| sex | -239 | -182 | -75 |
| bmi | 519 | 457 | 511 |
| bp | 324 | 284 | 234 |
| s1 | -792 | -48 | 0 |
| s2 | 476 | -78 | 0 |
| s3 | 101 | -189 | -170 |
| s4 | 177 | 119 | 0 |
| s5 | 751 | 400 | 450 |
| s6 | 67 | 97 | 0 |
Observation : Lorsque $\alpha$ augmente :
- Ridge : tous les coefficients diminuent progressivement
- Lasso : certains coefficients atteignent exactement zéro (élimination de features)
2.5 ElasticNet : le meilleur des deux mondes
from sklearn.linear_model import ElasticNet
model = ElasticNet(alpha=1, l1_ratio=0.2)
2.6 Quelles features sont pénalisées ?
Analyse avec statsmodels :
import statsmodels.api as sm
# Test de significativité statistique
ols = sm.OLS(y, sm.add_constant(X)).fit()
print(ols.summary())
Observation : La régularisation tend à pénaliser en priorité les features qui ne sont pas statistiquement significatives (p-value élevée).
Exemple : Features avec p-value > 0.05 sont souvent éliminées par Lasso.
2.7 Conclusions sur la régularisation
| Situation | Méthode recommandée |
|---|---|
| Overfitting détecté | Appliquer régularisation (learning curves) |
| Tous les coefficients importants | Ridge (réduit l’influence sans éliminer) |
| Besoin d’interprétabilité | Lasso (sélection automatique de features) |
| Par défaut | Ridge activé dans la plupart des modèles ML |
Bonne pratique : La régularisation est presque toujours appropriée ! Il suffit d’ajuster le paramètre $\alpha$.
Important : Toujours normaliser les features avant régularisation pour pénaliser chaque feature équitablement.
3. Model Tuning : Grid Search & Random Search
3.1 Objectif du tuning
Question centrale : Comment choisir les meilleurs hyperparamètres (par exemple $\alpha$) ?
3.2 Méthode Grid Search
3.3 Implémentation manuelle
from sklearn.model_selection import train_test_split
from sklearn.linear_model import ElasticNet
from sklearn.model_selection import cross_val_score
import itertools
# Split train/test
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Définir les valeurs à tester
alphas = [0.01, 0.1, 1]
l1_ratios = [0.2, 0.5, 0.8]
# Créer toutes les combinaisons
hyperparams = itertools.product(alphas, l1_ratios)
# Tester chaque combinaison
for hyperparam in hyperparams:
alpha = hyperparam[0]
l1_ratio = hyperparam[1]
model = ElasticNet(alpha=alpha, l1_ratio=l1_ratio)
r2 = cross_val_score(model, X_train, y_train, cv=5).mean()
print(f"alpha: {alpha}, l1_ratio: {l1_ratio}, r2: {r2}")
3.4 Grid Search avec Cross-Validation
3.5 GridSearchCV de sklearn
from sklearn.model_selection import GridSearchCV
# Split train/test
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Instancier le modèle
model = ElasticNet()
# Définir la grille d'hyperparamètres
grid = {
'alpha': [0.01, 0.1, 1],
'l1_ratio': [0.2, 0.5, 0.8]
}
# Instancier Grid Search
search = GridSearchCV(
model,
grid,
scoring='r2',
cv=5,
n_jobs=-1 # paralléliser le calcul
)
# Entraîner
search.fit(X_train, y_train)
# Résultats
print('Best score:', search.best_score_)
print('Best params:', search.best_params_)
print('Best estimator:', search.best_estimator_)
Sortie :
Best score: 0.4417
Best params: {'alpha': 0.01, 'l1_ratio': 0.8}
Best estimator: ElasticNet(alpha=0.01, l1_ratio=0.8)
3.6 Limites de Grid Search
| Limitation | Description |
|---|---|
| Coût computationnel | Nombre de combinaisons = produit cartésien |
| Valeurs optimales manquées | Grille discrète peut manquer l’optimum |
| Overfitting des hyperparamètres | Si trop de combinaisons testées sur petit dataset |
3.7 Random Search
3.8 RandomizedSearchCV de sklearn
from sklearn.model_selection import RandomizedSearchCV
from scipy import stats
# Instancier le modèle
model = ElasticNet()
# Définir les distributions d'hyperparamètres
grid = {
'l1_ratio': stats.uniform(0, 1), # Uniforme [0, 1]
'alpha': [0.001, 0.01, 0.1, 1, 10]
}
# Instancier Random Search
search = RandomizedSearchCV(
model,
grid,
scoring='r2',
n_iter=100, # nombre de tirages aléatoires
cv=5,
n_jobs=-1
)
# Entraîner
search.fit(X_train, y_train)
print('Best estimator:', search.best_estimator_)
Sortie :
Best estimator: ElasticNet(alpha=0.001, l1_ratio=0.7098)
3.9 Choix des distributions de probabilité
from scipy import stats
import matplotlib.pyplot as plt
# Différentes distributions selon la connaissance a priori
dist1 = stats.norm(10, 2) # Si on a une estimation (autour de 10)
dist2 = stats.randint(1, 100) # Si on n'a aucune idée (uniforme discret)
dist3 = stats.uniform(1, 100) # Uniforme continu
dist4 = stats.loguniform(0.01, 1) # Recherche coarse-grain
# Visualiser
r = dist4.rvs(size=10000)
plt.hist(r, bins=50)
plt.xlabel('Valeur')
plt.title('Distribution loguniform')
3.10 Random Search vs Grid Search
| Critère | Random Search | Grid Search |
|---|---|---|
| Flexibilité | Moins de code, teste beaucoup de valeurs | Toutes les combinaisons spécifiées |
| Contrôle du temps | ✓ Nombre fixe d’itérations | ✗ Temps = taille grille |
| Hyperparamètres inégaux | ✓ Utile si certains plus importants | ✗ Teste tout uniformément |
Stratégie recommandée :
- Commencer par une approche coarse-grain (Grid ou Random)
- Affiner ensuite la recherche autour des meilleures valeurs trouvées
3.11 🔥 Résumé clé 🔥
4. Support Vector Machines (SVMs)
4.1 Qu’est-ce qu’une bonne frontière de décision ?
Contexte : Classification binaire avec séparabilité linéaire.
Observation : Il existe un nombre infini de frontières de décision potentielles qui séparent les classes.
Question : Laquelle généralise le mieux aux données non vues ?
4.2 Maximum Margin Classifier
4.3 Problème : sensibilité aux outliers
Limite du Maximum Margin Classifier :
- Extrêmement sensible aux outliers
- Overfitte les données d’entraînement
On peut se demander si autoriser quelques erreurs ne permettrait pas de mieux généraliser ?
4.4 Soft Margin Classifier
4.5 Hinge Loss
Questions :
- Quelle intensité pour la pénalité des points mal classés ?
- Quelle largeur pour la marge ?
Réponse : Compromis Biais-Variance → Régularisation !
4.6 Hyperparamètre C
4.7 Implémentation sklearn
from sklearn.svm import SVC
from sklearn.linear_model import SGDClassifier
# SVM linéaire avec kernel='linear'
svc = SVC(kernel='linear', C=10)
# Équivalent avec solveur SGD
svc_sgd = SGDClassifier(
loss='hinge', # Hinge loss
penalty='l2', # Régularisation L2
alpha=1/10 # alpha = 1/C
)
⚠️ Avertissement : Tous les modèles SVM nécessitent un scaling des features !
4.8 (Bonus) SVM Regressors
from sklearn.svm import SVR
regressor = SVR(epsilon=0.1, C=1, kernel='linear')
5. SVM Kernels
5.1 SVM linéaire : rappel mathématique
5.2 Problème : données non linéairement séparables
Exemple : Données circulaires en 2D.
Question : Comment séparer ces classes avec un SVM ?
Solution : Ajouter une nouvelle feature transformée !
\[Z = X^2 + Y^2\]Dans l’espace 3D $(X, Y, Z)$, les données deviennent linéairement séparables !
5.3 Feature mapping polynomial
5.4 Le Kernel Trick 🔥
5.5 Liste des kernels SVM
| Kernel | Description | Hyperparamètres |
|---|---|---|
| linear | Pas de transformation | C |
| poly | Polynomial de degré $d$ | C, degree, coef0 |
| rbf | Radial Basis Function (Gaussien) | C, gamma |
| sigmoid | Fonction sigmoïde | C, gamma, coef0 |
Rappel : C est l’intensité de la pénalité pour les mauvaises classifications.
5.6 Détails des kernels
a) Linear Kernel
b) Polynomial Kernel
Application en régression :
from sklearn.svm import SVR
regressor = SVR(epsilon=0.1, C=1, kernel='poly', degree=2)
Le kernel polynomial permet de capturer des relations non-linéaires très complexes.
c) RBF Kernel (Gaussien)
Exemple d’usage :
from sklearn.svm import SVC
# RBF avec gamma élevé (overfitting possible)
svc_overfit = SVC(kernel='rbf', C=1, gamma=10)
# RBF avec gamma faible (underfitting possible)
svc_underfit = SVC(kernel='rbf', C=1, gamma=0.01)
# Valeur par défaut : gamma='scale' ou 'auto'
svc_balanced = SVC(kernel='rbf', C=1, gamma='scale')
5.7 Recommandations pratiques
| Situation | Kernel recommandé | Justification |
|---|---|---|
| Données linéairement séparables | linear |
Simplicité, interprétabilité |
| Relations polynomiales connues | poly (degree=2 ou 3) |
Capture interactions quadratiques |
| Relations non-linéaires complexes | rbf |
Flexible, fonctionne bien par défaut |
| Très grands datasets | linear ou SGD |
Performance computationnelle |
Tuning des hyperparamètres :
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
# Grille pour RBF kernel
param_grid = {
'C': [0.1, 1, 10, 100],
'gamma': ['scale', 'auto', 0.001, 0.01, 0.1, 1],
'kernel': ['rbf']
}
svc = SVC()
search = GridSearchCV(svc, param_grid, cv=5, scoring='f1')
search.fit(X_train, y_train)
print('Best params:', search.best_params_)
Bibliographie
| Ressource | Type | Description |
|---|---|---|
| Hands-On ML with Scikit-Learn | Livre (Chapitre 5) | A. Géron - Régularisation et SVM |
| Kernels Explained | Article technique | Fondements mathématiques |
| SVM vs Logistic Regression | Article | Comparaison théorique |
# Workflow
# Méthode des ensembles
# Apprentissage non supervisé
# Séries temporelles
# Traitement du language naturel
