| Ensemble | Maisons | Voisinages |
|---|---|---|
| Apprentissage | 500 | 269 |
| Test | 100 | 67 |
Prédire une valeur foncière et valider par voisinage
Un modèle plus élaboré prédit-il mieux la valeur évaluée sur des voisinages réservés au test ?
À produire : Des scores MAE et RMSE en dollars sur le test, un tableau de coefficients, deux graphiques de diagnostic et une conclusion comparant les modèles aux références constantes.
Prérequis : Utiliser lm et predict dans R. Comprendre logarithmes, résidus et séparation apprentissage-test.
Préparation enseignante
Extraire la trousse, ouvrir Donnees-bleues.Rproj et installer les packages avant la séance avec installer-packages.R. Exécuter ensuite datasets/maisons-quebec/activite-modeliser.R. Le script fonctionne hors ligne avec le CSV fourni. La durée de 120 minutes est indicative; la validation croisée est facultative. L’activité n’a pas été testée en classe.
Télécharger la trousse avec les données
30 Ko. Préparation : 01/10/2026 à 14:20 (heure du Québec).
Les fichiers de données sont inclus dans la trousse.
Conditions de la source · Provenance et empreinte du ZIP
Fichiers inclus
-
maisons_quebec.csv: 600 lignes, 19 colonnes.
Contexte
La fiche décrit 600 maisons à Québec et une cible continue : la valeur foncière évaluée au rôle 2025, avec référence au marché du 1er juillet 2023. Il n’y a aucun prix de vente. L’aire d’étages est brute, et les données sont observationnelles.
Les valeurs terrain et bâtiment sont conservées pour faire reconnaître la fuite de cible : leur somme donne exactement le total. Elles sont exclues des formules de prédiction.
Consignes
- Vérifier les cas utilisables et la séparation des codes de voisinage. Un code doit se trouver dans un seul ensemble.
- Ajuster les deux modèles fixés et calculer les références constantes dans l’apprentissage uniquement.
- Comparer MAE et RMSE sur les mêmes maisons de test, en dollars, puis lire les diagnostics dans l’apprentissage.
- Répondre aux six exercices. Ne pas modifier les formules après observation du test pour améliorer son résultat.
Résultats attendus
Les 600 cas utilisables sont séparés par voisinage, avec la graine 20261002. On réserve environ 20 % des codes, ce qui ne garantit pas 20 % des lignes. Aucun code n’est commun aux deux ensembles.
Deux modèles prédéfinis
Le modèle simple utilise l’aire d’étages sur l’échelle brute. Le modèle multiple utilise le logarithme de la valeur, les logarithmes des deux superficies, l’année de construction centrée en 1970 et le type de maison. Les deux modalités en rangée sont regroupées selon leur définition, sans consulter leurs valeurs.
valeur_fonciere_cad ~ aire_etages_m2
log(valeur_fonciere_cad) ~ log(aire_etages_m2) +
log(superficie_terrain_m2) + annee_centree + type_maison| Terme du modèle logarithmique | Estimation |
|---|---|
| (Intercept) | 9.4718 |
| log(aire_etages_m2) | 0.7043 |
| log(superficie_terrain_m2) | 0.0117 |
| annee_centree | 0.0028 |
| type_maisonJumelé | -0.1772 |
| type_maisonEn rangée | -0.0995 |
Dans ce modèle, doubler l’aire d’étages multiplie la valeur ajustée par 1.629, à caractéristiques restantes fixées. Ce contraste décrit le modèle ajusté et ne représente pas l’effet causal d’un agrandissement. La catégorie de référence est « Détaché ».
Validation en dollars
La médiane constante fournit une référence pour l’erreur absolue; la moyenne constante fournit une référence pour l’erreur quadratique. Toutes deux proviennent de l’apprentissage.
Pour revenir de l’échelle logarithmique aux dollars, le script multiplie exp(prédiction_log) par la moyenne de exp(résidu) dans l’apprentissage (1.0153). Cette correction de retransformation suit Duan (1983). Ce facteur global vise une moyenne; il ne garantit pas une moyenne conditionnelle correcte lorsque la dispersion change selon les caractéristiques.
| Modèle | Maisons de test | MAE (CAD) | RMSE (CAD) | Biais (CAD) |
|---|---|---|---|---|
| reference_mediane | 100 | 150 660 | 256 447 | -100 440 |
| reference_moyenne | 100 | 151 845 | 244 971 | -65 832 |
| regression_simple | 100 | 82 838 | 112 686 | -20 997 |
| regression_log | 100 | 90 690 | 148 110 | -40 717 |
Le modèle simple a ici une MAE et une RMSE plus faibles que le modèle multiple logarithmique. La complexité et la transformation ne garantissent pas une amélioration. Cette comparaison concerne une séparation prédéfinie de cet instantané, sans estimation de l’incertitude de l’écart de performance.
Le biais est calculé comme prédiction moins valeur observée. Un biais négatif correspond à une sous-estimation moyenne dans cet ensemble de test.
Diagnostics dans l’apprentissage
| ajuste_log | residu_log | levier | distance_cook | maison_id |
|---|---|---|---|---|
| 12.4199 | 0.4844 | 0.1074 | 0.1725 | MQ-0394 |
| 12.9138 | -0.7803 | 0.0455 | 0.1661 | MQ-0078 |
| 13.3249 | 0.5489 | 0.0612 | 0.1142 | MQ-0105 |
| 12.6342 | -0.5060 | 0.0497 | 0.0768 | MQ-0361 |
| 13.6396 | 0.4070 | 0.0708 | 0.0741 | MQ-0537 |
Les distances de Cook indiquent des observations à examiner, pas à supprimer automatiquement. Vérifier leur contexte, les unités et la plausibilité avant toute décision. Une analyse de sensibilité éventuelle doit rester dans l’apprentissage.
Prolongement facultatif : validation croisée
Cinq plis regroupent les voisinages dans les 500 maisons d’apprentissage. Pour chaque pli, la référence et la correction de retransformation sont réestimées sur les quatre autres plis. Le test réservé n’y participe pas.
| Modèle | Prédictions hors pli | MAE (CAD) | RMSE (CAD) |
|---|---|---|---|
| reference_mediane | 500 | 89 856 | 143 606 |
| prediction_cad | 500 | 54 342 | 83 635 |
Ces scores portent sur d’autres observations que le test final. Leur différence ne mesure pas, à elle seule, un biais d’optimisme. Examiner les distributions et la variabilité des voisinages avant de proposer une explication.
Six exercices et corrigé
- Pourquoi les codes de voisinage sont-ils importés comme texte ? Ils sont des identifiants, sans signification arithmétique; préserver leur écriture facilite le regroupement.
- Pourquoi séparer par voisinage ? Pour éviter de retrouver le même groupe administratif des deux côtés. Cela ne prouve pas l’indépendance entre voisinages voisins.
- Pourquoi ne pas prédire avec les deux valeurs composantes ? Elles donnent le total par addition, ce qui constitue une fuite de cible.
- Identifier le modèle ayant les erreurs les plus faibles sur le test. Ici, le modèle simple parmi les deux régressions. Citer les deux indicateurs en dollars et conserver le résultat défavorable au modèle plus élaboré.
- Peut-on calculer la médiane de référence ou la retransformation avec les 600 maisons ? Non : cela utilise les réponses du test. Les estimer dans l’apprentissage seulement, et dans chaque pli d’apprentissage pour la validation croisée.
- Peut-on vendre une maison au prix prédit ou conclure que l’agrandir augmente sa valeur de la quantité ajustée ? Aucun de ces résultats n’est établi. Les prédictions portent sur des évaluations au rôle, sans validation des ventes ni identification causale.
Limites à faire nommer
- Test interne à un seul instantané et une seule municipalité, sans validation temporelle.
- Dépendance spatiale possible malgré l’absence de codes communs.
- Variables de rénovations, état du bâtiment et transactions absentes du jeu.
- Pas d’intervalle de performance ni de prix de vente prédit.
- Les modèles restent fixés après consultation du test; une amélioration ultérieure demanderait une nouvelle évaluation indépendante.
Code complet et source
Afficher le script R
# Régression et validation par voisinage, 120 minutes indicatives.
# Ouvrir le projet RStudio de la trousse, puis cliquer Source.
# Cible : valeur au rôle municipal, pas prix de vente. Données MAMH, CC BY 4.0.
library(readr)
library(dplyr)
library(ggplot2)
maisons <- read_csv("data/processed/maisons-quebec/maisons_quebec.csv",
col_types = cols(maison_id = col_character(), arrondissement_code = col_character(),
voisinage_code = col_character(), lien_physique_code = col_character(),
genre_construction_code = col_character()), show_col_types = FALSE)
stopifnot(nrow(maisons) == 600L, !anyDuplicated(maisons$maison_id))
dir.create("outputs", showWarnings = FALSE)
# 1. Prétraitement fixé avant d'observer les résultats du test.
# Regroupement des deux modalités en rangée selon leur définition, pas selon leur valeur.
# Les lignes incomplètes sont comptées; aucune imputation globale avant la séparation.
analyse <- maisons |>
mutate(type_maison = factor(case_when(lien_physique_code == "1" ~ "Détaché",
lien_physique_code == "2" ~ "Jumelé", lien_physique_code %in% c("3", "4") ~ "En rangée"),
levels = c("Détaché", "Jumelé", "En rangée")),
annee_centree = annee_construction - 1970) |>
filter(is.finite(valeur_fonciere_cad), valeur_fonciere_cad > 0,
is.finite(aire_etages_m2), aire_etages_m2 > 0,
is.finite(superficie_terrain_m2), superficie_terrain_m2 > 0,
is.finite(annee_centree), !is.na(type_maison), !is.na(voisinage_code))
effectifs <- tibble(etape = c("CSV", "Cas utilisables", "Cas écartés pour l'activité"),
n = c(nrow(maisons), nrow(analyse), nrow(maisons) - nrow(analyse)))
stopifnot(nrow(analyse) > 100L)
# 2. Test réservé : environ 20 % des codes de voisinage, jamais utilisés pour choisir le modèle.
# Les codes administratifs ne sont pas une garantie d'indépendance spatiale.
RNGkind(kind = "Mersenne-Twister", normal.kind = "Inversion", sample.kind = "Rejection")
set.seed(20261002)
groupes <- sort(unique(analyse$voisinage_code))
stopifnot(length(groupes) >= 10L)
groupes_test <- sample(groupes, max(1L, floor(0.20 * length(groupes))), replace = FALSE)
apprentissage <- analyse |> filter(!voisinage_code %in% groupes_test)
test <- analyse |> filter(voisinage_code %in% groupes_test)
stopifnot(!length(intersect(apprentissage$voisinage_code, test$voisinage_code)),
all(levels(analyse$type_maison) %in% as.character(apprentissage$type_maison)))
partage <- analyse |> transmute(maison_id, voisinage_code,
ensemble = if_else(voisinage_code %in% groupes_test, "Test", "Apprentissage"))
partage_resume <- partage |> group_by(ensemble) |>
summarise(maisons = n(), voisinages = n_distinct(voisinage_code), .groups = "drop")
# 3. Modèles fixés, uniquement des caractéristiques physiques.
# Interdiction de prédire le total avec ses composantes terrain + bâtiment.
formule_simple <- valeur_fonciere_cad ~ aire_etages_m2
formule_log <- log(valeur_fonciere_cad) ~ log(aire_etages_m2) +
log(superficie_terrain_m2) + annee_centree + type_maison
modele_simple <- lm(formule_simple, data = apprentissage)
modele_log <- lm(formule_log, data = apprentissage)
# Correction de retransformation estimée seulement dans l'apprentissage.
# Elle vise une moyenne en dollars; elle n'assure pas une moyenne conditionnelle exacte.
smearing <- mean(exp(residuals(modele_log)))
predictions <- test |> transmute(maison_id, voisinage_code, observe_cad = valeur_fonciere_cad,
reference_mediane = median(apprentissage$valeur_fonciere_cad),
reference_moyenne = mean(apprentissage$valeur_fonciere_cad),
regression_simple = as.numeric(predict(modele_simple, newdata = test)),
regression_log = exp(as.numeric(predict(modele_log, newdata = test))) * smearing)
model_names <- c("reference_mediane", "reference_moyenne", "regression_simple", "regression_log")
scores <- bind_rows(lapply(model_names, function(name) {
error <- predictions[[name]] - predictions$observe_cad
tibble(modele = name, n_test = nrow(test), mae_cad = mean(abs(error)),
rmse_cad = sqrt(mean(error^2)), biais_cad = mean(error))
}))
# 4. Validation croisée facultative, uniquement dans l'apprentissage (5 plis par voisinage).
train_groups <- sort(unique(apprentissage$voisinage_code))
fold_ids <- sample(rep(1:5, length.out = length(train_groups)))
plis <- tibble(voisinage_code = train_groups, pli = fold_ids)
train_cv <- apprentissage |> left_join(plis, by = "voisinage_code")
cv_predictions <- bind_rows(lapply(1:5, function(k) {
fitting <- train_cv |> filter(pli != k)
held_out <- train_cv |> filter(pli == k)
stopifnot(!length(intersect(fitting$voisinage_code, held_out$voisinage_code)),
all(levels(analyse$type_maison) %in% as.character(fitting$type_maison)))
m <- lm(formule_log, data = fitting)
retransformation <- mean(exp(residuals(m)))
held_out |> transmute(maison_id, pli, observe_cad = valeur_fonciere_cad,
prediction_cad = exp(as.numeric(predict(m, newdata = held_out))) * retransformation,
reference_mediane = median(fitting$valeur_fonciere_cad))
}))
stopifnot(nrow(cv_predictions) == nrow(apprentissage),
!anyDuplicated(cv_predictions$maison_id), all(is.finite(cv_predictions$prediction_cad)))
cv_scores <- bind_rows(lapply(c("reference_mediane", "prediction_cad"), function(name) {
error <- cv_predictions[[name]] - cv_predictions$observe_cad
tibble(modele = name, n = nrow(cv_predictions), mae_cad = mean(abs(error)),
rmse_cad = sqrt(mean(error^2)))
}))
coefficients <- tibble(terme = names(coef(modele_log)), estimation = unname(coef(modele_log)))
diagnostic <- tibble(ajuste_log = fitted(modele_log), residu_log = residuals(modele_log),
levier = hatvalues(modele_log), distance_cook = cooks.distance(modele_log),
maison_id = apprentissage$maison_id)
influence <- diagnostic |> arrange(desc(distance_cook)) |> slice_head(n = 5)
residual_plot <- ggplot(diagnostic, aes(x = ajuste_log, y = residu_log)) +
geom_point(alpha = 0.55, colour = "#185b83") + geom_hline(yintercept = 0, colour = "#b95319") +
labs(x = "Log de la valeur ajustée", y = "Résidu sur l'échelle logarithmique",
title = "Diagnostics dans l'apprentissage") + theme_minimal(base_size = 12)
prediction_plot <- ggplot(predictions, aes(x = observe_cad, y = regression_log)) +
geom_abline(slope = 1, intercept = 0, colour = "#b95319") +
geom_point(alpha = 0.6, colour = "#185b83") +
scale_x_continuous(labels = scales::label_number(big.mark = " ")) +
scale_y_continuous(labels = scales::label_number(big.mark = " ")) +
coord_equal() + labs(x = "Valeur foncière observée (CAD)",
y = "Valeur prédite (CAD)", title = "Voisinages réservés au test") + theme_minimal(base_size = 12)
# 5. Résultats reproductibles, pas de conclusion causale ni de prévision des ventes futures.
print(effectifs); print(partage_resume); print(coefficients); print(scores); print(cv_scores)
print(influence); print(residual_plot); print(prediction_plot)
write_csv(effectifs, "outputs/maisons-effectifs-modele.csv")
write_csv(partage, "outputs/maisons-partage.csv")
write_csv(scores, "outputs/maisons-scores-test.csv")
write_csv(cv_scores, "outputs/maisons-scores-cv.csv")
write_csv(cv_predictions, "outputs/maisons-predictions-cv.csv")
write_csv(predictions, "outputs/maisons-predictions-test.csv")
write_csv(coefficients, "outputs/maisons-coefficients.csv")
write_csv(influence, "outputs/maisons-influence.csv")
ggsave("outputs/maisons-residus.png", residual_plot, width = 8, height = 5, dpi = 160)
ggsave("outputs/maisons-test.png", prediction_plot, width = 7, height = 6, dpi = 160)
capture.output(sessionInfo(), file = "outputs/sessionInfo.txt")
message("Validation de valeurs foncières dans l'instantané. Aucune mesure de performance sur les ventes.")MAMH (2026), rôles d’évaluation foncière, extraction de Québec, CC BY 4.0. Sélection et activités par Aurélien Nicosia (2026). Les notices incluses documentent les définitions et le périmètre.
Vérifier le travail
- Aucun code de voisinage n’apparaît dans l’apprentissage et le test.
- Les valeurs du terrain et du bâtiment ne sont pas des prédicteurs du total.
- Les scores comparent les mêmes 100 maisons de test en dollars.
- La comparaison conserve le résultat défavorable au modèle multiple logarithmique.
- La conclusion distingue valeurs au rôle, ventes et causalité.
Objectifs et adaptations
Objectifs
- Réserver des groupes entiers pour évaluer une prédiction.
- Ajuster une régression simple et une régression multiple sur l’échelle logarithmique.
- Estimer les références et la retransformation uniquement dans l’apprentissage.
- Repérer une fuite de cible entre valeur totale et composantes.
- Interpréter les résidus et les erreurs sans conclusion causale.
Adaptations
- Limiter la séance à la régression simple et au test réservé.
- Approfondir avec les cinq plis groupés dans l’apprentissage, sans retoucher le modèle après observation du test.
Statut documenté : Script exécuté; validation interne à l’instantané, efficacité en classe non évaluée.
Contribution pédagogique : Aurélien Nicosia. Usage en cours non documenté.
- Ajout
- Mise à jour