TP 7 – Ce client laissera-t-il un pourboire ?
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.
1. Les données
Section intitulée « 1. Les données »- Lire
data/trips.parquet;heure=F.hour("tpep_pickup_datetime"). - Joindre les zones pour obtenir
Borough. - Garder
trip_distance > 0etpayment_type = 1(carte). label= 1.0 sitip_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 ?
2. Deux pipelines
Section intitulée « 2. Deux pipelines »VectorAssembler → StandardScaler → RandomForestClassifier(numTrees=50, maxDepth=6), puis la même chose avec
LinearSVC.
3. AUC des deux modèles
Section intitulée « 3. AUC des deux modèles »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.
5. Courbe ROC en Matplotlib
Section intitulée « 5. Courbe ROC en Matplotlib »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.
Bonus 1 – CrossValidator + ParamGridBuilder
Section intitulée « Bonus 1 – CrossValidator + ParamGridBuilder »Sur numTrees et maxDepth, 3 plis. 3 plis × 4 combinaisons = 12 entraînements : c’est plus long.
Bonus 2 – Les vraies données
Section intitulée « Bonus 2 – Les vraies données »python scripts/download_official.py, puis relancez le notebook. Les résultats sont-ils aussi nets ?
Correction
Section intitulée « Correction »from pyspark.sql import functions as Ffrom pyspark.ml import Pipelinefrom pyspark.ml.feature import VectorAssembler, StandardScalerfrom pyspark.ml.classification import RandomForestClassifier, LinearSVCfrom pyspark.ml.evaluation import BinaryClassificationEvaluator, MulticlassClassificationEvaluatorfrom pyspark.ml.functions import vector_to_arrayimport numpy as npimport matplotlib.pyplot as plt
# 1. Paiements par carte uniquement, label = pourboire ou nonzones = 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,52train, test = df.randomSplit([0.8, 0.2], seed=7)
# 2. Deux classifieurs sur les mêmes featuresassembleur = VectorAssembler(inputCols=["trip_distance", "heure", "passenger_count", "manhattan", "nuit"], outputCol="features_brutes")normalisation = StandardScaler(inputCol="features_brutes", outputCol="features") # indispensable pour la SVMrf_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. AUCauc = 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, rappelrf_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 raisonnablescores = rf_pred.select("label", vector_to_array("probability")[1].alias("p")).toPandas()y, p = scores.label.values, scores.p.valuesseuils = 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 11rf_model.write().overwrite().save("out/modele_pourboire")Résultats attendus
Section intitulée « Résultats attendus »| Mesure | Valeur |
|---|---|
| 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înementsfrom pyspark.ml.tuning import CrossValidator, ParamGridBuilderrf_est = RandomForestClassifier(seed=1) # un estimateur NEUF : la grille doit porter sur SES paramètresgrille = 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.Pièges rencontrés en séance
Section intitulée « Pièges rencontrés en séance »| Symptôme | Cause | Solution |
|---|---|---|
| AUC ≈ 0,77, trop belle | payment_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 SVM | LinearSVC ne produit pas de probabilité | Tracer la ROC sur le Random Forest ; rawPrediction pour la SVM |
| Courbe ROC en escalier à 3 points | Seuils réguliers de 0 à 1 alors que les scores vont de 0,3 à 0,7 | Seuils = quantiles des scores |
ERROR OWLQN: Failure! Resetting history | Message interne de l’optimiseur de LinearSVC | Ignorer |
| CrossValidator très long | 12 entraînements de 50 arbres | Pré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.