Dynamiques de marché 2016-2024 · Communes >20k hab · DVF + SITADEL + RP
Auteur·rice
Vincent R.
Date de publication
7 avril 2026
1. Données — Pool communes >20k
Source unique dbcln-kpi-commarm.csv (PTB, 409 colonnes). Critères d’inclusion : population ≥20 000 hab. (RP 2022), données DVF complètes sur les trois périodes (TCAM prix 16-22, 22-24, transactions 22-24), et au moins 50 mutations DVF en 2024 pour garantir la fiabilité des médianes.
Code
comm <-read.csv(file.path(PROJECT_ROOT, "data/ptb-output/dbcln-kpi-commarm.csv"),stringsAsFactors =FALSE, fileEncoding ="UTF-8-BOM")SEUIL_POP <-20000SEUIL_TRANS <-50df <- comm %>%filter(P22_POP >= SEUIL_POP,!is.na(logd_px2_glb_vtcam_1622),!is.na(logd_px2_glb_vtcam_2224),!is.na(logd_trans_tx1klog_2224), logd_trans_24 >= SEUIL_TRANS)cat(sprintf("Pool : %d communes (>%dk, DVF complet)\n", nrow(df), SEUIL_POP/1000))
Pool : 479 communes (>20k, DVF complet)
Code
log_step("POOL", n =nrow(df), seuil_pop = SEUIL_POP, n_total =nrow(comm))
2. Variables — 5 sets testés
Question centrale : quels profils de dynamique du marché du logement distinguent les communes françaises depuis 2016 ? On cherche à identifier des trajectoires-types croisant l’évolution des prix, l’effort de construction, la liquidité du marché (transactions) et l’attractivité résidentielle (démographie, vacance). Cinq combinaisons de variables sont testées, du noyau structurel (7 variables marché pur) au set enrichi (13 variables incluant revenus et rotation du parc).
Code
# SET A — Core 11 vars (spec originale prix + marché + parc + démo)SET_A <-c("logd_px2q2_glb_24", # Prix niveau"logd_px2_glb_vtcam_1622", # TCAM prix boom"logd_px2_glb_vtcam_2224", # TCAM prix correction"logs_logaut_tx1klog16_1622", # Construction boom"logs_logaut_tx1klog22_2225", # Construction correction"logd_trans_tx1klog_1621", # Transactions boom"logd_trans_tx1klog_2224", # Transactions correction"log_vac_pct_22", # Vacance"log_vac_vdifp_1622", # Vacance évol"log_ressec_pct_22", # Rés secondaires"dm_pop_vtcam_1622"# Démo)# SET B — Dynamique pure 9 vars (sans niveaux, focus trajectoires)SET_B <-c("logd_px2_glb_vtcam_1622", "logd_px2_glb_vtcam_2224","logs_logaut_tx1klog16_1622", "logs_logaut_tx1klog22_2225","logd_trans_tx1klog_1621", "logd_trans_tx1klog_2224","log_vac_vdifp_1622", "dm_pop_vtcam_1622","log_emmenrec_vdifp_1622")# SET C — Enrichi 13 vars (core + revenus + rotation parc)SET_C <-c(SET_A, "rev_med_21", "log_emmenrec_pct_22")# SET D — Attractivité 8 vars (prix + démo + revenus + transactions)SET_D <-c("logd_px2q2_glb_24", "logd_px2_glb_vtcam_2224","dm_pop_vtcam_1622", "dm_sma_vtcam_1622","rev_med_21", "logd_trans_tx1klog_2224","log_vac_pct_22", "log_ressec_pct_22")# SET E — Marché pur 7 vars (prix + construction + transactions, pas de parc)SET_E <-c("logd_px2q2_glb_24","logd_px2_glb_vtcam_1622", "logd_px2_glb_vtcam_2224","logs_logaut_tx1klog16_1622", "logs_logaut_tx1klog22_2225","logd_trans_tx1klog_1621", "logd_trans_tx1klog_2224")sets <-list(A = SET_A, B = SET_B, C = SET_C, D = SET_D, E = SET_E)cat(paste(sprintf("SET %s: %d vars", names(sets), sapply(sets, length)), collapse =" | "), "\n")
SET A: 11 vars | SET B: 9 vars | SET C: 13 vars | SET D: 8 vars | SET E: 7 vars
3. Corrélations
Seuil de retrait : si une paire dépasse |r| > 0.80, la variable la moins interprétable est candidate à l’exclusion. Paires à surveiller : transactions boom × transactions correction (inertie), prix niveau × TCAM prix (niveau vs dynamique).
Code
# SET A (le plus riche)df_a <- df %>%select(all_of(SET_A)) %>%na.omit()rownames(df_a) <- df$libelle[complete.cases(df[, SET_A])]cor_a <-cor(df_a, use ="pairwise.complete.obs")corrplot(cor_a, method ="color", type ="upper", tl.cex =0.6,addCoef.col ="black", number.cex =0.5,title ="Matrice corrélation — SET A (11 vars)", mar =c(0,0,2,0))
Code
high <-which(abs(cor_a) >0.7&upper.tri(cor_a), arr.ind =TRUE)if (nrow(high) >0) {cat("Paires corrélées >0.7 :\n")for (i inseq_len(nrow(high)))cat(sprintf(" %s × %s = %.2f\n", SET_A[high[i,1]], SET_A[high[i,2]], cor_a[high[i,1], high[i,2]]))} elsecat("✅ Aucune paire > 0.7\n")
Pour chaque set de variables, quatre valeurs de k sont testées : choix automatique (critère du saut d’inertie), k=3, k=4, k=5. La silhouette moyenne et la taille du plus petit cluster sont les deux critères de sélection. Seuil silhouette ≥0.25 = structure raisonnable. Pas de cluster <15 villes (sinon artefact).
Grille HCPC — 5 sets × 4 valeurs de k (trié par silhouette)
Vert ≥0.25, jaune ≥0.15, rouge <0.15. Min = plus petit cluster.
6. Meilleur résultat — Diagnostic complet
Le meilleur compromis silhouette × min_cluster est retenu et diagnostiqué en détail : biplot (flèches variables + individus colorés par cluster), carte factorielle axes 1-2 et 1-3, dendrogramme (hauteur de coupe), silhouette par cluster (individus mal classés).
Code
# Meilleure silhouette avec min_cluster >= 15valid <- grid_all %>%filter(min_cluster >=15) %>%slice_max(silhouette, n =1)best_set <-sub(" .*", "", valid$set[1])best_k <- valid$k_reel[1]cat(sprintf(">>> Retenu : SET %s, k=%d, sil=%.3f\n", best_set, best_k, valid$silhouette[1]))
Extraction des résultats via les helpers jcn-hcpc-typo.R. Le profil moyen (typo_profil_rt) montre les écarts par cluster vs la moyenne globale. Les v.test (typo_desc) identifient les variables significativement sur- ou sous-représentées dans chaque cluster (seuil p < 0.05). Les parangons sont les individus les plus proches du centroïde de leur cluster.
Table 3: Top 7 variables caractéristiques par cluster (p < 0.05)
cluster
variable
v.test
mean_clust
mean_global
1
logd_px2q2_glb_24
16.0
7377.80
3595.35
1
rev_med_21
15.1
31927.23
23245.09
1
dm_sma_vtcam_1622
-7.6
-0.83
-0.04
1
logd_px2_glb_vtcam_2224
-6.7
-5.85
-3.23
1
dm_pop_vtcam_1622
-6.7
-0.22
0.42
1
log_ressec_pct_22
2.6
6.48
4.37
1
log_vac_pct_22
2.3
8.31
7.49
2
logd_px2q2_glb_24
-14.5
2866.27
3595.35
2
rev_med_21
-12.6
21705.38
23245.09
2
log_ressec_pct_22
-11.7
2.33
4.37
2
logd_trans_tx1klog_2224
-3.6
23.06
23.49
3
log_ressec_pct_22
14.1
19.15
4.37
3
dm_sma_vtcam_1622
11.0
1.44
-0.04
3
logd_px2_glb_vtcam_2224
7.3
0.40
-3.23
3
logd_trans_tx1klog_2224
7.1
28.67
23.49
3
dm_pop_vtcam_1622
7.0
1.28
0.42
3
log_vac_pct_22
-4.7
5.41
7.49
3
logd_px2q2_glb_24
2.0
4199.22
3595.35
7.3 Parangons (5 par cluster)
Code
parangons <-typo_parangons(hcpc_best, n =5)parangons %>%filter(type =="paragon") %>% knitr::kable(digits =2, caption ="Individus les plus typiques par cluster")
L’Adjusted Rand Index (ARI) mesure la concordance entre deux partitions. ARI=1 signifie que les deux classifications sont identiques, ARI≈0 signifie une concordance aléatoire. Si les clusters sont stables entre sets de variables différents (ARI > 0.65), la structure identifiée est robuste. Si instable (ARI < 0.4), le résultat dépend fortement du choix de variables.
Code
# Individus communs aux 5 setsall_rownames <-lapply(res, function(r) rownames(r$df))common_ind <-Reduce(intersect, all_rownames)cat(sprintf("Individus communs aux 5 sets : %d\n", length(common_ind)))
Individus communs aux 5 sets : 436
Code
# Re-run HCPC pour chaque set, extraire clusters sur individus communsget_cl <-function(r, k, common) { hc <-tryCatch(HCPC(r$pca, nb.clust = k, graph =FALSE), error =function(e) NULL)if (is.null(hc)) return(NULL) cl <-as.character(hc$data.clust$clust)names(cl) <-rownames(hc$data.clust) cl[intersect(names(cl), common)]}cls <-lapply(res, get_cl, k = best_k, common = common_ind)# Matrice ARIset_names <-names(cls)ari_mat <-matrix(NA, length(set_names), length(set_names),dimnames =list(set_names, set_names))for (i inseq_along(set_names)) {for (j inseq_along(set_names)) {if (i <= j &&!is.null(cls[[i]]) &&!is.null(cls[[j]])) { ari_mat[i, j] <-round(adjustedRandIndex(cls[[i]], cls[[j]]), 3) ari_mat[j, i] <- ari_mat[i, j] } }}cat(sprintf("\nMatrice ARI (k=%d) :\n", best_k))
Matrice ARI (k=3) :
Code
print(ari_mat)
A B C D E
A 1.00 0.115 0.802 0.168 0.160
B 0.12 1.000 0.081 0.018 0.439
C 0.80 0.081 1.000 0.164 0.139
D 0.17 0.018 0.164 1.000 0.003
E 0.16 0.439 0.139 0.003 1.000