# Load libraries
library(tidyverse)
library(tidymodels)
library(rpart)
# Set a common visual theme
theme_set(theme_minimal(base_size = 16))Tutoriel: comparer trois modèles sans utiliser le futur
Régression logistique, arbre, forêt et validation temporelle
Objectif
Ce tutoriel construit une comparaison reproductible entre une régression logistique, un arbre de décision et une forêt aléatoire. La question n’est pas seulement de trouver la plus grande aire ROC. Il faut définir le futur à prédire, empêcher les fuites, répéter l’évaluation dans le temps, examiner la stabilité, réserver un test final et relier le choix à un mandat.
Les données décrivent 18 000 demandes de services citoyennes 311 créées à Montréal en 2024. La cible pédagogique distingue les demandes dont le dernier statut enregistré correspond ou non à une demande terminée dans les sept jours suivant la création. Cette fenêtre n’est pas une norme officielle de service. Elle fournit une définition opérationnelle commune pour apprendre la modélisation prédictive.
Carte théorique
Le tutoriel suit sept décisions:
- définir l’unité, la cible et le moment de prédiction;
- séparer l’information disponible de l’information future;
- réserver un test futur et construire des fenêtres temporelles;
- appliquer le même prétraitement aux trois candidats;
- comparer discrimination, précision-rappel, Brier et stabilité;
- ouvrir le test une seule fois;
- documenter le choix, la calibration, les limites et la surveillance.
Un modèle flexible n’est pas automatiquement meilleur. Il peut mieux représenter des relations non linéaires, mais aussi exploiter des particularités qui ne persistent pas. Une comparaison honnête doit donc donner aux candidats le même budget de données et une question identique.
Préparer l’environnement
tidymodels charge notamment rsample, recipes, parsnip, workflows, workflowsets, tune et yardstick. Le moteur rpart ajuste l’arbre. Le moteur ranger, appelé plus loin par parsnip, ajuste la forêt aléatoire.
1. Charger et documenter les données
requetes_ml <- read_csv(
"data/requetes_311_montreal_2024_eiom.csv",
show_col_types = FALSE
) |>
mutate(
date_creation = as.Date(date_creation),
verite = factor(
issue_7_jours,
levels = c("non_terminee_7_jours", "terminee_7_jours")
)
) |>
arrange(date_creation, identifiant_requete)
glimpse(requetes_ml)Rows: 18,000
Columns: 16
$ identifiant_requete <chr> "24-103", "24-35", "24-57", "24-71", "24-72…
$ date_creation <date> 2024-01-01, 2024-01-01, 2024-01-01, 2024-0…
$ mois_creation <chr> "Janvier", "Janvier", "Janvier", "Janvier",…
$ jour_semaine <chr> "Lundi", "Lundi", "Lundi", "Lundi", "Lundi"…
$ plage_horaire <chr> "Soir, 18 h à 23 h 59", "Matin, 6 h à 11 h …
$ activite <chr> "Feux de circulation - Entretien", "Collect…
$ type_lieu <chr> "Intersection", "Adresse", "Adresse", "Inte…
$ arrondissement <chr> "Mercier-Hochelaga-Maisonneuve", "L'Île-Biz…
$ provenance <chr> "Téléphone", "Téléphone", "Téléphone", "Tél…
$ dernier_statut <chr> "Terminée", "Terminée", "Terminée", "Termin…
$ date_dernier_statut <date> 2024-01-01, 2024-01-18, 2024-01-03, 2024-0…
$ delai_dernier_statut_jours <dbl> 0.036, 17.053, 1.833, 0.018, 2.754, 2.689, …
$ issue_7_jours <chr> "terminee_7_jours", "non_terminee_7_jours",…
$ source_page_url <chr> "https://www.donneesquebec.ca/recherche/dat…
$ access_date <date> 2026-07-14, 2026-07-14, 2026-07-14, 2026-0…
$ verite <fct> terminee_7_jours, non_terminee_7_jours, ter…
Une ligne représente une demande de service. Le fichier ne décrit pas une personne et ne mesure pas directement la qualité du service. Les processus, les catégories et le transfert de certaines demandes peuvent varier selon le temps ou le territoire.
Vérifions les principaux repères:
resume_source <- requetes_ml |>
summarise(
lignes = n(),
premiere_date = min(date_creation),
derniere_date = max(date_creation),
activites = n_distinct(activite),
territoires = n_distinct(arrondissement),
canaux = n_distinct(provenance)
)
resume_source# A tibble: 1 × 6
lignes premiere_date derniere_date activites territoires canaux
<int> <date> <date> <int> <int> <int>
1 18000 2024-01-01 2024-12-31 351 27 9
Le fichier contient 18 000 demandes, 351 activités, 27 valeurs territoriales et 9 canaux de provenance. Ces cardinalités expliquent pourquoi le prétraitement devra traiter les catégories rares et nouvelles.
2. Définir la cible et le moment de prédiction
La question prédictive est:
Au moment où une demande est créée, quelle est la probabilité que son dernier statut enregistré ne corresponde pas à une demande terminée dans les sept jours?
Cette formulation fixe quatre éléments:
| Élément | Définition |
|---|---|
| unité | une demande de l’échantillon |
| moment | date de création |
| événement positif | non_terminee_7_jours |
| sortie du modèle | probabilité conditionnelle |
L’ordre des niveaux du facteur verite place l’événement positif en premier. Cette convention sera utilisée par yardstick avec event_level = "first".
3. Reconnaître la fuite d’information
La réponse est construite en observant ce qui arrive après la création. Cette observation future est nécessaire pour étiqueter les lignes historiques. Elle ne peut pas devenir une information prédictive disponible au moment annoncé.
Variables permises:
jour_semaine;plage_horaire;activite;type_lieu;arrondissement;provenance.
Variables interdites comme prédicteurs:
dernier_statut;date_dernier_statut;delai_dernier_statut_jours;issue_7_jours, qui est la réponse source.
Une fuite peut produire une performance spectaculaire tout en détruisant la validité opérationnelle. La bonne question n’est pas « cette colonne prédit-elle bien? », mais « sa valeur serait-elle connue lorsque la probabilité doit être produite? ».
4. Observer le taux de l’événement dans le temps
frequence_mensuelle <- requetes_ml |>
mutate(mois = floor_date(date_creation, unit = "month")) |>
summarise(
demandes = n(),
proportion_non_terminee = mean(verite == "non_terminee_7_jours"),
.by = mois
)
frequence_mensuelle# A tibble: 12 × 3
mois demandes proportion_non_terminee
<date> <int> <dbl>
1 2024-01-01 1437 0.347
2 2024-02-01 985 0.380
3 2024-03-01 1044 0.428
4 2024-04-01 1812 0.475
5 2024-05-01 1915 0.502
6 2024-06-01 1805 0.482
7 2024-07-01 1894 0.460
8 2024-08-01 1816 0.422
9 2024-09-01 1643 0.411
10 2024-10-01 1471 0.405
11 2024-11-01 1238 0.397
12 2024-12-01 940 0.382
ggplot(
frequence_mensuelle,
aes(mois, proportion_non_terminee)
) +
geom_line(linewidth = 0.9, color = "#0f7c80") +
geom_point(size = 2.5, color = "#0f7c80") +
scale_y_continuous(labels = scales::label_percent()) +
labs(
x = NULL,
y = "Proportion non terminée en sept jours"
)La fréquence de la cible varie entre les mois. Cette variation peut refléter la composition des demandes, des processus, des saisons ou d’autres mécanismes non observés. Le graphique justifie une évaluation qui respecte l’ordre temporel. Il ne fournit pas une explication causale de la variation.
5. Réserver un test futur
Les demandes d’octobre à décembre forment le test final. Les demandes antérieures servent à construire et comparer les candidats.
date_coupure <- as.Date("2024-10-01")
entrainement_ml <- requetes_ml |>
filter(date_creation < date_coupure)
test_ml <- requetes_ml |>
filter(date_creation >= date_coupure)
resume_partage <- requetes_ml |>
mutate(
ensemble = if_else(
date_creation < date_coupure,
"Entraînement",
"Test futur"
)
) |>
summarise(
n = n(),
debut = min(date_creation),
fin = max(date_creation),
proportion_non_terminee = mean(verite == "non_terminee_7_jours"),
.by = ensemble
)
resume_partage# A tibble: 2 × 5
ensemble n debut fin proportion_non_terminee
<chr> <int> <date> <date> <dbl>
1 Entraînement 14351 2024-01-01 2024-09-30 0.441
2 Test futur 3649 2024-10-01 2024-12-31 0.396
L’entraînement contient 14 351 demandes de janvier à septembre, dont 44,1 % appartiennent à l’événement positif. Le test contient 3 649 demandes d’octobre à décembre, dont 39,6 % appartiennent à cet événement.
Le test ne sert ni à choisir les prédicteurs, ni à régler les modèles, ni à sélectionner le gagnant initial. Il répond à une question confirmatoire après que ces décisions sont écrites. Tidy Modeling with R insiste sur cette séparation entre le test et les opérations de sélection réalisées dans l’entraînement (M. Kuhn et J. Silge, 2022).
6. Construire une validation temporelle
Une seule séparation de l’entraînement serait sensible au mois choisi. Nous créons cinq fenêtres. Chaque fenêtre apprend sur quatre mois consécutifs et évalue le mois suivant.
plis_temporels <- sliding_period(
entrainement_ml,
index = date_creation,
period = "month",
lookback = 3,
assess_stop = 1,
complete = TRUE,
step = 1
)
plis_temporels# Sliding period resampling
# A tibble: 5 × 2
splits id
<list> <chr>
1 <split [5278/1915]> Slice1
2 <split [5756/1805]> Slice2
3 <split [6576/1894]> Slice3
4 <split [7426/1816]> Slice4
5 <split [7430/1643]> Slice5
Avec sliding_period(), le groupe temporel courant appartient à l’analyse. lookback = 3 ajoute les trois groupes antérieurs, ce qui produit quatre mois d’apprentissage. assess_stop = 1 réserve le groupe suivant à l’évaluation.
resume_plis <- tibble(
pli = plis_temporels$id,
n_entrainement = map_int(
plis_temporels$splits,
~ nrow(analysis(.x))
),
periode_entrainement = map_chr(
plis_temporels$splits,
~ paste(range(analysis(.x)$date_creation), collapse = " au ")
),
n_validation = map_int(
plis_temporels$splits,
~ nrow(assessment(.x))
),
periode_validation = map_chr(
plis_temporels$splits,
~ paste(range(assessment(.x)$date_creation), collapse = " au ")
)
)
resume_plis# A tibble: 5 × 5
pli n_entrainement periode_entrainement n_validation periode_validation
<chr> <int> <chr> <int> <chr>
1 Slice1 5278 2024-01-01 au 2024-04-30 1915 2024-05-01 au 202…
2 Slice2 5756 2024-02-01 au 2024-05-31 1805 2024-06-01 au 202…
3 Slice3 6576 2024-03-01 au 2024-06-30 1894 2024-07-01 au 202…
4 Slice4 7426 2024-04-01 au 2024-07-31 1816 2024-08-01 au 202…
5 Slice5 7430 2024-05-01 au 2024-08-31 1643 2024-09-01 au 202…
Chaque évaluation est strictement postérieure à son apprentissage. Les mois de validation sont mai, juin, juillet, août et septembre.
7. Comprendre ce que le protocole estime
Une validation aléatoire et une validation temporelle répondent à des questions différentes.
| Protocole | Question approximative |
|---|---|
| plis aléatoires | le modèle prédit-il une ligne cachée parmi des observations contemporaines? |
| fenêtres temporelles | le modèle entraîné récemment prédit-il le mois suivant? |
Le second protocole imite mieux l’usage annoncé. Il révèle aussi la variation de performance entre les mois. Il ne garantit pas que l’année suivante ressemblera à 2024. Il produit une preuve plus pertinente pour cette cible, ce calendrier et ce mécanisme de collecte.
8. Définir un prétraitement commun
recette_ml <- recipe(
verite ~ jour_semaine + plage_horaire + activite +
type_lieu + arrondissement + provenance,
data = entrainement_ml
) |>
step_novel(all_nominal_predictors(), new_level = "Nouveau") |>
step_unknown(all_nominal_predictors(), new_level = "Non précisé") |>
step_other(activite, threshold = 0.01, other = "Autres activités") |>
step_other(type_lieu, threshold = 0.01, other = "Autres lieux") |>
step_other(
arrondissement,
threshold = 0.005,
other = "Autres territoires"
) |>
step_other(provenance, threshold = 0.005, other = "Autres canaux") |>
step_zv(all_predictors())
recette_mlLes étapes ont des rôles distincts:
step_novel()réserve un niveau pour une catégorie jamais vue à l’apprentissage;step_unknown()transforme une valeur manquante en niveau explicite;step_other()regroupe les catégories trop rares selon l’apprentissage;step_zv()retire un prédicteur sans variation.
La recette n’est pas préparée avec le test ou toutes les validations. Dans fit_resamples(), elle est préparée séparément sur l’analyse de chaque fenêtre. Les fréquences qui déterminent les regroupements restent donc internes à l’apprentissage.
9. Comprendre un arbre de décision
Un arbre divise récursivement l’espace des prédicteurs. À chaque noeud, une règle sépare les observations en groupes plus homogènes par rapport à la cible. Une feuille terminale produit une probabilité à partir des réponses observées dans cette feuille.
Si une feuille contient 30 demandes, dont 18 appartiennent à l’événement positif, la probabilité estimée est 18 sur 30, soit 0,60. Le passage de cette probabilité à une classe exige encore un seuil. L’arbre n’impose pas à lui seul la décision.
Les arbres représentent naturellement des coupures et des interactions. Leur lisibilité peut être utile lorsque chaque prédiction doit être reliée à un chemin. Cette lisibilité ne garantit pas la stabilité: une petite modification des données peut changer une coupure précoce et donc une grande partie de l’arbre.
10. Contrôler le surajustement d’un arbre
Un arbre très profond peut créer des feuilles spécifiques à quelques observations. Son erreur d’entraînement diminue généralement, mais sa performance future peut se dégrader.
Trois paramètres contrôlent la complexité:
| Paramètre | Rôle |
|---|---|
tree_depth |
limite la longueur des chemins |
min_n |
exige assez de lignes avant une coupure |
cost_complexity |
pénalise une branche dont le gain est trop faible |
Le compromis biais-variance fournit une lecture utile. Un arbre peu profond peut manquer une structure réelle. Un arbre profond peut suivre le bruit. La validation hors échantillon, et non l’erreur d’entraînement seule, doit soutenir le choix.
11. Comprendre une forêt aléatoire
Une forêt construit de nombreux arbres à partir de perturbations des observations et des prédicteurs. Chaque arbre voit un rééchantillonnage des lignes. À chaque coupure, seulement un sous-ensemble aléatoire de prédicteurs est candidat. Les probabilités des arbres sont ensuite agrégées (L. Breiman, 2001).
Cette diversité réduit souvent la variance d’un arbre unique. Elle rend toutefois l’explication d’une prédiction plus indirecte. La forêt n’est pas une méthode causalement interprétable et son importance de variable ne mesure pas un effet.
Trois paramètres seront fixés:
trees = 200, nombre d’arbres;mtry = 4, prédicteurs candidats à chaque coupure;min_n = 30, taille minimale avant une coupure.
Ajouter des arbres peut stabiliser l’agrégation, mais ne corrige pas une cible mal définie, une fuite ou un protocole qui ne correspond pas à l’usage.
12. Définir les trois candidats
modele_logistique <- logistic_reg() |>
set_engine("glm")
modele_arbre <- decision_tree(
cost_complexity = 0.001,
tree_depth = 10,
min_n = 30
) |>
set_engine("rpart") |>
set_mode("classification")
modele_foret <- rand_forest(
mtry = 4,
min_n = 30,
trees = 200
) |>
set_engine(
"ranger",
num.threads = 2
) |>
set_mode("classification")
modele_foret_final <- rand_forest(
mtry = 4,
min_n = 30,
trees = 200
) |>
set_engine(
"ranger",
importance = "permutation",
num.threads = 2
) |>
set_mode("classification")La régression logistique sert de référence structurée. L’arbre ajoute des coupures et des interactions. La forêt ajoute l’agrégation. Les paramètres sont fixés avant le test. Ce tutoriel compare donc trois candidats définis, pas toutes les versions possibles de ces familles.
Les deux spécifications de forêt construisent le même candidat avec les mêmes paramètres et deux fils d’exécution. modele_foret sert aux cinq validations sans calculer une importance inutilisée. modele_foret_final active l’importance par permutation seulement pour l’ajustement final, où elle sera interprétée.
13. Construire les workflows comparables
modeles <- list(
logistique = modele_logistique,
arbre = modele_arbre,
foret = modele_foret
)
ensemble_workflows <- workflow_set(
preproc = list(pretraitement = recette_ml),
models = modeles
)
ensemble_workflows# A workflow set/tibble: 3 × 4
wflow_id info option result
<chr> <list> <list> <list>
1 pretraitement_logistique <tibble [1 × 4]> <opts[0]> <list [0]>
2 pretraitement_arbre <tibble [1 × 4]> <opts[0]> <list [0]>
3 pretraitement_foret <tibble [1 × 4]> <opts[0]> <list [0]>
Le prétraitement, les prédicteurs et la cible restent identiques. Seule la spécification du modèle change. Cette symétrie permet de relier une différence de performance à la famille et aux paramètres du candidat plutôt qu’à un avantage caché dans les données.
14. Définir les mesures avant les résultats
mesures_ml <- metric_set(
roc_auc,
pr_auc,
brier_class
)L’aire ROC mesure la capacité de classement sur tous les seuils. Elle peut être interprétée comme la probabilité qu’une observation positive choisie au hasard reçoive un score supérieur à une observation négative choisie au hasard, sous les conventions de la mesure. Elle n’est pas une exactitude.
L’aire précision-rappel met l’accent sur la récupération de l’événement positif et la précision des prédictions positives. Sa référence dépend de la fréquence de l’événement. Elle est donc utile lorsque l’événement positif et sa prévalence doivent rester visibles.
Le score de Brier est la moyenne de \((y - \widehat p)^2\) pour l’événement positif. Il vaut 0 pour des probabilités parfaites et augmente lorsque les probabilités s’éloignent des résultats. Il combine des aspects de calibration et de résolution, sans remplacer le graphique de calibration.
15. Évaluer dans les mêmes fenêtres
set.seed(20260828)
resultats_validation <- workflow_map(
ensemble_workflows,
"fit_resamples",
resamples = plis_temporels,
metrics = mesures_ml,
control = control_resamples(
save_pred = TRUE,
save_workflow = TRUE
)
)
tableau_validation <- collect_metrics(resultats_validation) |>
transmute(
modele = recode(
wflow_id,
pretraitement_logistique = "Logistique",
pretraitement_arbre = "Arbre",
pretraitement_foret = "Forêt"
),
mesure = .metric,
moyenne = mean,
erreur_type = std_err
)
tableau_validation# A tibble: 9 × 4
modele mesure moyenne erreur_type
<chr> <chr> <dbl> <dbl>
1 Logistique brier_class 0.214 0.00166
2 Logistique pr_auc 0.663 0.0141
3 Logistique roc_auc 0.715 0.00359
4 Arbre brier_class 0.213 0.00242
5 Arbre pr_auc 0.674 0.0146
6 Arbre roc_auc 0.721 0.00315
7 Forêt brier_class 0.210 0.00336
8 Forêt pr_auc 0.680 0.0133
9 Forêt roc_auc 0.727 0.00724
La forêt est première en moyenne: aire ROC de 0,726, aire précision-rappel de 0,679 et Brier de 0,210. L’arbre suit avec 0,721, 0,674 et 0,213. La logistique obtient 0,715, 0,663 et 0,214.
Ces écarts sont modestes. Une conclusion doit donc considérer la stabilité et le mandat. Les erreurs types résument la variation des cinq estimations, mais ces fenêtres temporelles se chevauchent et ne sont pas indépendantes. Des intervalles calculés mécaniquement avec 1,96 fois l’erreur type seraient des repères descriptifs, pas un test universel de différence entre algorithmes.
16. Examiner la stabilité temporelle
resultats_par_fenetre <- collect_metrics(
resultats_validation,
summarize = FALSE
) |>
transmute(
modele = recode(
wflow_id,
pretraitement_logistique = "Logistique",
pretraitement_arbre = "Arbre",
pretraitement_foret = "Forêt"
),
pli = id,
mesure = .metric,
valeur = .estimate
)
resultats_par_fenetre |>
filter(mesure == "roc_auc") |>
ggplot(aes(pli, valeur, group = modele, color = modele)) +
geom_line(linewidth = 0.9) +
geom_point(size = 2.5) +
labs(
x = "Fenêtre de validation",
y = "Aire ROC",
color = "Modèle"
)stabilite_auc <- resultats_par_fenetre |>
filter(mesure == "roc_auc") |>
group_by(modele) |>
summarise(
minimum = min(valeur),
maximum = max(valeur),
ecart_type = sd(valeur),
.groups = "drop"
)
stabilite_auc# A tibble: 3 × 4
modele minimum maximum ecart_type
<chr> <dbl> <dbl> <dbl>
1 Arbre 0.712 0.731 0.00705
2 Forêt 0.704 0.748 0.0162
3 Logistique 0.705 0.725 0.00803
La forêt varie de 0,704 à 0,748, avec un écart-type d’environ 0,016. L’arbre varie de 0,712 à 0,731, avec un écart-type de 0,007. La logistique varie de 0,705 à 0,725, avec un écart-type de 0,008.
La forêt obtient donc le meilleur résultat moyen, mais la plus grande variation mensuelle dans ce protocole. Ce résultat ne prouve pas que toutes les forêts sont instables. Il décrit ce modèle, ces paramètres, ces données et ces cinq validations.
17. Choisir selon un mandat
Un modèle ne peut être choisi sans définir ce que l’organisation valorise.
| Mandat | Critère prioritaire possible |
|---|---|
| communication scientifique | structure globale et limites explicables |
| tri prédictif expérimental | meilleure performance moyenne avec instabilité documentée |
| règle inspectable | chemin de décision lisible ligne par ligne |
| infrastructure minimale | coût d’ajustement et de surveillance faible |
Avant le test, une recommandation doit citer au moins deux mesures, la stabilité, un coût et un compromis. Plusieurs recommandations peuvent être cohérentes si les mandats diffèrent. Le test ne doit pas servir à inventer le mandat qui favorise le résultat observé.
18. Ajuster sur tout l’entraînement
Après le choix provisoire, nous ajustons les trois pipelines sur janvier à septembre afin de comparer leurs résultats confirmatoires.
modeles_finaux <- list(
logistique = modele_logistique,
arbre = modele_arbre,
foret = modele_foret_final
)
workflows_ajustes <- map(
modeles_finaux,
~ workflow() |>
add_recipe(recette_ml) |>
add_model(.x) |>
fit(data = entrainement_ml)
)Dans une procédure réelle, seuls les modèles nécessaires au plan d’analyse pourraient être ajustés. Nous conservons les trois ici pour comprendre les écarts sans effectuer de nouveau réglage.
19. Ouvrir le test futur une seule fois
predictions_finales <- imap_dfr(
workflows_ajustes,
function(ajustement, nom_modele) {
bind_cols(
test_ml |>
select(date_creation, verite),
predict(
ajustement,
new_data = test_ml,
type = "prob"
)
) |>
mutate(modele = nom_modele)
}
)
resultats_test <- predictions_finales |>
group_by(modele) |>
mesures_ml(
truth = verite,
.pred_non_terminee_7_jours,
event_level = "first"
) |>
ungroup()
resultats_test# A tibble: 9 × 4
modele .metric .estimator .estimate
<chr> <chr> <chr> <dbl>
1 arbre roc_auc binary 0.710
2 foret roc_auc binary 0.729
3 logistique roc_auc binary 0.699
4 arbre pr_auc binary 0.631
5 foret pr_auc binary 0.640
6 logistique pr_auc binary 0.600
7 arbre brier_class binary 0.207
8 foret brier_class binary 0.205
9 logistique brier_class binary 0.212
Sur le test, la forêt obtient une aire ROC de 0,730, une aire précision-rappel de 0,640 et un Brier de 0,205. L’arbre obtient 0,710, 0,631 et 0,207. La logistique obtient 0,699, 0,600 et 0,212.
La forêt reste première sur les trois mesures. La recommandation finale dépend toutefois du mandat écrit avant le test. Si le test conduit à modifier le candidat ou les paramètres, cette nouvelle décision devra être confirmée sur une période future indépendante. Le même test ne peut pas servir à modifier et à confirmer le pipeline.
20. Comparer validation et test
comparaison_validation_test <- bind_rows(
tableau_validation |>
transmute(
modele,
mesure,
ensemble = "Validation",
valeur = moyenne
),
resultats_test |>
transmute(
modele = recode(
modele,
logistique = "Logistique",
arbre = "Arbre",
foret = "Forêt"
),
mesure = .metric,
ensemble = "Test futur",
valeur = .estimate
)
)
comparaison_validation_test |>
ggplot(aes(ensemble, valeur, color = modele, group = modele)) +
geom_line(linewidth = 0.9) +
geom_point(size = 2.5) +
facet_wrap(~ mesure, scales = "free_y") +
labs(x = NULL, y = "Valeur", color = "Modèle")Les aires diminuent davantage pour la logistique et l’arbre que pour la forêt. Le Brier du test est plus faible pour les trois candidats. Cette amélioration ne doit pas être attribuée uniquement au modèle, puisque la proportion de l’événement et la composition des demandes changent aussi.
21. Examiner la calibration
La discrimination et la calibration répondent à des questions différentes. Un modèle peut bien classer les demandes tout en produisant des probabilités trop élevées ou trop faibles.
calibration_modeles <- predictions_finales |>
group_by(modele) |>
mutate(groupe = ntile(.pred_non_terminee_7_jours, 10)) |>
group_by(modele, groupe) |>
summarise(
probabilite_moyenne = mean(.pred_non_terminee_7_jours),
frequence_observee = mean(verite == "non_terminee_7_jours"),
n = n(),
.groups = "drop"
)
ggplot(
calibration_modeles,
aes(probabilite_moyenne, frequence_observee, color = modele)
) +
geom_abline(
slope = 1,
intercept = 0,
linetype = 2,
color = "#52606d"
) +
geom_line() +
geom_point() +
coord_equal(xlim = c(0, 1), ylim = c(0, 1)) +
labs(
x = "Probabilité moyenne prédite",
y = "Fréquence observée",
color = "Modèle"
)La diagonale représente une correspondance parfaite entre probabilité moyenne et fréquence observée dans chaque groupe. Plusieurs points de probabilité élevée se trouvent sous la diagonale, ce qui indique une surestimation locale dans ce test. Les dix groupes rendent le graphique lisible, mais leur choix reste descriptif et dépend de la taille du test.
22. Vérifier manuellement le Brier
verification_brier <- predictions_finales |>
mutate(
indicatrice = as.numeric(verite == "non_terminee_7_jours"),
erreur_carree = (
indicatrice - .pred_non_terminee_7_jours
)^2
) |>
group_by(modele) |>
summarise(brier_manuel = mean(erreur_carree), .groups = "drop")
verification_brier# A tibble: 3 × 2
modele brier_manuel
<chr> <dbl>
1 arbre 0.207
2 foret 0.205
3 logistique 0.212
Les résultats manuels correspondent à brier_class(). Cette vérification est importante parce que la probabilité sélectionnée doit correspondre au niveau défini comme événement. La documentation officielle de yardstick précise cette convention pour les réponses binaires.
23. Visualiser l’arbre final
arbre_final <- extract_fit_engine(workflows_ajustes$arbre)
par(mar = c(1, 1, 2, 1), xpd = TRUE)
plot(arbre_final, uniform = TRUE, margin = 0.08)
text(arbre_final, use.n = TRUE, cex = 0.58)L’arbre permet de suivre une suite de règles. Les nombres dans les feuilles décrivent les observations d’entraînement associées à ces règles. Ils ne prouvent pas qu’une coupure serait stable dans une autre année ou qu’elle représente un mécanisme causal.
L’objet contient 20 coupures avec les paramètres retenus. Une autre pénalité, une autre profondeur ou un autre échantillon pourrait produire une structure différente. La lisibilité doit donc être accompagnée d’une preuve de stabilité.
24. Examiner l’importance de la forêt
importance_foret <- extract_fit_engine(
workflows_ajustes$foret
)$variable.importance |>
enframe(name = "variable", value = "importance") |>
arrange(desc(importance))
importance_foret# A tibble: 6 × 2
variable importance
<chr> <dbl>
1 activite 0.0751
2 arrondissement 0.0565
3 provenance 0.0126
4 type_lieu 0.00451
5 plage_horaire 0.00267
6 jour_semaine 0.00195
importance_foret |>
ggplot(aes(importance, fct_reorder(variable, importance))) +
geom_col(fill = "#0f7c80") +
labs(x = "Importance par permutation", y = NULL)L’activité et l’arrondissement produisent les plus grandes pertes de performance lorsqu’ils sont permutés. L’importance peut être partagée entre des prédicteurs redondants ou corrélés et dépend de la mesure et du modèle. Elle ne donne pas le sens d’une relation, ne mesure pas un effet causal et ne hiérarchise pas la valeur des services ou des territoires.
25. Vérifier les performances par période et sous-groupe
Une moyenne globale peut cacher une dégradation localisée. Avant tout usage réel, il faudrait examiner les volumes, la fréquence de la cible, les mesures et la calibration selon des regroupements définis à l’avance.
Exemple de préparation d’un audit mensuel pour la forêt:
predictions_foret <- predictions_finales |>
filter(modele == "foret") |>
mutate(mois = floor_date(date_creation, unit = "month"))
audit_mensuel_foret <- predictions_foret |>
group_by(mois) |>
summarise(
n = n(),
frequence_observee = mean(verite == "non_terminee_7_jours"),
probabilite_moyenne = mean(.pred_non_terminee_7_jours),
brier = mean(
(
as.numeric(verite == "non_terminee_7_jours") -
.pred_non_terminee_7_jours
)^2
),
.groups = "drop"
)
audit_mensuel_foret# A tibble: 3 × 5
mois n frequence_observee probabilite_moyenne brier
<date> <int> <dbl> <dbl> <dbl>
1 2024-10-01 1471 0.405 0.445 0.208
2 2024-11-01 1238 0.397 0.426 0.199
3 2024-12-01 940 0.382 0.420 0.208
Une ventilation par arrondissement ou activité demanderait de conserver ces colonnes lors de la construction des prédictions. Les groupes de faible taille devraient être signalés. Un écart descriptif n’établit pas une cause ou une discrimination institutionnelle, mais peut déclencher une investigation.
26. Construire une fiche de modèle
Une fiche concise doit contenir:
- la question, l’unité et la population cible;
- le moment de prédiction et les variables interdites;
- les périodes d’apprentissage, de validation et de test;
- le prétraitement, les candidats et les paramètres;
- les mesures moyennes et leur stabilité;
- la décision selon le mandat et le compromis accepté;
- les limites, la surveillance et la condition de retrait.
Une recommandation qui utilise le test pour changer de modèle doit conserver la trace de la décision initiale. Le candidat révisé n’est pas confirmé par ce même test.
27. Définir une surveillance et une condition de retrait
La surveillance peut être organisée en quatre niveaux:
| Niveau | Exemples |
|---|---|
| données | volume, catégories nouvelles, valeurs manquantes |
| cible | fréquence non terminée, définition du statut |
| prédictions | distribution des scores, groupes extrêmes |
| performance | aire ROC, aire précision-rappel, Brier, calibration |
Une règle de retrait doit contenir une mesure, une période, un seuil et une action. Par exemple:
Si le Brier mensuel dépasse 0,24 pendant deux mois consécutifs, suspendre les scores et réexaminer les données, la cible et la calibration.
Le seuil 0,24 est un exemple pédagogique, pas une norme universelle. Dans un projet réel, il devrait être relié à une référence, aux conséquences des erreurs et au mandat.
28. Conditions de validité et limites
Les conclusions restent conditionnelles à plusieurs éléments:
- l’échantillon de demandes créé en 2024;
- la définition pédagogique de la cible;
- la disponibilité correcte des prédicteurs à la création;
- la stabilité du processus de collecte et de statut;
- la pertinence des fenêtres de quatre mois pour l’usage visé;
- le nombre limité de validations mensuelles;
- les paramètres fixés des trois candidats;
- l’absence d’une analyse complète par sous-groupe;
- l’absence de conclusion causale.
Une performance future observée en 2024 ne garantit pas une performance identique en 2025 ou 2026. Le pipeline doit être réévalué si les catégories, les processus, la cible ou le mandat changent.
29. Extensions possibles
Une suite plus avancée pourrait:
- régler
cost_complexity,tree_depth,mtryetmin_nà l’intérieur des fenêtres; - comparer des fenêtres mobiles et expansives;
- utiliser une évaluation imbriquée pour séparer réglage et comparaison;
- ajouter des courbes précision-rappel et des intervalles par rééchantillonnage;
- recalibrer un candidat à partir de données réservées;
- étudier la dérive des prédicteurs;
- construire un plan de surveillance par sous-groupe;
- comparer le gain prédictif à un modèle de référence simple.
Ces extensions exigent un nouveau budget de données. Le test d’octobre à décembre ne doit pas devenir une ressource illimitée de réglage.
Lectures complémentaires
| Lecture | Utilité |
|---|---|
| Breiman, Random Forests | construction et motivation originale des forêts aléatoires |
| Tidy Modeling with R, rééchantillonnage | rôle du test, analyses et évaluations répétées |
Documentation de sliding_period() |
fenêtres basées sur une période et signification de lookback |
Documentation de decision_tree() |
profondeur, taille minimale et pénalité de complexité |
Documentation de rand_forest() |
nombre d’arbres, mtry et min_n |
Documentation de brier_class() |
orientation de l’événement et interprétation du score |
Documentation de step_novel() |
traitement des catégories futures inconnues |
Documentation de step_unknown() |
traitement explicite des catégories manquantes |
Documentation de step_other() |
regroupement des catégories rares |
| Données ouvertes 311 de Montréal | provenance et documentation de la source |
Conclusion
Le résultat principal n’est pas qu’une forêt obtient ici la meilleure moyenne et reste première sur le test. La compétence transférable est le protocole: définir le moment de prédiction, protéger le futur, appliquer les mêmes règles, examiner moyenne et stabilité, puis relier le choix à un mandat et à une surveillance.
Un modèle défendable n’est pas seulement un objet ajusté. C’est une décision documentée dont les données, les limites, les conditions d’usage et les raisons de retrait sont explicites.