scikit-learn
Une API uniforme : tout estimateur a fit, tout transformateur a transform, tout
prédicteur a predict. Le reste en découle.
Découper
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
stratify=y conserve la proportion des classes — indispensable en classification
déséquilibrée. random_state rend le découpage reproductible.
Le pipeline, à utiliser systématiquement
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
numeric = ["age", "montant"]
categorical = ["region", "segment"]
pre = ColumnTransformer([
("num", Pipeline([
("impute", SimpleImputer(strategy="median")),
("scale", StandardScaler()),
]), numeric),
("cat", Pipeline([
("impute", SimpleImputer(strategy="most_frequent")),
("onehot", OneHotEncoder(handle_unknown="ignore")),
]), categorical),
])
model = Pipeline([("pre", pre), ("clf", RandomForestClassifier(random_state=42))])
model.fit(X_train, y_train)
Le pipeline n'est pas une élégance : il empêche la fuite de données. Un StandardScaler
ajusté sur tout le jeu avant le split fait fuiter la moyenne du test dans l'entraînement, et
le score devient un mensonge.
handle_unknown="ignore" évite un crash quand une catégorie inconnue apparaît en
production.
Évaluer
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
y_pred = model.predict(X_test)
print(classification_report(y_test, y_pred))
precision recall f1-score support
0 0.91 0.95 0.93 248
1 0.78 0.66 0.71 72
accuracy 0.89 320
macro avg 0.84 0.80 0.82 320
weighted avg 0.88 0.89 0.88 320
Sur un jeu déséquilibré, l'accuracy ment : ici 89 % semble bon alors que la classe minoritaire n'est rattrapée qu'à 66 %. Regarder le rappel par classe, la matrice de confusion, et l'AUC.
proba = model.predict_proba(X_test)[:, 1]
print(roc_auc_score(y_test, proba))
Validation croisée
from sklearn.model_selection import cross_val_score, StratifiedKFold
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=cv, scoring="f1", n_jobs=-1)
print(scores.mean().round(3), scores.std().round(3))
0.712 0.031
L'écart-type compte autant que la moyenne : un modèle à 0.71 ± 0.03 est utilisable, à 0.71 ± 0.18 il est instable.
Recherche d'hyperparamètres
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
grid = {
"clf__n_estimators": [100, 300],
"clf__max_depth": [None, 10, 20],
"clf__min_samples_leaf": [1, 5],
}
search = GridSearchCV(model, grid, cv=cv, scoring="f1", n_jobs=-1, verbose=1)
search.fit(X_train, y_train)
print(search.best_params_, search.best_score_)
Le double underscore adresse un paramètre à travers le pipeline : clf__max_depth vise le
max_depth de l'étape nommée clf. Au-delà d'une centaine de combinaisons,
RandomizedSearchCV(n_iter=50) donne presque le même résultat bien plus vite.
Estimateurs courants
| Tâche | Point de départ | Ensuite |
|---|---|---|
| Classification tabulaire | LogisticRegression |
HistGradientBoostingClassifier, RandomForest |
| Régression tabulaire | Ridge |
HistGradientBoostingRegressor |
| Clustering | KMeans |
DBSCAN, AgglomerativeClustering |
| Réduction de dimension | PCA |
TSNE, UMAP (hors sklearn) |
HistGradientBoosting* gère les valeurs manquantes nativement et bat presque toujours une
forêt aléatoire sur du tabulaire.
Toujours établir une référence triviale avant de comparer :
from sklearn.dummy import DummyClassifier
DummyClassifier(strategy="most_frequent").fit(X_train, y_train).score(X_test, y_test)
Classes déséquilibrées
RandomForestClassifier(class_weight="balanced")
LogisticRegression(class_weight="balanced")
Et ajuster le seuil de décision plutôt que d'accepter 0.5 par défaut :
from sklearn.metrics import precision_recall_curve
p, r, seuils = precision_recall_curve(y_test, proba)
Sauvegarder
import joblib
joblib.dump(model, "model.joblib")
model = joblib.load("model.joblib")
Le pipeline entier est sérialisé, préprocessing compris — c'est le second intérêt majeur du pipeline : le même objet sert en entraînement et en production.