Aller au contenu principal

Module 5 — Spark ML : transformateurs et estimateurs

Nous avons de quoi lire, filtrer, agréger. Il reste à préparer les données pour un algorithme. Spark ML, la bibliothèque d'apprentissage automatique moderne de Spark (aussi appelée MLlib DataFrame), le fait avec deux abstractions simples et deux règles à retenir : la colonne features doit être un vecteur unique, et les statistiques d'apprentissage doivent venir uniquement de l'ensemble d'entraînement.

Deux briques, deux contrats

Un transformateur (Transformer) prend un DataFrame et renvoie un DataFrame enrichi de nouvelles colonnes, sans avoir besoin d'apprendre quoi que ce soit sur les données. Sa méthode s'appelle transform. Un VectorAssembler qui empile des colonnes numériques dans un vecteur est un transformateur ; un SQLTransformer qui applique du SQL en est un autre.

Un estimateur (Estimator) doit apprendre sur les données avant de pouvoir transformer. Sa méthode s'appelle fit et retourne un modèle — qui est, par construction, un transformateur. Un StringIndexer qui apprend le vocabulaire d'une colonne catégorielle est un estimateur ; sa version ajustée StringIndexerModel est un transformateur. Toutes les classes d'algorithmes (LogisticRegression, RandomForestClassifier…) sont des estimateurs.

from pyspark.ml.feature import StringIndexer

indexeur = StringIndexer(inputCol="compagnie", outputCol="compagnie_idx")
modele_index = indexeur.fit(vols_train) # estimateur -> modèle
vols_train_idx = modele_index.transform(vols_train) # transformateur
vols_test_idx = modele_index.transform(vols_test) # même vocabulaire

La règle qui découle : fit uniquement sur train, transform sur train et test. Cette discipline évite la fuite de données que le module 6 formalisera dans un Pipeline.

Assembler un vecteur de variables

Tous les algorithmes de Spark ML attendent une seule colonne d'entrée, appelée par convention features, contenant un Vector par ligne. On l'obtient avec un VectorAssembler :

from pyspark.ml.feature import VectorAssembler

assembleur = VectorAssembler(
inputCols=["distance_km", "heure_depart", "compagnie_idx", "jour_semaine"],
outputCol="features",
handleInvalid="skip", # ou "keep" (vecteur creux) ou "error"
)
vols_prets = assembleur.transform(vols_train_idx)
vols_prets.select("features", "retard_arrivee").show(3, truncate=False)

Trois précautions à intégrer dès le premier assemblage. Types : toutes les colonnes d'entrée doivent être numériques ; passer une StringType fait échouer l'étape avec un message parfois obscur. Nullité : handleInvalid doit être choisi explicitement ("skip" supprime les lignes contenant null, "keep" produit un vecteur creux avec un marqueur, "error" refuse). Ordre : la méthode .toArray() d'un Vector renvoie les valeurs dans l'ordre de inputCols ; c'est cet ordre qui pilotera aussi l'interprétation des coefficients d'un modèle linéaire.

Encoder les variables catégorielles

Une variable catégorielle passe par deux étapes. D'abord StringIndexer convertit les catégories en entiers 0, 1, 2, … par fréquence décroissante. Ensuite OneHotEncoder transforme ces entiers en vecteurs creux — sauf pour un modèle d'arbre, qui accepte l'index tel quel :

from pyspark.ml.feature import StringIndexer, OneHotEncoder

indexeur = StringIndexer(inputCol="origine", outputCol="origine_idx", handleInvalid="keep")
encodeur = OneHotEncoder(inputCol="origine_idx", outputCol="origine_ohe")

handleInvalid="keep" protège des catégories nouvelles vues à l'inférence — c'est très fréquent en production (un nouvel aéroport, un nouveau code compagnie). Sans ce paramètre, la première catégorie inconnue plante l'ensemble du travail.

Mettre à l'échelle les variables numériques

Les modèles linéaires et le k-moyennes sont sensibles à l'échelle des variables. Un StandardScaler retire la moyenne et divise par l'écart-type, appris sur train :

from pyspark.ml.feature import StandardScaler

echelle = StandardScaler(inputCol="features", outputCol="features_std",
withMean=True, withStd=True)
modele_echelle = echelle.fit(vols_prets)
vols_prets_std = modele_echelle.transform(vols_prets)

Notez que withMean=True densifie les vecteurs (soustraire la moyenne à un vecteur creux le rend dense) : sur des données très creuses, préférer withMean=False pour préserver la structure creuse.

Ce qui existe et ce qui manque

Le paquet pyspark.ml.feature couvre : Tokenizer et HashingTF/IDF (texte simple), Bucketizer (découpage en tranches), QuantileDiscretizer, PCA, Imputer (moyenne, médiane ou mode), StopWordsRemover, NGram, Word2Vec.

Ce qui n'existe pas et surprend : pas d'encodage cible (target encoding), pas d'encodage de fréquence prêt à l'emploi, pas de ColumnTransformer équivalent à celui de scikit-learn. Ce qui manque s'écrit à la main en SQL ou en withColumn, et se pose ensuite dans le pipeline sous forme de SQLTransformer.

Ne pas mélanger les schémas

Chaque Transformer conserve son schéma d'entrée : réutiliser un VectorAssembler ajusté à ["a", "b", "c"] sur un DataFrame qui ne contient plus c lève une erreur peu explicite. En production, cette mismatch se traduit par un modèle qui échoue au premier predict sur une nouvelle base.

En résumé

  • Un transformateur transforme sans apprendre ; un estimateur apprend d'abord (fit), puis fournit un modèle qui est un transformateur.
  • L'entrée d'un algorithme Spark ML est une seule colonne vectorielle produite par VectorAssembler.
  • Variables catégorielles : StringIndexer puis OneHotEncoder pour les modèles linéaires, StringIndexer seul pour les arbres.
  • Toujours faire fit sur train et transform sur train et test ; le module suivant automatise cette discipline avec Pipeline.

Module suivant : chaîner ces briques dans un Pipeline et valider avec un CrossValidator distribué, sans introduire de fuite.