ML-1 (Python) : Introduction au Machine Learning avec scikit-learn

Ce notebook est le jumeau Python de ML-1-Introduction.ipynb (.NET / ML.NET). Il reprend exactement les trois exemples guidés et les trois exercices du notebook C#, en utilisant la bibliothèque canonique scikit-learn.

Pourquoi un jumeau Python ? ML.NET et scikit-learn sont les deux bibliothèques de référence pour le Machine Learning « classique » (non profond), l’une côté .NET, l’autre côté Python. Comparer les deux sur les mêmes exemples pédagogiques permet de : 1. comprendre que les concepts (pipeline, entraînement, évaluation, prédiction) sont universels — seules les API diffèrent ; 2. voir les écarts de comportement entre solveurs (par ex. SDCA en ML.NET vs OLS en scikit-learn pour la régression linéaire) ; 3. disposer d’une version exécutable sans installation .NET.

Épic #4956 — Parité .NET ⇄ Python des séries de notebooks. Convention : kernel python3, prose FR-first, 3 exercices stub, exécution réelle scikit-learn.

Quelles sont les étapes à suivre ?

Un workflow de Machine Learning comporte toujours les mêmes étapes, quel que soit le framework :

  1. Référencer la bibliothèque (import en Python, #r nuget en .NET).
  2. Préparer le contexte (graine aléatoire, affichage).
  3. Définir les structures de données (ici, de simples tableaux NumPy).
  4. Charger les données d’entraînement.
  5. Construire le pipeline (transformation + algorithme).
  6. Entraîner le modèle (fit).
  7. Évaluer sur des données de test (R², accuracy, AUC…).
  8. Prédire de nouvelles valeurs.
  • Étape 1 : Référencer les bibliothèques

En Python, on importe les modules. NumPy pour les tableaux numériques, scikit-learn (sklearn) pour les algorithmes et métriques, Matplotlib pour les visualisations.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.metrics import r2_score, mean_absolute_error, accuracy_score, roc_auc_score

print("Bibliothèques chargées : numpy,", np.__version__, "| sklearn,", __import__('sklearn').__version__)
Bibliothèques chargées : numpy, 2.4.4 | sklearn, 1.8.0

On fixe la graine (42) pour rendre l’expérience déterministe : les tirages aléatoires (s’ils surviennent) reproduiront les mêmes résultats à chaque exécution.

rng = np.random.default_rng(42)

Pourquoi fixer la graine ?

Au-delà du déterminisme : sans graine, chaque exécution donnerait des chiffres légèrement différents — impossible de comparer une correction à un résultat attendu, ou de déboguer un échec intermittent. C’est l’équivalent du MLContext(seed: 0) de ML.NET : la reproductibilité est un prérequis de l’expérimentation, pas un luxe.

  • Étape 2 : Définir les structures de données

En ML.NET on définit des classes POCO (HouseData, Prediction). En Python on travaille directement avec des tableaux NumPy : un tableau 2D X pour les features (ici la taille) et un vecteur 1D y pour les étiquettes (ici le prix). C’est la convention universelle de scikit-learn : X de forme (n_samples, n_features), y de forme (n_samples,).

Exemple guide 1 : Prédiction du prix d’une maison (régression)

On veut prédire le prix d’une maison à partir de sa taille (en milliers de pieds carrés). C’est un problème de régression : la cible (Price) est continue.

# Données d'entraînement : 4 exemples (Size, Price)
# Size  en milliers de pieds carrés, Price en centaines de milliers de $ (comme le .NET)
house_train = np.array([
    [1.1, 1.2],
    [1.9, 2.3],
    [2.8, 3.0],
    [3.4, 3.7],
])
X_house_train = house_train[:, 0].reshape(-1, 1)   # feature : Size
y_house_train = house_train[:, 1]                   # cible : Price
print(f"Données d'entraînement : {len(y_house_train)} exemples")
Données d'entraînement : 4 exemples

Lecture des données d’entraînement

Quatre exemples suffisent pour une régression linéaire à une feature : le modèle n’a que deux paramètres (pente et ordonnée à l’origine) à estimer. Lisez le tableau : le prix suit presque proportionnellement la taille (1,1 → 1,2 ; 1,9 → 2,3 ; 2,8 → 3,0 ; 3,4 → 3,7) — le rapport prix/taille est proche de 1,05 partout. La relation paraît linéaire dans cette plage (1,1 à 3,4) ; nous verrons si elle tient au-delà.

Construire et entraîner le pipeline

Le pipeline le plus simple pour une régression linéaire : un modèle LinearRegression qui apprend la relation Price = a * Size + b par moindres carrés (OLS — Ordinary Least Squares). C’est l’équivalent naturel du Sdca de ML.NET sur ce petit jeu.

model_house = LinearRegression()
model_house.fit(X_house_train, y_house_train)
print(f"Modèle entraîné : Price = {model_house.coef_[0]:.3f} * Size + {model_house.intercept_:.3f}")
Modèle entraîné : Price = 1.049 * Size + 0.137

Lecture des coefficients appris

Le modèle apprend Price = 1,049 x Size + 0,137. La pente de 1,049 reflète le rapport prix/taille observé à l’œil dans les données (~1,05) ; l’ordonnée à l’origine de 0,137 est petite mais non nulle — un léger « tarif de base ». C’est la solution des moindres carrés ordinaires : pour deux paramètres, il existe une formule fermée (géométriquement, la projection orthogonale des points sur l’espace des droites). Aucune itération, aucun hyperparamètre — le modèle est exact.

Évaluer le modèle sur des données de test

On dispose de 15 exemples de test (dont certains sortent de la plage d’entraînement, jusqu’à 8 milliers de pieds carrés). On mesure le coefficient de détermination R² : 1.0 = prédiction parfaite, 0.0 = pas mieux que la moyenne.

# 15 exemples de test
house_test = np.array([
    [1.1, 1.2], [1.2, 1.5], [1.4, 1.7], [1.6, 1.9], [1.9, 2.3],
    [2.8, 3.0], [3.2, 3.5], [3.3, 3.6], [3.5, 3.9], [3.7, 4.3],
    [4.0, 6.1], [5.0, 7.2], [6.0, 8.5], [7.0, 9.8], [8.0, 10.3],
])
X_house_test = house_test[:, 0].reshape(-1, 1)
y_house_test = house_test[:, 1]

y_house_pred = model_house.predict(X_house_test)
r2_house = r2_score(y_house_test, y_house_pred)
mae_house = mean_absolute_error(y_house_test, y_house_pred)
print(f"R² sur le test  : {r2_house:.3f}")
print(f"MAE sur le test : {mae_house:.3f} (centaines de milliers de $)")
R² sur le test  : 0.852
MAE sur le test : 0.716 (centaines de milliers de $)

Lecture du jeu de test : l’extrapolation est volontaire

Regardez la construction du jeu de test : 15 exemples étalés de 1,1 à 8,0 — bien au-delà de la plage d’entraînement (1,1 à 3,4). Ce choix est délibéré : il va exposer le comportement du modèle en extrapolation, là où aucune donnée ne le contraint. Les points entre 4,0 et 8,0 testent la capacité de la droite apprise sur le segment initial à prédire le monde extérieur — et les valeurs réelles y montrent un prix au pied carré croissant.

Prédire le prix d’une nouvelle maison

Une fois le modèle entraîné, on peut prédire le prix pour une taille non vue.

size_new = np.array([[2.5]])            # 2 500 pieds carrés
price_new = model_house.predict(size_new)
print(f"Prix prévu pour {size_new[0,0]*1000:.0f} pieds carrés : {price_new[0]*100:.0f}k $")
Prix prévu pour 2500 pieds carrés : 276k $

Félicitations ! Vous avez entraîné un modèle de régression avec scikit-learn en quelques lignes. Visualisons la droite apprise et les données.

fig, ax = plt.subplots(figsize=(7, 4.5))
ax.scatter(X_house_train, y_house_train, color="tab:blue", s=90, label="Entraînement", zorder=3)
ax.scatter(X_house_test, y_house_test, color="tab:orange", marker="s", label="Test", zorder=3)
xs = np.linspace(0, 9, 100).reshape(-1, 1)
ax.plot(xs, model_house.predict(xs), color="tab:green", linewidth=2, label="Droite de régression")
ax.scatter(size_new, price_new, color="tab:red", marker="*", s=220, label=f"Prédiction (2.5 → {price_new[0]:.2f})", zorder=4)
ax.set_xlabel("Size (milliers de pieds carrés)")
ax.set_ylabel("Price (centaines de milliers de $)")
ax.set_title("Régression linéaire : prix d'une maison selon sa taille")
ax.legend(loc="upper left")
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()

Lecture du graphique

Le nuage de points et la droite de régression rendent visible ce que le R² = 0,852 quantifiait : la droite passe au milieu des points d’entraînement (bleu) mais sous-estime systématiquement les grandes tailles (orange, points test). Regardez l’écart croissant à droite du graphe : à 8 000 pieds carrés, le prix réel (10,3) dépasse largement la prédiction linéaire (~8,5). C’est la signature d’une relation non linéaire (effet « premium » des grandes surfaces) qu’un modèle de degré 1 ne peut pas capturer.

La prédiction pour 2,5 (l’étoile rouge) est raisonnable car elle tombe dans la plage des données d’entraînement — l’erreur locale y est faible. En extrapolation (au-delà de 3,4), l’erreur croît de façon non mesurée : c’est la limite d’un modèle linéaire appris sur 4 points.

Exemple guide 2 : Prédiction du temps de trajet (régression)

Deuxième exemple de régression : on prédit le temps de trajet (en minutes) à partir de la distance (en km). Cette fois la relation est presque exactement linéaire (≈ 3 min/km), ce qui donnera un R² proche de 1.

trip = np.array([
    [5, 15], [10, 30], [14, 41], [20, 60], [26, 79], [32, 95], [40, 121],
], dtype=float)
X_trip = trip[:, 0].reshape(-1, 1)
y_trip = trip[:, 1]

model_trip = LinearRegression()
model_trip.fit(X_trip, y_trip)

distance_new = np.array([[25.0]])
temps_prevu = model_trip.predict(distance_new)
print(f"Temps prévu pour 25 km : {temps_prevu[0]:.1f} minutes")
print(f"Pente apprise : {model_trip.coef_[0]:.3f} min/km")
Temps prévu pour 25 km : 75.1 minutes
Pente apprise : 3.021 min/km

Interpolation vs extrapolation : le contraste des deux exemples

Comparez les deux prédictions : ici, 25 km est dans la plage observée (5 à 40 km) — le modèle interpole, et l’erreur attendue est de l’ordre du bruit résiduel. Dans l’exemple 1, la prédiction pour 2,5 interpolait aussi (plage 1,1-3,4), mais l’évaluation test poussait jusqu’à 8,0 — en extrapolation, où l’erreur explose. La distinction est fondamentale en pratique : un modèle n’est fiable que dans la plage de ses données d’entraînement. Tout rapport de déploiement devrait répondre à la question : mes données de production sont-elles dans la plage d’entraînement ?

Exemple guide 3 : Classification binaire de transactions

On passe à la classification : prédire si une transaction bancaire est suspecte (oui/non) à partir de son montant et de l’heure. La cible est binaire — on utilise une régression logistique (LogisticRegression), l’équivalent du SdcaLogisticRegression de ML.NET.

# 12 transactions : 6 normales, 6 suspectes (montant élevé + heure nocturne)
transactions = np.array([
    [120, 9], [450, 14], [50, 11], [300, 17], [200, 15], [490, 8],     # normales
    [2500, 2], [3100, 4], [4000, 3], [2100, 1], [5000, 5], [2800, 2.5]  # suspectes
], dtype=float)
X_tx = transactions
y_tx = np.array([0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1])   # 0 = normale, 1 = suspecte

model_tx = LogisticRegression(random_state=42)
model_tx.fit(X_tx, y_tx)

# Évaluation (sur les données d'entraînement, comme le .NET)
y_tx_pred = model_tx.predict(X_tx)
y_tx_proba = model_tx.predict_proba(X_tx)[:, 1]
acc = accuracy_score(y_tx, y_tx_pred)
auc = roc_auc_score(y_tx, y_tx_proba)
print("--- Évaluation du modèle ---")
print(f"Accuracy : {acc:.1%}")
print(f"AUC      : {auc:.3f}")
--- Évaluation du modèle ---
Accuracy : 100.0%
AUC      : 1.000

Tests de prédiction

Deux nouveaux cas à classer : une transaction suspecte (montant élevé, heure nocturne) et une transaction normale.

tests = np.array([[3500, 3], [150, 10]])
preds = model_tx.predict(tests)
proba = model_tx.predict_proba(tests)[:, 1]
for (montant, heure), p, pr in zip(tests, preds, proba):
    label = "SUSPECTE" if p == 1 else "normale"
    print(f"Montant={montant:.0f}, Heure={heure:.0f}h -> {label} (probabilité : {pr:.1%})")
Montant=3500, Heure=3h -> SUSPECTE (probabilité : 100.0%)
Montant=150, Heure=10h -> normale (probabilité : 0.0%)

Interprétation : perfection apparente et sur-apprentissage

L’accuracy de 100 % et l’AUC de 1,000 sont parfaits — et c’est précisément le problème. Avec 12 exemples et 2 features (montant, heure), les classes sont linéairement séparables par construction : les transactions suspectes ont toutes un montant > 2000 $ et une heure < 6 h, les normales < 500 $ et > 8 h. La régression logistique n’a aucune difficulté à trouver une frontière qui les sépare sans erreur.

Ce score parfait sur le jeu d’entraînement ne dit rien sur la généralisation : en production, une transaction à 1 800 $ à 7 h du matin (proche de la frontière) serait classée avec une probabilité bien inférieure à 100 %, et potentiellement mal. La leçon : un score parfait sur des données synthétiques séparables est un signal d’alerte, pas de succès. Il faut des données plus bruitées, ou une évaluation sur un jeu de test indépendant, pour mesurer la performance réelle.

La grammaire commune des trois exemples

Relisez les trois exemples : derrière la variété des sujets (prix immobilier, temps de trajet, détection de fraude), la même séquence de gestes revient à chaque fois :

  1. Structurer les données : un tableau X de features (taille, distance, montant + heure) et un vecteur y de cibles (prix, durée, étiquette 0/1).
  2. Choisir l’estimateur selon la nature de la cible : LinearRegression pour une cible continue (régression), LogisticRegression pour une cible binaire (classification). Ce choix n’est pas cosmétique — il fixe la fonction de perte minimisée (erreur quadratique vs log-vraisemblance).
  3. Entraîner : fit(X, y) — deux lignes, quel que soit le modèle.
  4. Évaluer : r2_score / mean_absolute_error côté régression, accuracy_score / roc_auc_score côté classification. La métrique dépend elle aussi de la nature du problème : un R² sur un problème de classification n’a pas de sens.
  5. Prédire : predict sur de nouvelles entrées — le seul geste qui sorte du notebook, celui qui sera exposé en production.

C’est cette grammaire que scikit-learn rend uniforme, exactement comme ML.NET avec son context.Regression.Trainers.Sdca() et ses Evaluate(). Les exercices qui suivent l’étendent dans trois directions : mesurer quelles features portent le signal (importance par permutation), connaître les données avant de modéliser (exploration statistique), et sortir du cas une-feature-une-cible (régression multi-features).

Exercices supplémentaires

Les trois exercices suivent ceux du notebook .NET. Ils sont laissés à compléter : lisez les indices, écrivez votre code, puis exécutez la cellule.

Exercice 1 : Importance des features (permutation)

Objectif : mesurer quelle feature (Size, ou d’autres) pèse le plus sur la prédiction du prix.

  • Indice : scikit-learn fournit sklearn.inspection.permutation_importance.
  • Étape 1 : réutilisez model_house et les données de test X_house_test, y_house_test.
  • Étape 2 : appelez permutation_importance(model_house, X_house_test, y_house_test, random_state=42).
  • Étape 3 : affichez result.importances_mean (plus c’est grand, plus la feature compte).
# Exercice 1 : Importance des features
# TODO étudiant : calculez l'importance par permutation sur le modèle de régression maison
# from sklearn.inspection import permutation_importance
# result = permutation_importance(...)   # TODO étudiant
# print(result.importances_mean)          # TODO étudiant
print("Exercice à compléter : analyse de l'importance des features")
Exercice à compléter : analyse de l'importance des features

Exercice 2 : Exploration statistique des données

Objectif : calculer des statistiques descriptives sur un jeu de données d’employés.

  • Indice : utilisez pandas (pd.DataFrame) puis .describe().
  • Étape 1 : créez un DataFrame avec des colonnes expérience, education, taille_societe, salaire.
  • Étape 2 : appelez df.describe() pour obtenir moyenne, écart-type, min, max.
  • Étape 3 : comptez les valeurs uniques d’une colonne catégorielle avec df["col"].value_counts().
# Exercice 2 : Exploration statistique des données
# TODO étudiant : utilisez pandas pour résumer un jeu de données d'employés
# import pandas as pd
# df = pd.DataFrame(...)                   # TODO étudiant
# print(df.describe())                     # TODO étudiant
print("Exercice à compléter : exploration statistique des données")
Exercice à compléter : exploration statistique des données

Exercice 3 : Régression multi-features — prédiction de salaire

Objectif : entraîner une régression linéaire sur plusieurs features (années d’expérience, niveau d’éducation, taille de société) pour prédire le salaire.

  • Étape 1 : construisez X de forme (n, 3) et y (salaire) pour 10+ employés.
  • Étape 2 : entraînez LinearRegression().fit(X, y).
  • Étape 3 : évaluez avec r2_score et mean_absolute_error.
  • Étape 4 : prédisez le salaire pour un employé (5 ans, Master=2, Grande société=2).
  • Indice : valeurs possibles — expérience=2/10, education=1/3, societe=1/3.
# Exercice 3 : Régression multi-features - prédiction de salaire
# TODO étudiant : définir les données d'employés et entraîner le modèle
# X = np.array([[2, 1, 1], [10, 3, 3], ...])   # TODO étudiant (10+ lignes)
# y = np.array([32000, 110000, ...])            # TODO étudiant
# model = LinearRegression().fit(X, y)          # TODO étudiant
# pred = model.predict([[5, 2, 2]])             # TODO étudiant
print("Exercice à compléter : régression multi-features (salaire)")
Exercice à compléter : régression multi-features (salaire)

Résumé

Vous avez parcouru le workflow complet d’un projet de Machine Learning avec scikit-learn :

Étape scikit-learn (Python) ML.NET (.NET)
Imports import numpy, sklearn #r nuget Microsoft.ML
Contexte graine np.random.default_rng new MLContext(seed)
Données tableaux NumPy X, y classes POCO + IDataView
Pipeline LinearRegression() Concatenate + Trainers.Sdca
Entraînement model.fit(X, y) pipeline.Fit(data)
Évaluation r2_score, accuracy_score mlContext.Regression.Evaluate
Prédiction model.predict(X_new) predictionEngine.Predict(x)

Deux familles abordées à travers trois exemples guidés : régression linéaire (maison, trajet), classification binaire (transactions). Les concepts sont identiques entre .NET et Python — seules les API et les solveurs par défaut diffèrent.

Jumeau .NET : ML-1-Introduction.ipynb. Autres jumeaux Python de la série : ML-5-TimeSeries-Python, ML-7-Recommendation-Python, ML-9-Anomaly-Detection-Python.

Références

  • scikit-learn — documentation officielle : https://scikit-learn.org/stable/
  • LinearRegression (OLS) : https://scikit-learn.org/stable/modules/linear_model.html
  • LogisticRegression : https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  • permutation_importance : https://scikit-learn.org/stable/modules/permutation_importance.html
  • Notebook .NET d’origine : ML-1-Introduction.ipynb (ML.NET)
  • Épic de parité : issue #4956
Retour au sommet