Aller au contenu

TP 7 – Ce client laissera-t-il un pourboire ?

  • 50 min · 13h40 – 14h30
  • Notebook : jour2/TP7_classification.ipynb · corrigé Scala : scala/tp7_classification.scala
  • Données : data/trips.parquet, data/taxi_zone_lookup.csv

Objectif : classer les courses payées par carte (les pourboires en espèces ne sont pas enregistrés) : pourboire ou pas. Comparer Random Forest et SVM linéaire, tracer la courbe ROC.

Réussi si : AUC > 0,65 et courbe ROC tracée.
  • Lire data/trips.parquet ; heure = F.hour("tpep_pickup_datetime").
  • Joindre les zones pour obtenir Borough.
  • Garder trip_distance > 0 et payment_type = 1 (carte).
  • label = 1.0 si tip_amount > 0, sinon 0.0 (.cast("double")).
  • Features : trip_distance, heure, passenger_count, manhattan (Borough == “Manhattan”), nuit (heure ≥ 20 ou < 5).
  • Découpage : randomSplit([0.8, 0.2], seed=7).

Quel est le taux de pourboire ? Les classes sont-elles équilibrées ?

VectorAssembler → StandardScaler → RandomForestClassifier(numTrees=50, maxDepth=6), puis la même chose avec LinearSVC.

Indice : BinaryClassificationEvaluator(metricName="areaUnderROC").

4. Matrice de confusion (Random Forest, seuil 0,5)

Section intitulée « 4. Matrice de confusion (Random Forest, seuil 0,5) »

Indice : pred.groupBy("label").pivot("prediction").count(). Calculez précision et rappel à la main.

Récupérer label et la probabilité de la classe 1 avec vector_to_array("probability")[1], puis toPandas() (le test est petit). Les probabilités sont concentrées entre 0,3 et 0,7 : prenez comme seuils les quantiles des scores (numpy.quantile(p, numpy.linspace(0, 1, 50))) plutôt que 20 seuils réguliers. Pour chaque seuil, calculez le taux de vrais positifs et le taux de faux positifs, puis plt.plot(fpr, tpr).

6. Sauvegarder le pipeline Random Forest dans out/modele_pourboire

Section intitulée « 6. Sauvegarder le pipeline Random Forest dans out/modele_pourboire »

rf_model.write().overwrite().save("out/modele_pourboire").

Pourquoi le Random Forest et pas LinearSVC, alors que leurs AUC sont presque égales ? Le TP 11 de demain a besoin d’une probabilité (colonne probability), que LinearSVC ne fournit pas.

Sur numTrees et maxDepth, 3 plis. 3 plis × 4 combinaisons = 12 entraînements : c’est plus long.

python scripts/download_official.py, puis relancez le notebook. Les résultats sont-ils aussi nets ?

from pyspark.sql import functions as F
from pyspark.ml import Pipeline
from pyspark.ml.feature import VectorAssembler, StandardScaler
from pyspark.ml.classification import RandomForestClassifier, LinearSVC
from pyspark.ml.evaluation import BinaryClassificationEvaluator, MulticlassClassificationEvaluator
from pyspark.ml.functions import vector_to_array
import numpy as np
import matplotlib.pyplot as plt
# 1. Paiements par carte uniquement, label = pourboire ou non
zones = spark.read.csv("data/taxi_zone_lookup.csv", header=True, inferSchema=True)
df = (spark.read.parquet("data/trips.parquet")
.filter("trip_distance > 0 AND payment_type = 1")
.withColumn("heure", F.hour("tpep_pickup_datetime"))
.join(zones.select(F.col("LocationID").alias("PULocationID"), "Borough"), "PULocationID")
.withColumn("label", (F.col("tip_amount") > 0).cast("double"))
.withColumn("manhattan", (F.col("Borough") == "Manhattan").cast("double"))
.withColumn("nuit", ((F.col("heure") >= 20) | (F.col("heure") < 5)).cast("double"))
.select("trip_distance", "heure", "passenger_count", "manhattan", "nuit", "label"))
print("Taux de pourboire :", round(df.agg(F.avg("label")).first()[0], 3)) # ≈ 0,52
train, test = df.randomSplit([0.8, 0.2], seed=7)
# 2. Deux classifieurs sur les mêmes features
assembleur = VectorAssembler(inputCols=["trip_distance", "heure", "passenger_count", "manhattan", "nuit"],
outputCol="features_brutes")
normalisation = StandardScaler(inputCol="features_brutes", outputCol="features") # indispensable pour la SVM
rf_model = Pipeline(stages=[assembleur, normalisation,
RandomForestClassifier(numTrees=50, maxDepth=6, seed=1)]).fit(train)
svm_model = Pipeline(stages=[assembleur, normalisation, LinearSVC(maxIter=50)]).fit(train)
rf_pred, svm_pred = rf_model.transform(test), svm_model.transform(test)
# 3. AUC
auc = BinaryClassificationEvaluator(metricName="areaUnderROC")
print(f"AUC Random Forest : {auc.evaluate(rf_pred):.3f} AUC LinearSVC : {auc.evaluate(svm_pred):.3f}")
# 4. Matrice de confusion, précision, rappel
rf_pred.groupBy("label").pivot("prediction").count().orderBy("label").show()
for metrique in ["accuracy", "weightedPrecision", "weightedRecall"]:
print(metrique, round(MulticlassClassificationEvaluator(metricName=metrique).evaluate(rf_pred), 3))
# Importance des features : qu'est-ce qui fait pencher la balance ?
rf = rf_model.stages[-1]
for nom, imp in sorted(zip(assembleur.getInputCols(), rf.featureImportances.toArray()), key=lambda t: -t[1]):
print(f" {nom:16s} {imp:.3f}")
# 5. Courbe ROC : le test est petit, toPandas() est raisonnable
scores = rf_pred.select("label", vector_to_array("probability")[1].alias("p")).toPandas()
y, p = scores.label.values, scores.p.values
seuils = list(np.quantile(p, np.linspace(0, 1, 50))) + [1.01]
tpr = [((p >= t) & (y == 1)).sum() / (y == 1).sum() for t in seuils]
fpr = [((p >= t) & (y == 0)).sum() / (y == 0).sum() for t in seuils]
plt.plot(fpr, tpr, label=f"Random Forest (AUC = {auc.evaluate(rf_pred):.2f})")
plt.plot([0, 1], [0, 1], "--", color="gray", label="Hasard (AUC = 0,5)")
plt.xlabel("Taux de faux positifs"); plt.ylabel("Taux de vrais positifs (rappel)"); plt.legend()
# 6. Sauvegarde pour le TP 11
rf_model.write().overwrite().save("out/modele_pourboire")
MesureValeur
Taux de pourboire (carte)≈ 52 %, classes équilibrées
AUC Random Forest≈ 0,70
AUC LinearSVC≈ 0,70
Exactitude / précision / rappel (RF, seuil 0,5)≈ 63 % / 67 % / 60 %
# Bonus 1 – CrossValidator : 3 plis × 4 combinaisons = 12 entraînements
from pyspark.ml.tuning import CrossValidator, ParamGridBuilder
rf_est = RandomForestClassifier(seed=1) # un estimateur NEUF : la grille doit porter sur SES paramètres
grille = ParamGridBuilder().addGrid(rf_est.numTrees, [20, 50]).addGrid(rf_est.maxDepth, [4, 8]).build()
cv = CrossValidator(estimator=Pipeline(stages=[assembleur, normalisation, rf_est]),
estimatorParamMaps=grille, evaluator=auc, numFolds=3, seed=1)
best = cv.fit(train).bestModel.stages[-1]
print(best.getNumTrees, best.getMaxDepth())
# Piège : une grille écrite sur `rf` (le modèle déjà ajusté) serait ignorée sans erreur, car un Param n'est
# reconnu que si son parent (uid) est l'estimateur du pipeline. Les 12 entraînements garderaient les valeurs par défaut.
SymptômeCauseSolution
AUC ≈ 0,77, trop bellepayment_type resté dans les features, ou filtre carte oubliéFiltrer payment_type = 1, retirer la colonne : c’est une fuite
probability introuvable sur le modèle SVMLinearSVC ne produit pas de probabilitéTracer la ROC sur le Random Forest ; rawPrediction pour la SVM
Courbe ROC en escalier à 3 pointsSeuils réguliers de 0 à 1 alors que les scores vont de 0,3 à 0,7Seuils = quantiles des scores
ERROR OWLQN: Failure! Resetting historyMessage interne de l’optimiseur de LinearSVCIgnorer
CrossValidator très long12 entraînements de 50 arbresPrévenir, ou réduire la grille
  • Un participant présente sa courbe ROC.
  • AUC 0,70 : est-ce un bon modèle ? Honnête pour un comportement humain avec 5 features.
  • Précision ou rappel : lequel privilégier ? Selon le coût d’un faux positif par rapport à un faux négatif.
  • À retenir : la métrique se choisit selon le coût des erreurs. Le modèle sauvegardé resservira demain, en temps réel.

Dépôt des participants : formations2026/formation-spark-ml (branche participants)

Contenu pédagogique © Ambient IT · 140 Avenue Jean Lolive, 93500 Pantin · formation@ambient-it.net · +33 (0)1 48 10 09 46

Organisme de formation enregistré sous le numéro 11 75 54743 75. Cet enregistrement ne vaut pas agrément de l'État.