Logement — rapport national et fiche Seine-Saint-Denis

Cadrage France métropolitaine, puis le marché du logement en Seine-Saint-Denis

Rapport
Auteur·rice

Vincent Roue

Date de publication

Édition septembre 2026

En synthèse

Le marché résidentiel français a changé de régime en 2022. Le prix médian s’établit à 2 616 € le m² et recule de 3 % en trois ans, mais l’ajustement passe d’abord par les volumes : les ventes tombent à 782 k mutations, en recul de 21 %, soit plusieurs fois plus vite que les valeurs — un marché se bloque avant de se déprécier. La correction est proportionnelle à la tension : elle frappe les marchés les plus chers quand le rural tient ses prix. Et l’offre neuve amplifie le mouvement au lieu de l’amortir — 348 k logements autorisés en 2025, 21 % sous 2022, malgré un rebond de 15 % sur la dernière année qui part d’un point bas historique.

L’étude s’appuie sur les mutations DV3F 2006-2025, le recensement 2023, les DPE ADEME et Sitadel 3, lus à quatre échelles emboîtées — France, type de territoire, 101 départements et 306 zones d’emploi, 287 grandes communes.

Ce que le rapport interroge

  • Transactions — le marché s’est-il grippé, et partout de la même façon ?
  • Prix — la hausse est-elle enrayée, et la hiérarchie des territoires tient-elle ?
  • Parc et énergie — quel état du bâti, et quelle exposition au calendrier DPE ?
  • Construction — le robinet de l’offre neuve s’est-il refermé durablement ?
  • Retournement — qui décroche depuis 2022, qui résiste, et pourquoi ?

Ce que montre l’analyse

  • Le retournement est synchrone — commune-centre, couronne et rural décrochent ensemble, ce qui signe une cause macro-financière et non un phénomène local.
  • La hiérarchie se rebat — les marchés les plus tendus corrigent le plus, les moins chers tiennent : la décennie de hausse avait creusé des écarts que la correction referme en partie.
  • Le rural tient ses prix, mais pas son parc — 19,6 % de passoires énergétiques hors des aires d’attraction contre 7,3 % dans les communes-centres : le calendrier d’interdiction de location frappera là où le marché est déjà le moins liquide.
  • L’offre future se contracte au moment où la demande solvable revient — c’est ce décalage qui décidera du prochain cycle.

Synthèse nationale

Afficher le code
# Encadré de messages-clés, modèle psect (`.encadre-synth` du thème canonical). Il REMPLACE les
# `.insight` + puces `.cld-add` : mêmes chiffres, mais rassemblés en un bloc de tête lisible seul.
.rur <- TYPO_LOG$logd_dpe_fg_pct_26[TYPO_LOG$type_lib == "Hors attraction (rural)"]
.cen <- TYPO_LOG$logd_dpe_fg_pct_26[TYPO_LOG$type_lib == "Commune-centre"]
cat("::: {.encadre-synth}
[Ce qu'il faut retenir]{.encadre-synth-titre}

")

Ce qu’il faut retenir

Afficher le code
cat(sprintf(
"- **Le marché s'est retourné en 2022** — après une hausse de **%s %%** entre 2019 et 2022, le prix médian recule de **%s %%** en trois ans, à **%s €** le m².
- **L'ajustement passe par les volumes, pas par les prix** — les ventes reculent de **%s %%** depuis 2022, soit six fois plus vite que les prix. Le marché se bloque avant de se déprécier.
- **La construction décroche encore plus fort** — **%s** logements autorisés en 2025, un recul de **%s %%** depuis 2022 et de **%s %%** pour l'individuel : c'est l'offre future qui se contracte.
- **Le rural tient les prix, au prix d'un parc dégradé** — seul type de territoire encore en hausse, il concentre **%s %%** de passoires énergétiques contre **%s %%** dans les communes-centres. Le calendrier d'interdiction de location va révéler cette fragilité.
",
  fmt_fr(KPI_LOG$px_tot_1922, 0), fmt_fr(abs(KPI_LOG$px_tot_2225), 0), fmt_fr(sy_px, 0),
  fmt_fr(abs(KPI_LOG$trans_tot_2225), 0),
  fmt_k(KPI_LOG$constr_25), fmt_fr(abs(KPI_LOG$constr_ev), 0), fmt_fr(abs(KPI_LOG$constr_ind_ev), 0),
  fmt_fr(.rur, 1), fmt_fr(.cen, 1)))
  • Le marché s’est retourné en 2022 — après une hausse de 18 % entre 2019 et 2022, le prix médian recule de 3 % en trois ans, à 2 616 € le m².
  • L’ajustement passe par les volumes, pas par les prix — les ventes reculent de 21 % depuis 2022, soit six fois plus vite que les prix. Le marché se bloque avant de se déprécier.
  • La construction décroche encore plus fort — 348 k logements autorisés en 2025, un recul de 21 % depuis 2022 et de 33 % pour l’individuel : c’est l’offre future qui se contracte.
  • Le rural tient les prix, au prix d’un parc dégradé — seul type de territoire encore en hausse, il concentre 19,6 % de passoires énergétiques contre 7,3 % dans les communes-centres. Le calendrier d’interdiction de location va révéler cette fragilité.
Afficher le code
cat("
:::

")

Indicateurs clés · Logement · France Niveau 2025 et dynamique depuis 2022

Afficher le code
# Bandeau KPI = kpi_row (grille auto-fit) : 3 tuiles CHIFFRES + 1 tuile à DEUX MINI-GRAPHES.
# ⚠️ kpi_tiles_synth (jcn-rpt-kpi) = signature (category, items, footer) — À NE PAS CONFONDRE avec
#    kpi_tiles_psect (jcn-kpi-tiles, ex-psect) qui prend title/poids/dyn. Collision de noms résolue
#    par le renommage 260822 : ici on veut la version bandeau.
# ⚠️ Un ggplot NE PEUT PAS entrer dans une cellule de grille → gg_img() le convertit en PNG base64.
#
# ÉVOLUTIONS EN SOUS-LIGNE (item$note) : évolution TOTALE, jamais %/an — conv projet, un %/an sur
# 3 ans ne parle à personne alors qu'un « −9 % depuis 2022 » se retient. Deux références (2022 = le
# retournement, 2019 = l'avant-Covid) sur une ligne italique EN RETRAIT, qui dit visuellement
# qu'elle qualifie le chiffre au-dessus. Et la SPARKLINE est rattachée à SON indicateur (item$spark)
# plutôt qu'en pied de tuile, où l'on ne savait pas quelle courbe illustrait quel chiffre.
.tot  <- function(tcam, n) if (!is.finite(tcam)) NA_real_ else ((1 + tcam / 100)^n - 1) * 100
# forme COMPACTE « ▼ −3,3 % / 2022 » : le `ref` de kpi_evo pose déjà l'année en gris clair. Écrire
# « depuis 2022 » en toutes lettres faisait passer la note sur 3 lignes dans la colonne label.
# Deux lignes ASSUMÉES (<br>) plutôt qu'un séparateur « · » : la colonne label est étroite, le point
# médian se retrouvait orphelin en fin de ligne au gré du retour automatique.
.note2 <- function(t22, t19) paste0(
  kpi_evo(t22, "%", ref = "2022"),
  if (is.finite(t19)) paste0(" · ", kpi_evo(t19, "%", ref = "2019")) else "")
# Légende de sparkline : sans elle on ne sait pas quelle courbe illustre quel chiffre.
.sp_cap <- function(txt) sprintf(
  "<div style='font-size:.7rem;color:#9a9a9a;margin-bottom:1px'>%s</div>", txt)

.p_ix <- SER_FR$px_ix        # médiane des indices départementaux, base 100 = 2015
.p_tr <- SER_FR$trans / 1000 # transactions, milliers/an

# ⚠️ Transactions : les totaux nationaux se lisent sur la SOMME des départements (des stocks, la
# somme est exacte) — surtout PAS sur la médiane des TCAM départementaux, qui n'est pas le TCAM
# national et peut même en inverser le signe.
.trv <- stats::setNames(SER_FR$trans, as.character(SER_FR$annee))
sy_tr_t22 <- (.trv[["2025"]] / .trv[["2022"]] - 1) * 100
sy_tr_t19 <- (.trv[["2025"]] / .trv[["2019"]] - 1) * 100

# Évolutions totales du prix, dérivées des TCAM NATIFS de dbcln (aucune approximation) :
# (1+r1925)^6 = (1+r1922)^3 × (1+r2225)^3  →  la fenêtre 19-22, absente de dbcln, s'en déduit exactement.
sy_px_t2225 <- .tot(sy_px_ev, 3)
sy_px_t1922 <- ((1 + sy_px_evl / 100)^6 / (1 + sy_px_ev / 100)^3 - 1) * 100

# ⚠️ PIÈGE ggplot2 : theme_urbn() règle EXPLICITEMENT axis.title.x / axis.title.y. Or l'héritage de
# thème va du parent vers l'ENFANT, et l'enfant l'emporte → `axis.title = element_blank()` NE LES
# EFFACE PAS. Il faut viser les enfants.
# STYLE DÉPOUILLÉ (user 260825) : ni quadrillage, ni fond blanc, ni graduations — dans une tuile de
# 155 px ces éléments ne portent aucune information lisible et ne font qu'ajouter du bruit. Ne
# restent que la forme de la courbe et deux repères d'années. Fond TRANSPARENT pour épouser le bleu
# de la tuile au lieu d'y découper un rectangle blanc.
# Les mini-graphes ggplot de l'ancienne tuile « Le retournement » ont ete RETIRES : ils
# retracaient exactement les sparklines des tuiles 1 et 2. Leur habillage n'est pas perdu pour
# autant — il est promu dans mutils (`kpi_minichart()` / `kpi_minichart_ends()`, jcn-kpi-tiles.R)
# et reste utilisable partout. La tuile 4 accueille desormais la CONSTRUCTION. (260828)
.p_ct <- SER_CONSTR$logaut / 1000   # logements autorises, milliers/an

# delta SANS triangle : le nombre est deja signe, le triangle faisait doublon et debordait.
.dl <- function(t) kpi_evo(t, "%", ref = NULL, arrow = FALSE)

kpi_row(cols = 4,
  kpi_tiles_synth(kcat("currency-euro", "Prix médian au m²"),
    sub = "2025 · évol / 2022", compact = TRUE, delta_style = "inline",
    items = list(
      list(value = paste0(fmt_fr(sy_px, 0), " €"),             label = "Ensemble",
           delta = .dl(.tot(sy_px_ev, 3))),
      list(value = paste0(fmt_fr(KPI_LOG$px_apt_25, 0), " €"), label = "Appart.",
           delta = .dl(.tot(KPI_LOG$px_apt_ev, 3))),
      list(value = paste0(fmt_fr(KPI_LOG$px_mai_25, 0), " €"), label = "Maison",
           delta = .dl(.tot(KPI_LOG$px_mai_ev, 3)))),
    footer = kpi_sparkline(.p_ix, xlab = SER_FR$annee, w = 150, h = 46, annot = TRUE, lab_left = "2015", lab_right = "2025",
                           annot_fmt = function(v) formatC(v, format = "f", digits = 0))),

  kpi_tiles_synth(kcat("arrow-left-right", "Transactions immobilières"),
    sub = "Mutations 2025 · évol / 2022", compact = TRUE, delta_style = "inline",
    items = list(
      list(value = fmt_k(KPI_LOG$trans_25),     label = "Ensemble",
           delta = .dl(sy_tr_t22)),
      list(value = fmt_k(KPI_LOG$trans_apt_25), label = "Appart.",
           delta = .dl(KPI_LOG$trans_apt_ev)),
      list(value = fmt_k(KPI_LOG$trans_mai_25), label = "Maisons",
           delta = .dl(KPI_LOG$trans_mai_ev))),
    footer = kpi_sparkline(.p_tr, col1 = "#8a939b", area = TRUE, zero_base = TRUE,
                           w = 150, h = 46, annot = TRUE, lab_left = "2015", lab_right = "2025", xlab = SER_CONSTR$annee,
                           annot_fmt = function(v) paste0(formatC(v, format = "f", digits = 0), " k"))),

  kpi_tiles_synth(kcat("house-door", "État du parc"),
    sub = "Parc · 2023-2026 · évol / 2017", compact = TRUE, delta_style = "inline",
    items = list(
      list(value = paste0(fmt_fr(sy_vac, 1), " %"),          label = "Logements vacants",
           delta = .dl(KPI_LOG$vac_evol)),
      list(value = paste0(fmt_fr(KPI_LOG$vac2ans, 1), " %"), label = "Vacants > 2 ans"),
      list(value = paste0(fmt_fr(sy_dpe, 1), " %"),          label = "Passoires F-G"))),

  # ⚠️ AUTORISÉS, pas commencés : entre le permis et le chantier il y a un délai, et une part des
  # permis n'est jamais mise en œuvre. Et la série SITADEL s'arrête en 2025, pas 2026 comme le DPE.
  # DOUBLE ÉVOLUTION (delta2) : / 2022 = la chute du retournement, / 2024 = le rebond en cours —
  # une seule référence cachait l'inflexion 2025 (base SITADEL 3 : rebond +15 % vs 2024).
  # sub porte DEJA les deux references -> la ligne d'en-tetes de colonnes ferait doublon (retiree,
  # user 260901). delta_style="inline" : deltas colles au libelle plutot qu'en rangee dediee.
  kpi_tiles_synth(kcat("building", "Logements autorisés"),
    sub = "2025 · évol / 2022 / 2024", compact = TRUE, delta_style = "inline",
    items = list(
      list(value = fmt_k(KPI_LOG$constr_25),     label = "Ensemble",
           delta = .dl(KPI_LOG$constr_ev),     delta2 = .dl(KPI_LOG$constr_ev24)),
      list(value = fmt_k(KPI_LOG$constr_ind_25), label = "Individuel",
           delta = .dl(KPI_LOG$constr_ind_ev), delta2 = .dl(KPI_LOG$constr_ind_ev24)),
      list(value = fmt_k(KPI_LOG$constr_m2_25), label = "m² autorisés",
           delta = .dl(KPI_LOG$constr_m2_ev),  delta2 = .dl(KPI_LOG$constr_m2_ev24))),
    footer = kpi_sparkline(.p_ct, col1 = "#8a6d3b", area = TRUE, zero_base = TRUE,
                           w = 150, h = 46, annot = TRUE, lab_left = "2015", lab_right = "2025", xlab = SER_CONSTR$annee,
                           annot_fmt = function(v) paste0(formatC(v, format = "f", digits = 0), " k"))))
Prix médian au m² 2025 · évol / 2022
2 616 € Ensemble -3,3 %
3 355 € Appart. -3,3 %
2 177 € Maison -1,5 %
Transactions immobilières Mutations 2025 · évol / 2022
782 k Ensemble -20,8 %
350 k Appart. -23,2 %
431 k Maisons -18,8 %
État du parc Parc · 2023-2026 · évol / 2017
7,8 % Logements vacants -0,3 %
3,4 % Vacants > 2 ans
9,2 % Passoires F-G
Logements autorisés 2025 · évol / 2022 / 2024
348 k Ensemble -20,8 % +15,2 %
117 k Individuel -33,3 % +10,5 %
25,8 M m² autorisés -29,1 % +11,9 %

Où le mètre carré est-il cher, et où décroche-t-il ? · départements

Afficher le code
.d_px <- build_map(.geo_dep_j, "px_25", tooltip_lab = "Prix médian au m²", bins = sy_bins_px, label_col = "nom_officiel",
    interactive = TRUE, hover_key = TRUE, legend_drop = FALSE, show_counts = FALSE,
    titre = "Niveau : prix médian au m² · 2025",
    subtitle = "€/m² · départements",
    source_txt = NULL, n_top = 3, n_bottom = 2, show_values = FALSE,
    label_style = "text", legend_title = "€/m²",
    legend_keywidth = 0.5, legend_keyheight = 0.17,
    contour_geo = GEO_REG, contour_lw = 0.35, contour_color = "#b0b0b0", dd = DD,
    zoom_box = BBOX_IDF) + th_map_sm
map_inset(.d_px, .geo_dep_j, "px_25", codes = INSET_IDF_PC, bins = sy_bins_px,
          code_col = "code_insee", label_col = "nom_officiel", dd = DD,
          points = FALSE, interactive = TRUE, pos = "topright") |>
  girafe_legend(sy_bins_px, width_svg = 4.4, height_svg = 4.35, title = "€/m²", zoom = TRUE,
                values = DEP_LOG$px_25)
€/m²clic & ctrl-clic
Afficher le code
.d_ev <- build_map(.geo_dep_j, "px_tcam_2225", tooltip_lab = "Évolution du prix", bins = sy_bins_ev, label_col = "nom_officiel",
    interactive = TRUE, hover_key = TRUE, legend_drop = FALSE, show_counts = FALSE,
    titre = "Dynamique : évolution du prix médian · 2022-2025",
    subtitle = "%/an · départements",
    source_txt = NULL, n_top = 3, n_bottom = 2,
    show_values = TRUE, auto_text_contrast = TRUE,
    label_style = "text", legend_title = "%/an",
    legend_keywidth = 0.5, legend_keyheight = 0.17,
    contour_geo = GEO_REG, contour_lw = 0.35, contour_color = "#b0b0b0", dd = DD,
    zoom_box = BBOX_IDF) + th_map_sm
map_inset(.d_ev, .geo_dep_j, "px_tcam_2225", codes = INSET_IDF_PC, bins = sy_bins_ev,
          code_col = "code_insee", label_col = "nom_officiel", dd = DD,
          points = FALSE, interactive = TRUE, pos = "topright") |>
  girafe_legend(sy_bins_ev, width_svg = 4.4, height_svg = 4.35, title = "%/an", zoom = TRUE,
                values = DEP_LOG$px_tcam_2225)
%/anclic & ctrl-clic

Le même marché vu par les grandes villes · communes de plus de 30 000 habitants

Afficher le code
.m_px <- build_map_points(.cpx, "px_25", tooltip_lab = "Prix médian au m²", bins = sy_bins_px_cm,
    size_col = "pop_bulle", size_range = c(1.3, 9.5), point_border = "#8f8f8f", label_col = "libelle",
    label_exclude = .LBL_SKIP,
    label_style = "text", n_top = 2, n_bottom = 2,
    bg_geo = GEO_DEP, bg_fill = "#f7f6f2", bg_color = "#dedede", bg_lw = 0.22,
    contour_geo = GEO_REG, contour_color = "#c9c9c9", contour_lw = 0.3, interactive = TRUE, dd = DD,
    titre = "Niveau : prix médian au m² · 2025",
    subtitle = "€/m² · taille = population", source_txt = NULL,
    legend_title = "€/m²", show_counts = FALSE, terr_lbl = "communes") + th_map_sm
map_inset(.m_px, .inset_px, "px_25", codes = INSET_IDF_PC, bins = sy_bins_px_cm,
          bg_geo = GEO_DEP, size_col = "P22_POP", label_col = "libelle", dd = DD,
          point_border = "#8f8f8f", size_range = c(0.9, 3.4),
          interactive = TRUE, pos = "topright") |>
  girafe_legend(sy_bins_px_cm, width_svg = 4.4, height_svg = 4.35, title = "€/m²", zoom = TRUE,
                values = sf::st_drop_geometry(.cpx)$px_25)
€/m²clic & ctrl-clic
Afficher le code
.m_ev <- build_map_points(.cev, "px_tcam_2225", tooltip_lab = "Évolution du prix", bins = sy_bins_ev_cm,
    size_col = "pop_bulle", size_range = c(1.3, 9.5), point_border = "#8f8f8f", label_col = "libelle",
    label_exclude = .LBL_SKIP,
    label_style = "text", n_top = 2, n_bottom = 2,
    bg_geo = GEO_DEP, bg_fill = "#f7f6f2", bg_color = "#dedede", bg_lw = 0.22,
    contour_geo = GEO_REG, contour_color = "#c9c9c9", contour_lw = 0.3, interactive = TRUE, dd = DD,
    titre = "Dynamique : évolution du prix médian · 2022-2025",
    subtitle = "%/an · taille = population", source_txt = NULL,
    legend_title = "%/an", show_counts = FALSE, terr_lbl = "communes") + th_map_sm
map_inset(.m_ev, .inset_ev, "px_tcam_2225", codes = INSET_IDF_PC, bins = sy_bins_ev_cm,
          bg_geo = GEO_DEP, size_col = "P22_POP", label_col = "libelle", dd = DD,
          point_border = "#8f8f8f", size_range = c(0.9, 3.4),
          interactive = TRUE, pos = "topright") |>
  girafe_legend(sy_bins_ev_cm, width_svg = 4.4, height_svg = 4.35, title = "%/an", zoom = TRUE,
                values = sf::st_drop_geometry(.cev)$px_tcam_2225)
%/anclic & ctrl-clic

Lecture : chaque maille a sa propre échelle de couleurs. Encart en haut à droite : Paris et la petite couronne, à la maille arrondissement sur les cartes communales. Taille des bulles plafonnée à celle de Marseille. Survol pour le détail, clic sur une classe de la légende pour la filtrer.

Le logement par type de territoire Zonage INSEE en aires d’attraction des villes (CATEAAV 2020) · repère France en trait vertical

Afficher le code
.ty  <- TYPO_LOG[TYPO_LOG$type_lib != "France métropolitaine", ]
.fr  <- TYPO_LOG[TYPO_LOG$type_lib == "France métropolitaine", ]
.ser <- as.character(.ty$type_lib)
# ⚠️ Une ÉVOLUTION ne se représente pas par un databar : la barre part toujours vers la droite, si
# bien que −2,9 % et +0,9 % produisaient deux barres de même sens — illisible et faux. databar_psect
# prévoit exactement ça : `evol` accroche une PASTILLE signée (rose hausse / bleu baisse) au chiffre
# de niveau qu'elle qualifie. L'évolution des prix rejoint donc la colonne « Prix médian ». (260824)
.mk2 <- function(col, evol_col = NULL) list(
  vals = stats::setNames(.ty[[col]], .ser), ref = .fr[[col]][1],
  evol = if (!is.null(evol_col)) stats::setNames(.ty[[evol_col]], .ser) else NULL)
# ORDRE : le VOLUME d'abord (combien d'actes), le PRIX ensuite (à quel niveau) — on dit d'abord
# le poids du segment, puis sa valorisation. Chaque sous-titre porte SA source et SON millésime :
# la table croise du DV3F 2025, du recensement 2023 et du DPE 2026, et rien ne le signalait.
.ind <- list(
  # ⚠️ Pas de repère France sur un VOLUME : comparer les mutations d'un type au TOTAL national n'a
  # pas de sens (c'est une part, pas un écart). On coupe donc `ref` pour cette seule colonne.
  c(list(lbl = "Transactions", sub = "mutations · DV3F 2025 · pastille = évol. 22-25", unit = "", dec = 0),
    within(.mk2("logd_trans_25", "logd_trans_vevol_2225"), rm(ref))),
  c(list(lbl = "Prix médian au m²", sub = "€ · DV3F 2025 · pastille = évol. 22-25", unit = "", dec = 0),
    .mk2("logd_px2q2_glb_25", "logd_px2_glb_vevol_2225")),
  c(list(lbl = "Logements vacants", sub = "% · INSEE RP 2023 · pastille = évol. 17-23, en points", unit = "", dec = 1),
    .mk2("log_vac_pct_22", "log_vac_pct_vdifp_1622")),
  c(list(lbl = "% propriétaires",   sub = "% des résidences principales · INSEE RP 2023 · pastille = évol. 17-23, en points", unit = "", dec = 1),
    .mk2("log_prop_pct_22", "log_prop_pct_vdifp_1622")),
  c(list(lbl = "Passoires F-G",     sub = "% · DPE ADEME 2026", unit = "", dec = 1), .mk2("logd_dpe_fg_pct_26")))
# Une COULEUR PAR SÉRIE (et non 1 accent + du gris) : ici les 4 types se lisent comme un dégradé
# urbain -> rural, donc la couleur porte du sens en plus de distinguer les lignes. (modèle pbnb)
# target = FALSE : les 4 types sont des PAIRS, aucun n'est « la cible » — donc aucune ligne n'est
#   mise en avant, toutes ont le même aspect (défaut TRUE conservé pour psect/pterr : secteur vs France).
# ref_all = TRUE : le trait France est tracé sur CHAQUE barre, pas seulement la première — chaque
#   type se compare alors directement au repère national. L'étiquette « fr … » reste sur la 1re ligne.
# pal = PAL_TYPO_CURVE (famille pterr GOLD/orange/bleu/navy) : la MÊME couleur par type que les
# courbes indice 100 des parties transactions/prix — harmonisation databar ↔ courbes (user 260903).
cat(databar_psect(.ind, series = .ser, n_col = 5, ref_lab = "fr", col_header = FALSE,
                  pal = unname(PAL_TYPO_CURVE[.ser]), target = FALSE, ref_all = TRUE,
                  lbl_width = "132px"))
Transactions
mutations · DV3F 2025 · pastille = évol. 22-25
Prix médian au m²
€ · DV3F 2025 · pastille = évol. 22-25
Logements vacants
% · INSEE RP 2023 · pastille = évol. 17-23, en points
% propriétaires
% des résidences principales · INSEE RP 2023 · pastille = évol. 17-23, en points
Passoires F-G
% · DPE ADEME 2026
Commune-centre
207 546−19,8
2 400fr 2 616−1,2
9,6fr 7,8−0,6
42,3fr 59,0−0,1
7,3fr 8,9
Autre commune du pôle
157 725−24,7
3 583−8,4
6,3−0,1
49,4−0,8
8,0
Couronne périurbaine
299 767−19,3
2 279−2,2
7,2−0,4
71,7+0,0
9,9
Hors attraction (rural)
61 100−22,7
1 727+2,6
8,8−0,6
74,5+0,6
19,6
Afficher le code
# Valeurs de la note de lecture (règle ccom : AUCUN chiffre en dur dans la prose — les 3 583 €
# et « 689 communes » écrits à la main seraient figés/faux au prochain millésime)
.nl_px <- function(lb) TYPO_LOG$logd_px2q2_glb_25[TYPO_LOG$type_lib == lb][1]
.nl_n  <- function(lb) SER_TYPO$n_communes[SER_TYPO$annee == 2025 & SER_TYPO$type_lib == lb][1]

Lecture : une aire d’attraction se compose d’un pôle — la ville-centre et ses banlieues denses, dites « autres communes du pôle » — puis d’une couronne de communes dont les actifs viennent y travailler ; le reste est hors attraction. Les lignes vont donc du plus urbain au plus rural.
Les autres communes du pôle sont plus chères que les communes-centres (3 583 € contre 2 400 €) par un effet de composition : il n’existe qu’une commune-centre par aire, soit 689 — surtout des petites villes — quand les 1 017 communes de banlieue dense se concentrent dans les grandes agglomérations.

1 Méthode, données et axes d’analyse

Le marché du logement est lu à quatre échelles emboîtées, de la plus large à la plus fine : la France, le type de territoire (commune-centre, autre commune du pôle, couronne périurbaine, hors attraction), le département et la zone d’emploi, enfin la commune de plus de 30 000 habitants. Chaque partie thématique suit cette descente : d’abord la tendance nationale, puis sa géographie, enfin le détail communal.

Périmètre : France métropolitaine — 101 départements, 306 zones d’emploi et 287 communes de plus de 30 000 habitants.

Sources et millésimes : prix et transactions DV3F (séries annuelles 2006-2025, prix médian au m² et nombre de mutations) · parc et statut d’occupation INSEE recensement 2023 · performance énergétique ADEME DPE 2026 · construction Sitadel 3 (logements autorisés 2013-2025 et commencés 2013-2024, extraction SDES d’août 2026). Agrégation PTB.

Périodes de lecture : les cartes retiennent 2019-2025 (six ans, avant-COVID à aujourd’hui). L’analyse du retournement compare deux fenêtres de trois ans chacune — 2019-2022 (reprise) et 2022-2025 (retournement) — pour que les rythmes soient directement comparables.

Note de fiabilité — les zones blanches des cartes de prix ont deux causes distinctes. La Moselle, le Bas-Rhin et le Haut-Rhin n’apparaissent sur aucune carte de prix ni de transaction : ces trois départements relèvent du livre foncier d’Alsace-Moselle et non du fichier immobilier dont DV3F est extrait — l’absence est juridique et définitive, non un défaut de couverture. Ailleurs, le prix médian au m² n’est calculé que si le territoire enregistre assez de mutations dans l’année, ce qui laisse quelques communes de côté. La couverture atteint 92 % des départements et 91 % des communes de plus de 30 000 habitants. Les données de construction, elles, couvrent la France entière : les cartes de la partie construction n’ont aucune zone blanche.

2 Contexte : ce qui a retourné le marché

Le marché résidentiel français a connu deux régimes successifs en six ans. Comprendre le retournement de 2022 suppose de rappeler ce qui l’a provoqué : un choc de financement, puis un durcissement réglementaire du parc.

Le choc de financement : la fin de l’argent gratuit

Le taux moyen des crédits immobiliers passe de 1,1 % fin 2021 à plus de 4 % fin 2023 — un quadruplement en deux ans.

La remontée des taux directeurs de la Banque centrale européenne, engagée en juillet 2022 pour contrer l’inflation, s’est transmise très vite au crédit immobilier. Le renchérissement du coût de l’emprunt a mécaniquement réduit la capacité d’achat des ménages : à mensualité constante, un acquéreur perd de l’ordre d’un cinquième de son pouvoir d’achat immobilier entre 2021 et 2023. La demande solvable s’est contractée, et le nombre de transactions avec elle.

L’effet ne joue pas partout de la même façon. Là où les prix sont élevés et la part d’acheteurs primo-accédants importante — grandes agglomérations, marchés tendus — la contrainte de financement mord davantage. Les marchés peu chers, où l’apport pèse moins lourd, encaissent mieux le choc.

Sources : Banque de France, taux d’intérêt des crédits nouveaux à l’habitat (série mensuelle) · INSEE, indice des prix à la consommation. Ordres de grandeur de cadrage, hors pipeline de données du rapport : ils datent la mécanique du retournement, ils ne se substituent pas aux mesures des parties suivantes.

Le durcissement réglementaire : le parc sous contrainte énergétique

Depuis 2023, les logements les plus énergivores sortent progressivement du marché locatif — un calendrier qui pèse sur la valeur du parc ancien.

La loi Climat et résilience organise l’interdiction progressive de location des passoires énergétiques : étiquette G depuis 2025, F à horizon 2028, E à horizon 2034. Le diagnostic de performance énergétique, opposable depuis 2021, est devenu un paramètre de prix : à caractéristiques comparables, un logement mal classé se négocie avec une décote, et le coût des travaux de rénovation entre dans la négociation.

Cette contrainte frappe inégalement les territoires : le parc ancien, rural et de maisons individuelles concentre les mauvaises étiquettes, quand le parc récent et collectif des zones denses s’en tire mieux. La partie consacrée au parc et à l’énergie quantifie cet écart.

S’y ajoutent des dispositifs qui déplacent la demande plutôt qu’ils ne la réduisent : encadrement des loyers dans les zones tendues, restrictions sur les meublés de tourisme dans les communes touristiques, extinction progressive du dispositif Pinel dans le neuf.

Sources : loi n° 2021-1104 du 22 août 2021 dite « Climat et résilience » (calendrier d’interdiction de location des logements classés G, F puis E) · diagnostic de performance énergétique opposable depuis le 1er juillet 2021. L’effet de la location de courte durée sur les marchés locaux relève d’une autre source (données de plateformes) et d’un autre périmètre : il n’est pas traité ici.

3 Analyse

Transactions : le marché s’est-il grippé ?

Le nombre de ventes réagit avant les prix : quand le crédit se renchérit, les acquéreurs renoncent ou reportent, et le volume décroche pendant que les valeurs tiennent encore. C’est donc par les transactions que se lit le premier signal du retournement.

Afficher le code
# ÉLÉMENTS CLÉS — puces EXTRAITES des [..]{.insight} de ce fichier (bloc_cles_srpt, jcn-prose) :
# la synthèse de tête ne peut pas diverger du corps, elle en est tirée. Appelé APRÈS calc-trans,
# sinon les variables des insights ne sont pas encore définies. Modèle pterr (bloc_synthese).
cat(bloc_cles_srpt("srpt/_srpt-thlog-03-transactions.qmd"))

Éléments clés

  • Le marché a culminé en 2021 avant de reculer de 25 % en trois ans : un retournement brutal, et de même ampleur dans tous les types de territoire.
  • Le recul des ventes touche la quasi-totalité des départements : la médiane s’établit à -6,8 % par an depuis 2022.

Le repli est général, et de même ampleur partout

Le marché a culminé en 2021 avant de reculer de 25 % en trois ans : un retournement brutal, et de même ampleur dans tous les types de territoire.

La reprise post-COVID avait porté les ventes à un niveau inédit — desserrement résidentiel, épargne accumulée, crédit encore bon marché. Le renversement de 2021 est synchrone : commune-centre, couronne ou rural reculent ensemble, ce qui signe une cause macro-financière et non un phénomène local. Le rural, parti de plus bas, amortit à peine mieux que les pôles.

Afficher le code
log_table_auto("serie_trans_typo", tr_idx[tr_idx$annee %in% c(2015, 2019, 2022, 2025),
                                          c("type_lib", "annee", "indice")], label_col = "type_lib")
# Courbes LISSÉES modèle pterr (plot_serie_pal, spline monotone) + palette HARMONISÉE avec la
# databar (PAL_TYPO_CURVE : 1 type = 1 couleur partout) + barres grises = volume France/an,
# comme la fiche commune. Remplace plot_line_indice100 (segments anguleux, couleurs propres). 260902
.tri <- rbind(tr_idx[, c("annee", "indice", "type_lib")] |> stats::setNames(c("annee", "indice", "serie")),
              data.frame(annee = tr_ref$annee, indice = tr_ref$fr, serie = "France"))
.tri$serie <- factor(.tri$serie, levels = names(PAL_TYPO_CURVE))
.ev_end <- vapply(levels(.tri$serie), function(l) {
  d <- .tri[.tri$serie == l & is.finite(.tri$indice), ]; d <- d[order(d$annee), ]
  if (!nrow(d)) NA_real_ else d$indice[nrow(d)] - 100 }, numeric(1))
.short <- c("Commune-centre" = "Centre", "Autre commune du pôle" = "Banlieue du pôle",
            "Couronne périurbaine" = "Couronne", "Hors attraction (rural)" = "Rural",
            "France" = "France")
.lab <- stats::setNames(sprintf("%s · %+.0f %%", .short[levels(.tri$serie)], .ev_end), levels(.tri$serie))
g_tr <- plot_serie_pal(.tri, x_col = "annee", y_col = "indice", group_col = "serie",
  pal_named = PAL_TYPO_CURVE, lty_named = LTY_TYPO_CURVE,
  smooth = TRUE, hline_ref = 100, interactive = TRUE, point_size = 1.1,
  # label_last_fn OBLIGATOIRE pour activer les etiquettes de fin (le helper ne les trace que si
  # label_last_fn ou emphasis_groups est fourni) ; lab_override remplace ensuite le texte.
  end_inside = TRUE, lab_override = .lab, label_last_fn = function(y) sprintf("%+.0f %%", y - 100),
  bar_df = data.frame(annee = SER_FR$annee, val = SER_FR$trans),
  bar_name = "Mutations/an (France)", bar_label_fn = function(v) paste0(round(v / 1000), " k"),
  title    = sprintf("Les ventes ont culminé en %d avant de décrocher partout", tr_pic),
  subtitle = sprintf("Nombre de mutations · indice base 100 = %d · par type de territoire · barres : volume France/an — DV3F",
                     params$millesime_base),
  y_lab = NULL, x_breaks = seq(2015, 2025, 2))
render_girafe(g_tr, width_svg = 9, height_svg = 4.2)

Quelques bassins résistent dans des départements qui décrochent

Le recul des ventes touche la quasi-totalité des départements : la médiane s’établit à -6,8 % par an depuis 2022.

La carte départementale donne la vue d’ensemble, la zone d’emploi révèle les contrastes que la moyenne départementale efface (figure ci-contre). L’effet de maille est net : quelques bassins résistent au sein de départements qui, globalement, décrochent.

Afficher le code
bins_tr <- auto_bins(c(DEP_LOG$trans_tcam_2225, ZE_LOG$trans_tcam_2225) |>
                       (\(x) x[is.finite(x)])(), var_name = "trans_tcam_2225")
log_map_auto("carte_trans_dep", "trans_tcam_2225", DEP_LOG, "libelle")
(build_map(dplyr::left_join(GEO_DEP, DEP_LOG, by = c("code_insee" = "code")),
  "trans_tcam_2225", bins = bins_tr, label_col = "nom_officiel",
  titre = "Évolution des transactions · 2022-2025", subtitle = "%/an · départements",
  source_txt = make_source("DV3F"), n_top = 3, n_bottom = 3,
  label_style = "text", show_values = FALSE, legend_keywidth = 0.62, legend_keyheight = 0.2,
  contour_geo = GEO_REG, contour_lw = 0.4, contour_color = "#b0b0b0", dd = DD,
  interactive = TRUE, hover_key = TRUE, legend_drop = FALSE, legend_title = "%/an") + th_map_sm) |>
  girafe_legend(bins_tr, width_svg = 4.5, height_svg = 4.95, title = "%/an", zoom = TRUE)
%/anclic & ctrl-clic
Afficher le code
(build_map(dplyr::left_join(GEO_ZE, ZE_LOG, by = c("ze2020" = "code")),
  "trans_tcam_2225", bins = bins_tr, label_col = "libze2020",
  titre = "Évolution des transactions · 2022-2025", subtitle = "%/an · zones d'emploi",
  source_txt = make_source("DV3F"), n_top = 3, n_bottom = 3,
  label_style = "text", show_values = FALSE, legend_keywidth = 0.62, legend_keyheight = 0.2,
  contour_geo = GEO_REG, contour_lw = 0.4, contour_color = "#b0b0b0", dd = DD,
  interactive = TRUE, hover_key = TRUE, legend_drop = FALSE, legend_title = "%/an") + th_map_sm) |>
  girafe_legend(bins_tr, width_svg = 4.5, height_svg = 4.95, title = "%/an", zoom = TRUE)
%/anclic & ctrl-clic

Les grandes villes ne sont pas épargnées

À l’échelle communale, le recul des ventes est quasi général : 99 % des 261 communes de plus de 30 000 habitants enregistrent moins de mutations en 2025 qu’en 2022 (carte et classement ci-contre).

Afficher le code
log_describe_ranking("comm_trans_2225", .cm, "libelle", "trans_tcam_2225")
# Interactif + légende filtrante (idiome synthèse) ; bins divergents asymétriques, baisse en bleu.
bins_cm_tr <- make_bins_divergent_asym(.cm$trans_tcam_2225[is.finite(.cm$trans_tcam_2225)],
                                       n_total = 9, sign_meaning = "red_pos", suffix = " %")
(build_map_points(cm, "trans_tcam_2225", tooltip_lab = "Évolution des transactions", bins = bins_cm_tr,
  size_col = "P22_POP", size_range = c(1.3, 9.5), point_border = "#8f8f8f",
  label_col = "libelle", label_style = "text", n_top = 3, n_bottom = 3,
  bg_geo = GEO_DEP, bg_fill = "#f7f6f2", bg_color = "#dedede", bg_lw = 0.22,
  contour_geo = GEO_REG, contour_color = "#c9c9c9", contour_lw = 0.3, interactive = TRUE, dd = DD,
  titre = "Évolution des transactions · 2022-2025",
  subtitle = sprintf("%%/an · communes de plus de %s habitants · taille = population", format(params$seuil_commune, big.mark = " ")),
  source_txt = make_source("DV3F · INSEE RP"), legend_title = "%/an", show_counts = FALSE,
  terr_lbl = "communes") + th_map_sm) |>
  girafe_legend(bins_cm_tr, width_svg = 4.5, height_svg = 4.95, title = "%/an", zoom = TRUE)
%/anclic & ctrl-clic
Afficher le code
# Tableau à en-têtes groupés (rt_topbot_pairs) : Mutations (nb | ▲ 22-25) puis prix, population.
# Tri sur l'ÉVOLUTION : en tête celles qui résistent, en queue celles qui décrochent.
.cmt <- .cm[, c("libelle", "trans_25", "trans_tcam_2225", "px_25", "P22_POP")]
.fr_tr_tcam <- (SER_FR$trans[SER_FR$annee == 2025] / SER_FR$trans[SER_FR$annee == 2022])^(1/3) * 100 - 100
rt_topbot_pairs(.cmt, lib_col = "libelle", lib_label = "Commune",
  vars   = c("trans_25", "trans_tcam_2225", "px_25", "P22_POP"),
  labels = c("Mutations", "Évol.", "Prix médian", "Population"),
  units  = c("nb 2025", "22-25 %/an", "€/m²", "hab."),
  digits = c(0, 1, 0, 0), pairs = c(trans_tcam_2225 = "trans_25"),
  sort_col = "trans_tcam_2225", n_top_bot = 6, n_middle = 0,
  ref_rows = list(
    list(lib = "Médiane des communes", values = c(trans_25 = median(.cmt$trans_25, na.rm = TRUE), trans_tcam_2225 = median(.cmt$trans_tcam_2225, na.rm = TRUE), px_25 = median(.cmt$px_25, na.rm = TRUE), P22_POP = median(.cmt$P22_POP, na.rm = TRUE))),
    list(lib = "France", values = c(trans_25 = SER_FR$trans[SER_FR$annee == 2025], trans_tcam_2225 = .fr_tr_tcam, px_25 = FR$logd_px2q2_glb_25[1], P22_POP = NA))),
  title    = "Les villes qui résistent, celles qui décrochent",
  subtitle = sprintf("6 premières et 6 dernières des %d communes de plus de %s habitants — DV3F, INSEE RP", nrow(.cmt), format(params$seuil_commune, big.mark = " ")),
  footnote = "Barre = position dans le classement · évolution en bordeaux (hausse) / bleu (baisse) · footer : médiane des communes puis France (total des mutations, évolution nationale).")
Les villes qui résistent, celles qui décrochent
6 premières et 6 dernières des 261 communes de plus de 30 000 habitants — DV3F, INSEE RP
⇅ triable
Barre = position dans le classement · évolution en bordeaux (hausse) / bleu (baisse) · footer : médiane des communes puis France (total des mutations, évolution nationale).

La dispersion reste toutefois marquée : entre la commune la plus touchée et celle qui résiste le mieux, l’écart atteint 31 points de croissance annuelle. Les marchés les plus chers et les plus dépendants de l’investissement locatif figurent parmi les plus exposés, tandis que les villes moyennes au foncier abordable amortissent le choc.

Prix : la hausse est-elle enrayée ?

Les prix réagissent avec retard au retournement des volumes : les vendeurs résistent à la baisse avant de céder. Cette partie lit d’abord le niveau — combien coûte le mètre carré, et où — puis l’évolution, pour distinguer les marchés qui poursuivent leur hausse de ceux qui corrigent.

Afficher le code
cat(bloc_cles_srpt("srpt/_srpt-thlog-04-prix.qmd"))

Éléments clés

  • Le rural a le plus renchéri depuis 2015 (indice 128), les communes les plus chères le moins (114) : la hausse a resserré les écarts.
  • Un facteur 11,1 sépare Paris (9 706 €/m²) de Creuse (874 €/m²) — mais ce sont les marchés abordables qui montent le plus vite.

La décennie a joué contre la hiérarchie établie

Le rural a le plus renchéri depuis 2015 (indice 128), les communes les plus chères le moins (114) : la hausse a resserré les écarts.

La décennie écoulée a joué contre la hiérarchie établie. Les territoires les moins chers — communes hors attraction des villes, à 1 727 € le m² en 2025 — ont connu la progression la plus vive, portée par le desserrement résidentiel et la recherche d’espace après 2020. À l’inverse, les communes du pôle les plus valorisées (3 583 € le m²) ont vu leur avance s’éroder.

Le retournement, lui, est visible sur la fin de courbe : après le pic de 2022, les indices s’aplatissent voire refluent, sans effacer les gains accumulés (figure ci-contre).

Afficher le code
log_table_auto("serie_prix_typo", px_idx[px_idx$annee %in% c(2015, 2019, 2022, 2025),
                                         c("type_lib", "annee", "indice")], label_col = "type_lib")
# Même modèle que la courbe transactions : plot_serie_pal lissé + PAL_TYPO_CURVE (databar).
# Pas de barres ici — le volume est déjà porté par la partie transactions.
.pxi <- rbind(px_idx[, c("annee", "indice", "type_lib")] |> stats::setNames(c("annee", "indice", "serie")),
              data.frame(annee = SER_FR$annee, indice = SER_FR$px_ix, serie = "France"))
.pxi$serie <- factor(.pxi$serie, levels = names(PAL_TYPO_CURVE))
.ev_end <- vapply(levels(.pxi$serie), function(l) {
  d <- .pxi[.pxi$serie == l & is.finite(.pxi$indice), ]; d <- d[order(d$annee), ]
  if (!nrow(d)) NA_real_ else d$indice[nrow(d)] - 100 }, numeric(1))
.short <- c("Commune-centre" = "Centre", "Autre commune du pôle" = "Banlieue du pôle",
            "Couronne périurbaine" = "Couronne", "Hors attraction (rural)" = "Rural",
            "France" = "France")
.lab <- stats::setNames(sprintf("%s · %+.0f %%", .short[levels(.pxi$serie)], .ev_end), levels(.pxi$serie))
g_px <- plot_serie_pal(.pxi, x_col = "annee", y_col = "indice", group_col = "serie",
  pal_named = PAL_TYPO_CURVE, lty_named = LTY_TYPO_CURVE,
  smooth = TRUE, hline_ref = 100, interactive = TRUE, point_size = 1.1,
  # label_last_fn OBLIGATOIRE pour activer les etiquettes de fin (le helper ne les trace que si
  # label_last_fn ou emphasis_groups est fourni) ; lab_override remplace ensuite le texte.
  end_inside = TRUE, lab_override = .lab, label_last_fn = function(y) sprintf("%+.0f %%", y - 100),
  title    = "Le rural a rattrapé une partie de son retard sur les pôles",
  subtitle = sprintf("Prix médian au m² · indice base 100 = %d · par type de territoire — DV3F",
                     params$millesime_base),
  y_lab = NULL, x_breaks = seq(2015, 2025, 2))
render_girafe(g_px, width_svg = 9, height_svg = 4.2)

La carte des hausses inverse presque celle des niveaux

Un facteur 11,1 sépare Paris (9 706 €/m²) de Creuse (874 €/m²) — mais ce sont les marchés abordables qui montent le plus vite.

La géographie des prix est celle de la tension : Île-de-France, littoral méditerranéen, façade atlantique et couronnes alpines forment un arc de valeurs élevées, tandis que la diagonale intérieure concentre les valeurs les plus basses. La zone d’emploi révèle des îlots que la moyenne départementale masque — stations littorales ou de montagne enclavées dans des départements abordables (cartes ci-contre).

Afficher le code
bins_px <- auto_bins(c(DEP_LOG$px_25, ZE_LOG$px_25) |> (\(x) x[is.finite(x)])(), var_name = "px_25")
(build_map(dplyr::left_join(GEO_DEP, DEP_LOG, by = c("code_insee" = "code")),
  "px_25", bins = bins_px, label_col = "nom_officiel",
  titre = "Prix médian au m² · 2025", subtitle = "€/m² · départements",
  source_txt = make_source("DV3F"), n_top = 3, n_bottom = 3,
  label_style = "text", show_values = FALSE, legend_keywidth = 0.62, legend_keyheight = 0.2,
  contour_geo = GEO_REG, contour_lw = 0.4, contour_color = "#b0b0b0", dd = DD,
  interactive = TRUE, hover_key = TRUE, legend_drop = FALSE, legend_title = "€/m²") + th_map_sm) |>
  girafe_legend(bins_px, width_svg = 4.5, height_svg = 4.95, title = "€/m²", zoom = TRUE)
€/m²clic & ctrl-clic
Afficher le code
(build_map(dplyr::left_join(GEO_ZE, ZE_LOG, by = c("ze2020" = "code")),
  "px_25", bins = bins_px, label_col = "libze2020",
  titre = "Prix médian au m² · 2025", subtitle = "€/m² · zones d'emploi",
  source_txt = make_source("DV3F"), n_top = 3, n_bottom = 3,
  label_style = "text", show_values = FALSE, legend_keywidth = 0.62, legend_keyheight = 0.2,
  contour_geo = GEO_REG, contour_lw = 0.4, contour_color = "#b0b0b0", dd = DD,
  interactive = TRUE, hover_key = TRUE, legend_drop = FALSE, legend_title = "€/m²") + th_map_sm) |>
  girafe_legend(bins_px, width_svg = 4.5, height_svg = 4.95, title = "€/m²", zoom = TRUE)
€/m²clic & ctrl-clic

Passer du niveau au rythme renverse presque la lecture. Les départements les plus chers affichent les progressions les plus faibles sur six ans, quand l’intérieur rural enregistre les hausses les plus fortes : le rattrapage amorcé pendant la crise sanitaire s’y lit directement, et il explique le resserrement des écarts observé sur les courbes précédentes.

Afficher le code
bins_pe <- auto_bins(c(DEP_LOG$px_tcam_1925, ZE_LOG$px_tcam_1925) |> (\(x) x[is.finite(x)])(),
                     var_name = "px_tcam_1925")
log_map_auto("carte_prix_evol_dep", "px_tcam_1925", DEP_LOG, "libelle")
(build_map(dplyr::left_join(GEO_DEP, DEP_LOG, by = c("code_insee" = "code")),
  "px_tcam_1925", bins = bins_pe, label_col = "nom_officiel",
  titre = "Évolution du prix médian · 2019-2025", subtitle = "%/an · départements",
  source_txt = make_source("DV3F"), n_top = 3, n_bottom = 3,
  label_style = "text", show_values = FALSE, legend_keywidth = 0.62, legend_keyheight = 0.2,
  contour_geo = GEO_REG, contour_lw = 0.4, contour_color = "#b0b0b0", dd = DD,
  interactive = TRUE, hover_key = TRUE, legend_drop = FALSE, legend_title = "%/an") + th_map_sm) |>
  girafe_legend(bins_pe, width_svg = 4.5, height_svg = 4.95, title = "%/an", zoom = TRUE)
%/anclic & ctrl-clic
Afficher le code
(build_map(dplyr::left_join(GEO_ZE, ZE_LOG, by = c("ze2020" = "code")),
  "px_tcam_1925", bins = bins_pe, label_col = "libze2020",
  titre = "Évolution du prix médian · 2019-2025", subtitle = "%/an · zones d'emploi",
  source_txt = make_source("DV3F"), n_top = 3, n_bottom = 3,
  label_style = "text", show_values = FALSE, legend_keywidth = 0.62, legend_keyheight = 0.2,
  contour_geo = GEO_REG, contour_lw = 0.4, contour_color = "#b0b0b0", dd = DD,
  interactive = TRUE, hover_key = TRUE, legend_drop = FALSE, legend_title = "%/an") + th_map_sm) |>
  girafe_legend(bins_pe, width_svg = 4.5, height_svg = 4.95, title = "%/an", zoom = TRUE)
%/anclic & ctrl-clic

Entre grandes villes, un rapport de 1 à 3,3 sur le mètre carré

À l’échelle des grandes villes, le prix médian s’établit à 3 211 € le m². L’écart reste considérable : les 10 % les plus chères dépassent 5 565 € quand les 10 % les plus abordables restent sous 1 667 €, soit un rapport de 3,3 (carte et classement ci-contre).

Afficher le code
log_describe_ranking("comm_prix_25", .cp, "libelle", "px_25")
# Interactif + légende HTML filtrante (même idiome que la synthèse) — l'ancienne légende ggplot
# avec effectifs entre parenthèses est retirée (user 260903).
bins_cm_px <- auto_bins(.cp$px_25[is.finite(.cp$px_25)], var_name = "px_25")
(build_map_points(cp, "px_25", tooltip_lab = "Prix médian au m²", bins = bins_cm_px,
  size_col = "P22_POP", size_range = c(1.3, 9.5), point_border = "#8f8f8f",
  label_col = "libelle", label_style = "text", n_top = 3, n_bottom = 3,
  bg_geo = GEO_DEP, bg_fill = "#f7f6f2", bg_color = "#dedede", bg_lw = 0.22,
  contour_geo = GEO_REG, contour_color = "#c9c9c9", contour_lw = 0.3, interactive = TRUE, dd = DD,
  titre = "Prix médian au m² · 2025",
  subtitle = sprintf("€/m² · communes de plus de %s habitants · taille = population", format(params$seuil_commune, big.mark = " ")),
  source_txt = make_source("DV3F · INSEE RP"), legend_title = "€/m²", show_counts = FALSE,
  terr_lbl = "communes") + th_map_sm) |>
  girafe_legend(bins_cm_px, width_svg = 4.5, height_svg = 4.95, title = "€/m²", zoom = TRUE)
€/m²clic & ctrl-clic
Afficher le code
# Tableau top/bottom à EN-TÊTES GROUPÉS (modèle rt_topbot_ville pbnb, porté en générique dans mutils) :
# niveau | ▲ évol sous un même groupe, footer = médiane des communes puis France.
.cpt <- .cp[, c("libelle", "px_25", "px_tcam_2225", "trans_25", "P22_POP")]
.fr_tr_tcam <- (SER_FR$trans[SER_FR$annee == 2025] / SER_FR$trans[SER_FR$annee == 2022])^(1/3) * 100 - 100
rt_topbot_pairs(.cpt, lib_col = "libelle", lib_label = "Commune",
  vars   = c("px_25", "px_tcam_2225", "trans_25", "P22_POP"),
  labels = c("Prix médian", "Évol.", "Mutations", "Population"),
  units  = c("€/m²", "22-25 %/an", "nb 2025", "hab."),
  digits = c(0, 1, 0, 0), pairs = c(px_tcam_2225 = "px_25"),
  sort_col = "px_25", n_top_bot = 6, n_middle = 0,
  ref_rows = list(
    list(lib = "Médiane des communes", values = c(px_25 = median(.cpt$px_25, na.rm = TRUE), px_tcam_2225 = median(.cpt$px_tcam_2225, na.rm = TRUE), trans_25 = median(.cpt$trans_25, na.rm = TRUE), P22_POP = median(.cpt$P22_POP, na.rm = TRUE))),
    list(lib = "France", values = c(px_25 = FR$logd_px2q2_glb_25[1], px_tcam_2225 = KPI_LOG$px_evol, trans_25 = SER_FR$trans[SER_FR$annee == 2025], P22_POP = NA))),
  title    = "Les villes les plus chères, les plus abordables",
  subtitle = sprintf("6 premières et 6 dernières des %d communes de plus de %s habitants — DV3F, INSEE RP", nrow(.cpt), format(params$seuil_commune, big.mark = " ")),
  footnote = "Barre = position dans le classement · évolution en bordeaux (hausse) / bleu (baisse) · footer : médiane des communes puis France (prix = département médian).")
Les villes les plus chères, les plus abordables
6 premières et 6 dernières des 261 communes de plus de 30 000 habitants — DV3F, INSEE RP
⇅ triable
Barre = position dans le classement · évolution en bordeaux (hausse) / bleu (baisse) · footer : médiane des communes puis France (prix = département médian).

Cette maille est la plus parlante pour un lecteur : elle isole les marchés urbains de leur environnement rural, et fait apparaître des situations que le département noie — une ville moyenne tendue dans un département abordable, ou l’inverse.

Le logement selon huit lectures du territoire

Un même parc se décrit très différemment selon la grille qu’on lui applique. Les quatre catégories d’aires d’attraction utilisées jusqu’ici disent l’essentiel du gradient urbain-rural, mais elles ignorent ce qui structure vraiment un marché du logement : la tension réglementaire, la proximité de la mer, la pression touristique, la taille de la ville.

La correction des prix est proportionnelle à la tension : la zone Abis perd 9,4 % depuis 2022 quand la zone C en gagne 1,4 %.

Le zonage A/B/C — celui qui commande les dispositifs fiscaux et l’encadrement des loyers — donne la lecture la plus nette. Le prix médian va de 6 003 € le m² en zone Abis à 1 630 € en zone C, un facteur 3,7. Et le retournement suit exactement le même ordre : plus un marché était tendu, plus il corrige.

Deux grilles apportent ce que l’urbain-rural ne voit pas. Les communes hyper-touristiques (plus de 40 % de résidences secondaires) affichent 4 185 € le m² — au niveau des grandes agglomérations — et continuent de progresser de 5,6 % quand le reste recule. Le littoral joue le même rôle.

À l’inverse, les bourgs et villages de moins de 3 500 habitants, qui rassemblent 31 618 communes, cumulent le prix le plus bas (1 875 €) et la part de passoires énergétiques la plus élevée (15,5 %) : un parc abordable mais coûteux à remettre aux normes.

Afficher le code
# tx_constr (260903) : logements autorisés moy/an 2022-2025 pour 1 000 logements — la SEULE colonne
# de flux du tableau, elle dit où le parc se renouvelle (même définition que la carte de la partie
# construction, calculée ici depuis la série typo).
.tb <- TYPO_RECAP[, c("bloc", "lib", "n_comm", "px_25", "px_ev", "vac_22", "dpe_fg", "tx_constr")]
names(.tb) <- c("bloc", "Type de territoire", "Communes", "Prix médian",
                "Évol. prix", "Vacance", "Passoires F-G", "Constr. /1000 lgts")
gt_styled(.tb, groupname_col = "bloc",
  title = "Le zonage de tension classe les marchés mieux que l'opposition urbain-rural",
  subtitle = "Prix médian au m² 2025 · évolution totale 2022-2025 · parc RP 2023 · DPE 2026 · logements autorisés/an 2022-2025 pour 1 000 logements",
  source = "DV3F · INSEE RP · ADEME · SDES Sitadel 3 · grille de typologies TDC-ref") |>
  gt::fmt_number(c("Communes"), decimals = 0, sep_mark = " ") |>
  gt::fmt_number(c("Prix médian"), decimals = 0, sep_mark = " ", pattern = "{x} €") |>
  gt::fmt_number(c("Évol. prix", "Vacance", "Passoires F-G"), decimals = 1,
                 dec_mark = ",", pattern = "{x} %") |>
  gt::fmt_number(c("Constr. /1000 lgts"), decimals = 1, dec_mark = ",") |>
  gt::sub_missing(columns = "Constr. /1000 lgts", missing_text = "—") |>
  gt::data_color(columns = "Évol. prix", palette = c("#2166ac", "#f7f7f7", "#b2182b"),
                 domain = c(-10, 10)) |>
  gt::tab_options(quarto.disable_processing = TRUE)
Le zonage de tension classe les marchés mieux que l’opposition urbain-rural
Prix médian au m² 2025 · évolution totale 2022-2025 · parc RP 2023 · DPE 2026 · logements autorisés/an 2022-2025 pour 1 000 logements
Type de territoire Communes Prix médian Évol. prix Vacance Passoires F-G Constr. /1000 lgts
References
Petite couronne (92-93-94) — communes limitrophes Paris 27 6 857 € −10,3 % 6,9 % 11,8 % 11,4
Ruralites & centralites
rural periurbain (AAV>=50k) 14 449 2 039 € −1,4 % 7,3 % 12,2 % 9,3
rural non periurbain 16 147 1 658 € 2,2 % 9,1 % 18,2 % 6,8
Rural (peu dense) — isole : hors attraction villes 8 897 1 727 € 2,6 % 8,8 % 19,6 % 6,7
dense 741 3 551 € −5,7 % 7,8 % 8,3 % 9,3
intermediaire 3 580 2 518 € −3,0 % 7,6 % 7,4 % 11,7
Geographie & pression touristique
Zones touristiques — touristique modere (15-40%) 7 673 2 656 € 4,3 % 8,4 % 13,8 % 7,5
Zones touristiques — hyper-touristique (>40%) 2 513 4 185 € 5,6 % 3,1 % 11,4 % 8,3
Aires d attraction
Paris — pole 355 5 238 € −4,3 % 7,2 % 11,8 % 9,7
Paris — couronne 1 590 2 730 € −8,5 % 6,7 % 11,5 % 9,8
Aires >=700k hors Paris — pole 413 3 060 € −3,8 % 7,9 % 5,8 % 9,2
Aires >=700k hors Paris — couronne 7 953 2 622 € −3,2 % 6,3 % 7,5 % 12,1
Aires 200-700k — pole 224 1 946 € −0,5 % 9,5 % 7,2 % 10,0
Aires 200-700k — couronne 7 392 1 846 € −0,1 % 7,6 % 11,7 % 8,7
Autres aires 7 825 1 824 € 1,4 % 9,1 % 12,6 % 8,1
Tension immobiliere
Tendu (Abis+A) 1 001 4 179 € −4,2 % 6,2 % 7,4 % 11,3
Zone Abis 132 6 003 € −9,4 % 6,6 % 11,7 % 12,0
Zone A 869 3 881 € −4,2 % 6,2 % 6,1 % 11,1
Intermediaire (B1+B2) 5 545 2 429 € −2,8 % 7,6 % 7,3 % 11,6
Zone B1 2 383 2 667 € −3,0 % 7,5 % 6,6 % 12,6
Zone B2 3 162 1 899 € −1,3 % 8,0 % 9,1 % 9,2
Detendu (C) 28 326 1 630 € 1,4 % 9,2 % 14,8 % 7,2
Taille de ville
Bourg ou village (<3.5k) 31 618 1 875 € 0,0 % 7,9 % 15,5 % 7,8
Petite commune (3.5-10k) 2 145 2 559 € −3,5 % 7,2 % 8,1 % 12,2
Petite ville (10-20k) 529 2 761 € −4,7 % 7,0 % 7,1 % 11,4
Ville moyenne (20-50k) 346 3 111 € −6,4 % 7,6 % 7,9 % 10,3
Ville (50-100k) 98 3 587 € −4,7 % 7,4 % 7,4 % 10,3
Grande ville (100-200k) 39 3 176 € −4,1 % 9,4 % 10,0 % 6,2
Metropole (>=200k) 9 3 927 € −3,9 % 8,3 % 6,4 % 8,5
Centralites INRAE
Centre majeur 175 2 975 € −2,4 % 9,2 % 8,4 % 7,3
Centre structurant 742 3 337 € −6,4 % 7,3 % 7,9 % 11,4
Centre intermediaire 2 877 2 601 € −3,8 % 7,1 % 8,4 % 11,6
Centre local 6 999 2 171 € −1,3 % 7,1 % 12,0 % 9,7
Centres fragiles (score 31-45) 2 456 1 547 € 0,8 % 11,3 % 12,3 % 6,4
Centres mod. fragiles (16-30) 5 480 2 560 € −2,0 % 7,4 % 7,4 % 10,2
Centres peu fragiles (0-15) 2 851 3 673 € −7,4 % 6,5 % 9,0 % 11,4
Programmes nationaux
France Ruralites Revitalisation 19 979 1 818 € 2,5 % 9,8 % 14,8 % 5,8
Plus Beaux Villages de France 191 2 166 € 7,1 % 8,3 % 15,3 % 7,0
Petites Cites de Caractere 206 1 863 € 2,0 % 9,5 % 12,7 % 8,4
Station Classee de Tourisme 472 3 667 € 1,6 % 6,9 % 6,6 % 9,4
Commune Touristique 1 096 2 505 € −1,7 % 8,1 % 7,5 % 10,4
Ville Art et Histoire 118 2 722 € −1,3 % 9,2 % 6,6 % 8,8
Ville Fleurie 4 fleurs 75 2 500 € −1,9 % 8,9 % 7,2 % 10,9
DV3F · INSEE RP · ADEME · SDES Sitadel 3 · grille de typologies TDC-ref

Lecture : les lignes agrégées de la grille de référence (« Zones touristiques » = modérée et hyper-touristique réunies) sont écartées — on peut sommer des stocks, pas des médianes de prix. Les programmes Petites Villes de Demain et Action Cœur de Ville figurent dans la grille mais n’ont aucune donnée de prix dans la source : ils n’apparaissent donc pas.

Ces grilles disent où le marché se tend, se détend ou décroche. L’état physique du parc — vacance, ancienneté énergétique — suit-il les mêmes lignes de partage ?

Parc et énergie : quel état du bâti ?

Au-delà des prix et des volumes, la valeur d’un marché tient à l’état de son parc : un logement vacant ne se vend pas, une passoire énergétique se décote et sortira du marché locatif. Cette partie mesure ces contraintes structurelles et leur géographie.

Afficher le code
cat(bloc_cles_srpt("srpt/_srpt-thlog-05-parc-energie.qmd"))

Éléments clés

  • Le rural cumule les handicaps : 8,8 % de logements vacants et 19,6 % de passoires énergétiques, contre 9,6 % et 7,3 % dans les communes-centres.
  • La part de passoires recule à mesure que le prix au m² monte : les territoires valorisés disposent d’un parc plus récent ou davantage rénové.

Vacance et passoires : deux fragilités qui se cumulent dans le rural

Le rural cumule les handicaps : 8,8 % de logements vacants et 19,6 % de passoires énergétiques, contre 9,6 % et 7,3 % dans les communes-centres.

La vacance signale un parc qui ne trouve pas preneur — logements trop anciens, mal situés ou trop coûteux à remettre en état. Elle est structurellement plus élevée hors des aires d’attraction, où la demande est faible et le bâti ancien.

Les passoires énergétiques (étiquettes F et G) suivent la même géographie, pour une raison mécanique : le rural concentre les maisons individuelles construites avant les premières réglementations thermiques. Avec le calendrier d’interdiction de location, ce parc devient invendable au prix d’hier sans travaux — un facteur de décote qui pèsera d’autant plus que la demande locale est déjà faible.

Afficher le code
log_map_auto("carte_dpe_ze", "logd_dpe_fg_pct_26", ZE, "libelle")
build_map(dplyr::left_join(GEO_ZE, ZE[, c("code", "logd_dpe_fg_pct_26")], by = c("ze2020" = "code")),
  "logd_dpe_fg_pct_26", bin_type = "auto", label_col = "libze2020",
  titre = "Les passoires énergétiques dessinent la France rurale et de montagne",
  subtitle = "Part des logements classés F ou G · 2026 (%)",
  source_txt = make_source("ADEME DPE"), n_top = 4, n_bottom = 4,
  label_style = "text", show_values = FALSE, legend_keywidth = 0.62, legend_keyheight = 0.2,
  contour_geo = GEO_REG, contour_lw = 0.4, contour_color = "#b0b0b0", dd = DD) + th_map_sm

Lecture : part des diagnostics de performance énergétique classés F ou G dans le parc diagnostiqué de la zone d’emploi. Un taux élevé signale un parc ancien exposé au calendrier d’interdiction de location de la loi Climat et résilience.

Prix et performance énergétique : les marchés chers ont le parc le plus sain

La part de passoires recule à mesure que le prix au m² monte : les territoires valorisés disposent d’un parc plus récent ou davantage rénové.

Le croisement confirme un cercle qui s’auto-entretient. Un parc performant soutient la valeur, la valeur finance la rénovation. À l’inverse, un parc dégradé décote, et la décote décourage l’investissement de rénovation — piège dont les marchés détendus peinent à sortir.

Afficher le code
ds <- dplyr::left_join(ZE_LOG[, c("code", "libelle", "px_25")],
                       ZE[, c("code", "logd_dpe_fg_pct_26", "P22_POP")], by = "code")
ds <- ds[is.finite(ds$px_25) & is.finite(ds$logd_dpe_fg_pct_26), ]
log_scatter_auto("scatter_prix_dpe", ds, "px_25", "logd_dpe_fg_pct_26", "libelle")
plot_scatter(ds, x = "px_25", y = "logd_dpe_fg_pct_26",
  label_col = "libelle", size_col = "P22_POP",
  n_labels = 10, label_mode = "quadrant_extremes",
  ref_lines = "none", mean_lines = TRUE, engine = "ggplot2",
  title    = "Plus le marché est cher, moins le parc compte de passoires",
  subtitle = "Prix médian m² 2025 × part d'étiquettes F-G 2026 · zones d'emploi · taille = population",
  xlab = "Prix médian au m² 2025 (€) →", ylab = "Part de passoires F-G (%)",
  caption = make_source("DV3F · ADEME"))

La construction neuve : un rebond qui part de très bas

Le parc décrit dans la partie précédente est un stock ; la construction est le robinet qui l’alimente. Deux questions structurent cette partie : le retournement de 2022 a-t-il fermé ce robinet durablement, et l’effort de construction se porte-t-il là où le parc est déjà tendu ?

Afficher le code
cat(bloc_cles_srpt("srpt/_srpt-thlog-08-construction.qmd"))

Éléments clés

  • 348 000 logements autorisés en 2025 : un rebond de 15 % en un an, après une chute d’un tiers entre 2022 et 2024.
  • La médiane départementale s’établit à 8,8 logement autorisé par an pour 1 000 logements existants — Val-de-Marne en produit 2,0 fois plus.

Le retournement a fermé le robinet, 2025 le rouvre à peine

348 000 logements autorisés en 2025 : un rebond de 15 % en un an, après une chute d’un tiers entre 2022 et 2024.

La remontée des taux a frappé la production neuve encore plus fort que les transactions : entre 2022 et 2024, les autorisations perdent 31,3 %, le point bas de toute la série. Le rebond de 2025 (+15,2 % sur un an) est réel mais part de très bas : le niveau reste 20,8 % sous celui de 2022 et 23,3 % sous le pic de 2017 (figure ci-contre). La courbe des mises en chantier suit le même profil avec un étage de moins — une part des permis n’est jamais mise en œuvre, et le chantier suit l’autorisation avec des mois de délai.

Afficher le code
.sc <- SER_CONSTR
ggplot(.sc, aes(x = annee)) +
  geom_col(aes(y = logaut, fill = "Logements autorisés"), width = 0.62) +
  geom_line(aes(y = logcom, colour = "Logements commencés"), linewidth = 0.9, na.rm = TRUE) +
  geom_point(aes(y = logcom, colour = "Logements commencés"), size = 1.7, na.rm = TRUE) +
  scale_fill_manual(values = c("Logements autorisés" = "#b08d57")) +
  scale_colour_manual(values = c("Logements commencés" = "#54595f")) +
  scale_x_continuous(breaks = .sc$annee) +
  scale_y_continuous(labels = function(x) paste0(x / 1000, " k")) +
  labs(title = "Un tiers d'autorisations en moins entre 2022 et 2024, un premier rebond en 2025",
       subtitle = "Logements autorisés (barres) et commencés (courbe) · France métropolitaine · données collectées, non estimées",
       x = NULL, y = NULL, fill = NULL, colour = NULL,
       caption = make_source("SDES Sitadel 3, extraction 08/2026")) +
  theme_urbn() + theme(legend.position = "top")

Lecture : logements autorisés = permis délivrés, l’indicateur le plus exhaustif au niveau local ; logements commencés = chantiers ouverts, remontés avec un délai d’environ dix-huit mois — le millésime 2025 n’est pas encore collecté et n’est donc pas affiché. Série SDES en base Sitadel 3 (bascule mars 2026) : niveaux non comparables aux chiffres publiés avant cette date.

L’effort de construction fuit les marchés déjà tendus

La médiane départementale s’établit à 8,8 logement autorisé par an pour 1 000 logements existants — Val-de-Marne en produit 2,0 fois plus.

Rapportées au parc existant, les autorisations 2022-2025 dessinent une géographie qui ne recouvre ni celle des prix ni celle de la tension : l’effort le plus intense se lit dans Val-de-Marne, Haute-Savoie et Haute-Corse (17,3 à 15,4 pour 1 000), quand Paris, Nièvre et Haute-Marne renouvellent leur parc deux à trois fois moins vite (cartes ci-contre). La maille des zones d’emploi affine le constat : les bassins littoraux et périurbains concentrent la production, les bassins de tradition industrielle et les centres les plus chers construisent peu.

Afficher le code
log_map_auto("carte_constr_tx_dep", "logs_logaut_tx1klog22_2225", CT_DEP, "libelle")
.m_ct_d <- build_map(.geo_dep_ct, "logs_logaut_tx1klog22_2225",
    tooltip_lab = "Logements autorisés /1000 logements", bins = ct_bins_dep, label_col = "nom_officiel",
    interactive = TRUE, hover_key = TRUE, legend_drop = FALSE, show_counts = FALSE,
    titre = "Taux de construction : logements autorisés pour 1 000 logements · 2022-2025",
    subtitle = "moyenne annuelle /1000 logements (parc 2022) · départements",
    source_txt = NULL, n_top = 3, n_bottom = 2, show_values = FALSE,
    label_style = "text", legend_title = "/1000",
    legend_keywidth = 0.5, legend_keyheight = 0.17,
    contour_geo = GEO_REG, contour_lw = 0.35, contour_color = "#b0b0b0", dd = DD,
    zoom_box = BBOX_IDF) + th_map_sm
map_inset(.m_ct_d, .geo_dep_ct, "logs_logaut_tx1klog22_2225", codes = INSET_IDF_PC, bins = ct_bins_dep,
          code_col = "code_insee", label_col = "nom_officiel", dd = DD,
          points = FALSE, interactive = TRUE, pos = "topright") |>
  girafe_legend(ct_bins_dep, width_svg = 4.4, height_svg = 4.35, title = "/1000", zoom = TRUE)
/1000clic & ctrl-clic
Afficher le code
log_map_auto("carte_constr_tx_ze", "logs_logaut_tx1klog22_2225", CT_ZE, "libelle")
.m_ct_z <- build_map(.geo_ze_ct, "logs_logaut_tx1klog22_2225",
    tooltip_lab = "Logements autorisés /1000 logements", bins = ct_bins_ze, label_col = "libelle",
    interactive = TRUE, hover_key = TRUE, legend_drop = FALSE, show_counts = FALSE,
    titre = "Taux de construction : logements autorisés pour 1 000 logements · 2022-2025",
    subtitle = "moyenne annuelle /1000 logements (parc 2022) · zones d'emploi",
    source_txt = NULL, n_top = 3, n_bottom = 2, show_values = FALSE,
    label_style = "text", legend_title = "/1000",
    legend_keywidth = 0.5, legend_keyheight = 0.17,
    contour_geo = GEO_REG, contour_lw = 0.35, contour_color = "#b0b0b0", dd = DD) + th_map_sm
girafe_legend(.m_ct_z, ct_bins_ze, width_svg = 4.4, height_svg = 4.35, title = "/1000", zoom = TRUE)
/1000clic & ctrl-clic

Lecture : le taux rapporte la production au parc existant (et non aux habitants) — il se lit comme un rythme de renouvellement du stock, et il ne flatte pas les territoires à forte vacance. Chaque maille a sa propre échelle de couleurs. Survol ou clic sur la légende pour isoler une classe. Ce sont les seules cartes du rapport sans zone blanche : Sitadel couvre la France entière, quand la Moselle, le Bas-Rhin et le Haut-Rhin sont absents des cartes de prix et de transactions — ces trois départements relèvent du livre foncier d’Alsace-Moselle et non du fichier immobilier, donc des bases DVF.

Construire, c’est aussi consommer du sol. Sur la décennie de référence de la loi (2011-2021), le département médian a consommé 0,40 % de son territoire en espaces naturels, agricoles ou forestiers ; depuis 2021 le rythme recule de 3,4 % par an — la sobriété foncière est engagée. Rapportée à ce sol consommé pour l’habitat, la production neuve 2021-2025 atteint 2 200 m² de plancher par hectare dans le département médian ; 4 départements n’ont pas de valeur — en Île-de-France dense, on construit sans consommer d’espaces naturels, et le ratio cesse d’être une mesure. La consommation d’espaces a sa propre logique, ses propres sources et sa propre loi : elle appelle un bloc à part entière.

Point mort : à quoi a servi ce qui a été construit ?

Sur 2017-2023, -39 % de la construction française a servi à maintenir la population en place — pas à en accueillir.

Le point mort est le nombre de logements qu’il faut produire juste pour que rien ne change : compenser le desserrement des ménages (4 006 000 logements sur la fenêtre), la disparition ou transformation de logements, et l’absorption par la vacance (144 000) et les résidences secondaires (847 000). Sur 2,04 million de logements commencés entre 2016 et 2021, seuls 2 834 000 ont effectivement servi à loger de la population supplémentaire — un ratio production / point mort de -2,59.

Décomposition standard des programmes locaux de l’habitat (PLH) : construits = desserrement + renouvellement + Δ vacance + Δ résidences secondaires + effet démographique. Le desserrement = logements nécessaires pour loger la population de départ à la taille des ménages d’arrivée ; le renouvellement = construits − variation du parc total (il se calcule et peut être négatif : divisions de logements, transformations de locaux). Fenêtre : recensements 2017 et 2023, numérateur = logements commencés cumulés 2017-2022 (Sitadel 3, données collectées). Le bouclage des cinq composantes redonne exactement les logements commencés. Le point mort est une méthode, pas une norme officielle : les composantes varient légèrement selon les bureaux d’études — la formule utilisée ici est celle ci-dessus, sans ajustement.

Le retournement : qui décroche depuis 2022 ?

Les parties précédentes ont établi le double mouvement — volumes en chute, prix qui s’aplatissent. Reste à savoir qui décroche. En comparant deux fenêtres de trois ans, 2019-2022 puis 2022-2025, on distingue les territoires qui ont simplement ralenti de ceux qui ont vraiment basculé.

Afficher le code
cat(bloc_cles_srpt("srpt/_srpt-thlog-06-retournement.qmd"))

Éléments clés

  • Le rythme des prix passe de +5,7 % à +0,2 % par an entre les deux périodes : la hausse ne ralentit pas, elle s’arrête.
  • Les marchés les plus chers corrigent, les moins chers tiennent : le retournement rebat la hiérarchie construite pendant la décennie de hausse.

Deux temps du marché : la hausse s’est arrêtée net

Le rythme des prix passe de +5,7 % à +0,2 % par an entre les deux périodes : la hausse ne ralentit pas, elle s’arrête.

L’écart entre les deux fenêtres est sans ambiguïté. Sur 2019-2022, la quasi-totalité des départements progressaient de plus de 4 % par an. Sur 2022-2025, la médiane tombe à +0,2 % et 41 départements passent en territoire négatif.

Le nuage ci-contre croise les deux rythmes : chaque point sous la diagonale a ralenti, et tout point sous l’axe horizontal baisse désormais. La dispersion verticale montre que le freinage n’est pas uniforme — c’est là que se lit la différenciation territoriale.

Afficher le code
log_scatter_auto("scatter_retourn_dep", dd_ok, "px_tcam_2225", "px_tcam_1922", "libelle")
plot_scatter(dd_ok, x = "px_tcam_2225", y = "px_tcam_1922",
  label_col = "libelle", size_col = "px_25",
  n_labels = 12, label_mode = "quadrant_extremes",
  ref_lines = "auto", ref_x = 0, ref_y = 0, mean_lines = FALSE, engine = "ggplot2",
  title    = "La hausse d'hier ne garantit pas la tenue d'aujourd'hui",
  subtitle = sprintf("Prix médian m² · %%/an %d-%d (vertical) et %d-%d (horizontal) · départements · taille = niveau de prix",
                     params$retourn_p1[1], params$retourn_p1[2], params$retourn_p2[1], params$retourn_p2[2]),
  xlab = sprintf("Rythme %d-%d (%%/an) →", params$retourn_p2[1], params$retourn_p2[2]),
  ylab = sprintf("Rythme %d-%d (%%/an)", params$retourn_p1[1], params$retourn_p1[2]),
  caption = make_source("DV3F"))

Afficher le code
zz <- ZE_LOG[is.finite(ZE_LOG$px_tcam_1922) & is.finite(ZE_LOG$px_tcam_2225), ]
plot_scatter(zz, x = "px_tcam_2225", y = "px_tcam_1922",
  label_col = "libelle", size_col = "px_25",
  n_labels = 12, label_mode = "quadrant_extremes",
  ref_lines = "auto", ref_x = 0, ref_y = 0, mean_lines = FALSE, engine = "ggplot2",
  title    = "À l'échelle des bassins, le freinage se lit plus contrasté",
  subtitle = "Prix médian m² · rythmes comparés · zones d'emploi · taille = niveau de prix",
  xlab = sprintf("Rythme %d-%d (%%/an) →", params$retourn_p2[1], params$retourn_p2[2]),
  ylab = sprintf("Rythme %d-%d (%%/an)", params$retourn_p1[1], params$retourn_p1[2]),
  caption = make_source("DV3F"))

Afficher le code
cc <- sf::st_drop_geometry(COMM_PTS)
cc <- cc[is.finite(cc$px_tcam_1922) & is.finite(cc$px_tcam_2225), ]
plot_scatter(cc, x = "px_tcam_2225", y = "px_tcam_1922",
  label_col = "libelle", size_col = "P22_POP",
  n_labels = 12, label_mode = "quadrant_extremes",
  ref_lines = "auto", ref_x = 0, ref_y = 0, mean_lines = FALSE, engine = "ggplot2",
  title    = "Les grandes villes concentrent les corrections les plus nettes",
  subtitle = sprintf("Prix médian m² · rythmes comparés · %d communes de plus de %s habitants", nrow(cc), format(params$seuil_commune, big.mark = " ")),
  xlab = sprintf("Rythme %d-%d (%%/an) →", params$retourn_p2[1], params$retourn_p2[2]),
  ylab = sprintf("Rythme %d-%d (%%/an)", params$retourn_p1[1], params$retourn_p1[2]),
  caption = make_source("DV3F · INSEE RP"))

Lecture : chaque point est un territoire. En bas à droite, les marchés qui accélèrent ou tiennent ; en haut à gauche, ceux qui montaient fort et corrigent le plus brutalement. La diagonale sépare l’accélération du ralentissement.

La même bascule se lit en croisant prix et volumes sur chaque fenêtre : pendant la reprise, 78 départements combinaient hausse des prix et hausse des ventes ; depuis 2022, le nuage entier a glissé vers le quadrant opposé — 41 départements voient prix et transactions reculer ensemble.

Afficher le code
plot_scatter(.bb, x = "trans_tcam_1922", y = "px_tcam_1922",
  label_col = "libelle", size_col = "trans_25",
  n_labels = 10, label_mode = "quadrant_extremes",
  ref_lines = "auto", ref_x = 0, ref_y = 0, mean_lines = FALSE, engine = "ggplot2",
  title    = sprintf("%d départements en double hausse : prix et ventes montaient ensemble", .n_dd_19),
  subtitle = "Évolution des transactions (horizontal) et du prix médian (vertical) · %/an 2019-2022 · départements · bulle = mutations 2025",
  xlab = "Transactions (%/an) →", ylab = "Prix médian (%/an)",
  caption = make_source("DV3F"))

Afficher le code
plot_scatter(.bb, x = "trans_tcam_2225", y = "px_tcam_2225",
  label_col = "libelle", size_col = "trans_25",
  n_labels = 10, label_mode = "quadrant_extremes",
  ref_lines = "auto", ref_x = 0, ref_y = 0, mean_lines = FALSE, engine = "ggplot2",
  title    = sprintf("%d départements en double baisse : le nuage entier a changé de quadrant", .n_dd_22),
  subtitle = "Évolution des transactions (horizontal) et du prix médian (vertical) · %/an 2022-2025 · départements · bulle = mutations 2025",
  xlab = "Transactions (%/an) →", ylab = "Prix médian (%/an)",
  caption = make_source("DV3F"))

Qui décroche, qui résiste ?

Les marchés les plus chers corrigent, les moins chers tiennent : le retournement rebat la hiérarchie construite pendant la décennie de hausse.

Le décrochage le plus marqué frappe Loire-Atlantique, Rhône et Finistère — des marchés qui perdent respectivement 12,1, 10,9 et 10,6 points de rythme annuel entre les deux périodes.

À l’inverse, Hautes-Alpes, Ardennes et Aveyron maintiennent une progression de +4,2 % à +2,6 % par an. Le point commun de ces marchés résistants : un niveau de prix bas, donc une contrainte de financement moins mordante pour les acquéreurs.

Afficher le code
sel <- rbind(head(dd_ok[order(dd_ok$bascule), ], 8), head(dd_ok[order(-dd_ok$bascule), ], 8))
sel$lib <- sel$libelle
log_describe_ranking("arrow_retourn_dep", sel, "libelle", "bascule")
plot_arrow(sel[order(sel$px_tcam_2225), ], "px_tcam_1922", "px_tcam_2225",
  title    = "Les huit départements qui freinent le plus, face aux huit qui tiennent",
  subtitle = sprintf("Prix médian m² · rythme %d-%d (rond) puis %d-%d (pointe), en %%/an",
                     params$retourn_p1[1], params$retourn_p1[2], params$retourn_p2[1], params$retourn_p2[2]),
  xlab     = "Rythme annuel des prix (%/an)",
  caption  = make_source("DV3F"), n_highlight = 3, n_detail = 2)

Tableau de synthèse par type de territoire

Le tableau referme la partie en réunissant, pour chaque type de territoire, les indicateurs travaillés dans les quatre thématiques : niveau de prix, dynamique, vacance, propriétaires, performance énergétique.

Afficher le code
log_table_auto("synth_typo_logement", TYPO_LOG, label_col = "type_lib")
tbl_typo_transpose(
  TYPO_LOG, TBL_INDICS_LOG,
  focus_lab = "France métropolitaine",
  title    = "Le logement par type de territoire, toutes dimensions",
  subtitle = "France métropolitaine · prix et transactions DV3F 2025, parc RP 2023, DPE 2026",
  source   = paste0(make_source("DV3F · INSEE RP · ADEME"),
                    " · barre = position relative dans la colonne · ● polarité"))
Le logement par type de territoire, toutes dimensions
France métropolitaine · prix et transactions DV3F 2025, parc RP 2023, DPE 2026
Type de territoire
.
● Prix m2 global
€/m² · 2025
● △ Prix m² glob
%/an · 22-25
● Nb transactions
nb · 2025
● Tx vacance
% · 2022
● % Propriétaires
% · 2022
● % Passoires F+G
% · 2026
Commune-centre
2 400,0
-0,4
207 546,0
9,6
42,3
7,3
Autre commune du pôle
3 583,3
-2,9
157 725,0
6,3
49,4
8,0
Couronne périurbaine
2 279,4
-0,7
299 767,0
7,2
71,7
9,9
Hors attraction (rural)
1 727,3
+0,9
61 100,0
8,8
74,5
19,6
France métropolitaine
2 615,9
-1,1
726 138,0
7,8
59,0
8,9
Source : DV3F · INSEE RP · ADEME · barre = position relative dans la colonne · ● polarité

Lecture : chaque colonne est un indicateur ; la barre situe le type de territoire au sein de la colonne. La couleur de remplissage suit la polarité (vert = favorable si élevé, bordeaux = défavorable, gris = neutre). La ligne France métropolitaine sert de référence.

4 Le territoire : Seine-Saint-Denis

Seine-Saint-Denis : le marché local du logement

Afficher le code
# PROSE SEMI-AUTO (pipe ccom, cf _thnat-ccom.R). Trois régimes par clé : rien dans les sidecars =
# cette auto-prose ; section `+auto` = auto PUIS commentaire curé ; section nue = le curé remplace.
# ⚠️ glue : `]{{.insight}}` produit `]{.insight}` — une accolade simple serait lue comme du code.
cat(ccom_draft_legend())
cat(prose_cc("terr_intro", function() glue::glue(
  "Après le cadrage national, ce volet situe **{params$territoire_focus}** dans le retournement ",
  "du marché du logement, sur les mêmes fenêtres que la France — la reprise jusqu'en 2022, ",
  "l'ajustement depuis. La synthèse ci-dessous se lit seule ; les parties qui suivent détaillent ",
  "la trajectoire, la position parmi les {tt_maille_pl} comparables, puis l'ampleur de la bascule.

"
)))

La Seine-Saint-Denis occupe une place singulière dans le retournement : département le plus jeune et le plus dense de France métropolitaine hors Paris, elle cumule un marché tendu par la demande et un parc largement social. Ce volet suit les mêmes fenêtres que le cadrage national — la reprise jusqu’en 2022, l’ajustement depuis — pour situer ce marché parmi ses pairs franciliens d’abord, dans la France ensuite. Son niveau de prix la place au 7ᵉ rang des 93 départements français, mais son marché tourne moins vite que celui de ses voisins : 17,1 ventes pour 1 000 logements, contre 21,4 pour la médiane francilienne.

Indicateurs clés · Logement · Seine-Saint-Denis Niveau 2025 et dynamique depuis 2022 · repère France en gris

Afficher le code
# Bandeau KPI — mêmes tuiles que la synthèse nationale, avec UNE DIFFÉRENCE assumée : chaque tuile
# porte la VALEUR FRANCE en pied. Le national n'a pas ce besoin (il EST le repère) ; en territorial
# une valeur locale seule ne dit rien. C'est le point où le volet doit diverger, pas copier. 260903
.dl  <- function(t) if (is.finite(t)) kpi_evo(t, "%",  ref = NULL, arrow = FALSE) else ""
.dlp <- function(t) if (is.finite(t)) kpi_evo(t, "pt", ref = NULL, arrow = FALSE) else ""
.ref_line <- function(txt) sprintf(
  "<div style='font-size:.67rem;color:#8a8a8a;border-top:1px solid #e4e4e4;padding-top:3px;margin-top:2px;line-height:1.25'><span style='color:#aaa'>France</span> %s</div>", txt)
# Forme COURTE de l'évolution pour ces lignes de repère : « ▼ 3,3 % ». Le libellé complet
# (« un recul de 3,3 % ») passait sur deux lignes dans une tuile de 155 px.
.evc <- function(v) if (!is.finite(v)) "" else
  sprintf("%s %s %%", if (v < 0) "▼" else "▲", fmt_fr(abs(v), 1))

htmltools::tagList(kpi_row(cols = 4,
  kpi_tiles_synth(kcat("currency-euro", "Prix médian au m²"),
    sub = "2025 · évol / 2022", compact = TRUE, delta_style = "inline",
    items = list(
      list(value = paste0(fmt_fr(tt_px25, 0), " €"), label = "Ensemble", delta = .dl(tt_px_ev2225)),
      list(value = paste0(fmt_fr(.gv("pxm2_apt", 2025), 0), " €"), label = "Appart.",
           delta = .dl(tt_pxa_ev)),
      list(value = paste0(fmt_fr(.gv("pxm2_mai", 2025), 0), " €"), label = "Maison",
           delta = .dl(tt_pxm_ev))),
    footer = paste0(
      if (nrow(.spx)) as.character(kpi_sparkline(.spx$indice[order(.spx$annee)], w = 150, h = 40,
            annot = TRUE, lab_left = as.character(tt_base_px), lab_right = "2025",
            annot_fmt = function(v) formatC(v, format = "f", digits = 0))) else "",
      .ref_line(sprintf("%s € · %s", fmt_fr(KPI_LOG$px_25, 0), .evc(fr_px_t2225))))),

  kpi_tiles_synth(kcat("arrow-left-right", "Transactions immobilières"),
    sub = "Mutations 2025 · évol / 2022", compact = TRUE, delta_style = "inline",
    items = list(
      list(value = fmt_k(tt_tr25), label = "Ensemble", delta = .dl(tt_tr_ev2225)),
      list(value = fmt_k(.gv("nbtrans_apt", 2025)), label = "Appart.", delta = .dl(tt_tra_ev)),
      list(value = fmt_k(.gv("nbtrans_mai", 2025)), label = "Maisons", delta = .dl(tt_trm_ev))),
    footer = paste0(
      if (nrow(.str)) as.character(kpi_sparkline(.str$nbtrans_glb[order(.str$annee)],
            col1 = "#8a939b", area = TRUE, zero_base = TRUE, w = 150, h = 40, annot = TRUE,
            lab_left = as.character(min(.str$annee)), lab_right = "2025", annot_fmt = fmt_k)) else "",
      .ref_line(sprintf("%s mutations · %s", fmt_k(KPI_LOG$trans_25), .evc(fr_tr_t2225))))),

  kpi_tiles_synth(kcat("house-door", "État du parc"),
    sub = "RP 2023 · DPE 2026 · évol / RP 2017 (pts)", compact = TRUE, delta_style = "inline",
    items = list(
      list(value = paste0(fmt_fr(tt_vac, 1), " %"), label = "Logements vacants",
           delta = .dlp(tt_vac_dif)),
      list(value = paste0(fmt_fr(tt_prop, 1), " %"), label = "Propriétaires",
           delta = .dlp(tt_prop_dif)),
      list(value = paste0(fmt_fr(tt_dpe, 1), " %"), label = "Passoires F-G")),
    footer = .ref_line(sprintf("%s %% vac. · %s %% prop. · %s %% passoires",
                               fmt_fr(KPI_LOG$vac, 1), fmt_fr(KPI_LOG$prop, 1), fmt_fr(KPI_LOG$dpe_fg, 1)))),

  # ⚠️ AUTORISÉS, pas commencés — délai permis→chantier, et une part des permis reste sans suite.
  kpi_tiles_synth(kcat("building", "Logements autorisés"),
    sub = "2025 · évol / 2022 / 2024", compact = TRUE, delta_style = "inline",
    items = list(
      list(value = fmt_k(tt_aut25), label = "Ensemble",
           delta = .dl(tt_aut_ev),  delta2 = .dl(tt_aut_ev24)),
      list(value = fmt_k(tt_auti25), label = "Individuel",
           delta = .dl(tt_auti_ev), delta2 = .dl(tt_auti_ev24)),
      list(value = fmt_k(tt_m2_25), label = "m² autorisés",
           delta = .dl(tt_m2_ev),   delta2 = .dl(tt_m2_ev24))),
    footer = paste0(
      if (nrow(.sau)) as.character(kpi_sparkline(.sau$logs_logaut[order(.sau$annee)],
            col1 = "#8a6d3b", area = TRUE, zero_base = TRUE, w = 150, h = 40, annot = TRUE,
            lab_left = as.character(min(.sau$annee)), lab_right = "2025", annot_fmt = fmt_k)) else "",
      .ref_line(sprintf("%s autorisés · %s", fmt_k(KPI_LOG$constr_25), .evc(KPI_LOG$constr_ev)))))),
  kpi_source(sprintf("DV3F · INSEE RP · ADEME DPE · SDES SITADEL — %s", tt_lbl)))
Prix médian au m² 2025 · évol / 2022
3 980 € Ensemble -8,2 %
4 052 € Appart. -7,3 %
3 841 € Maison -9,9 %
Transactions immobilières Mutations 2025 · évol / 2022
12 k Ensemble -31,5 %
9 k Appart. -33,5 %
4 k Maisons -26,1 %
État du parc RP 2023 · DPE 2026 · évol / RP 2017 (pts)
6,0 % Logements vacants +0,2 pt
37,5 % Propriétaires -1,4 pt
9,0 % Passoires F-G
Logements autorisés 2025 · évol / 2022 / 2024
10 k Ensemble -24,4 % +52,3 %
566 Individuel -28,8 % +26,3 %
553 k m² autorisés -31,4 % +45,5 %
DV3F · INSEE RP · ADEME DPE · SDES SITADEL — Seine-Saint-Denis

Le territoire face à ses pairs Départements d’Île-de-France · repère France en trait vertical

Afficher le code
# Databar des PAIRS dès l'ouverture (demande 260903) : le pendant territorial du databar national
# par type de territoire. Indicateurs RAPPORTÉS AU PARC — comparer des volumes bruts entre
# territoires de tailles différentes ne classerait que la taille.
.mk <- function(col, evol_col = NULL, ref = NULL) list(
  vals = stats::setNames(tt_db[[col]], tt_db_ser),
  ref  = ref,
  evol = if (!is.null(evol_col)) stats::setNames(tt_db[[evol_col]], tt_db_ser) else NULL)
.indb <- list(
  c(list(lbl = "Prix médian au m²", sub = "€ · DV3F 2025 · pastille = évol. 22-25", unit = "", dec = 0),
    .mk("px_25", "px_ev", ref = KPI_LOG$px_25)),
  c(list(lbl = "Rotation du marché", sub = "ventes pour 1 000 logements · 2025 · pastille = évol. 22-25", unit = "", dec = 1),
    .mk("rota_25", "rota_ev", ref = fr_rota)),
  c(list(lbl = "Logements autorisés", sub = "pour 1 000 logements · SITADEL 2025 · pastille = évol. 22-25", unit = "", dec = 1),
    .mk("constr_25", "constr_ev", ref = fr_constr)),
  c(list(lbl = "Logements vacants", sub = "% du parc · INSEE RP 2023 · pastille = évol. 17-23, en points", unit = "", dec = 1),
    .mk("vac_22", "vac_dif", ref = fr_vac)))
cat(databar_psect(.indb, series = tt_db_ser, n_col = 4, ref_lab = "fr", col_header = FALSE,
                  pal = tt_db_pal, target = (tt_maille != "region"), ref_all = TRUE,
                  lbl_width = "150px"))
Prix médian au m²
€ · DV3F 2025 · pastille = évol. 22-25
Rotation du marché
ventes pour 1 000 logements · 2025 · pastille = évol. 22-25
Logements autorisés
pour 1 000 logements · SITADEL 2025 · pastille = évol. 22-25
Logements vacants
% du parc · INSEE RP 2023 · pastille = évol. 17-23, en points
Seine-Saint-Denis
3 980fr 2 616−8,2
17,1fr 20,6−31,5
13,1fr 9,4−28,2
6,0fr 7,8+0,2
Paris
9 706−9,5
20,5−14,7
1,0−4,4
9,9+1,5
Seine-et-Marne
3 065−10,1
23,8−29,0
9,7−41,7
6,5−0,3
Yvelines
3 850−8,2
22,1−27,0
8,7−29,2
5,9−0,4
Essonne
3 151−10,1
21,9−29,4
10,8−2,8
6,6+0,2
Hauts-de-Seine
6 534−10,2
21,9−23,4
14,6−3,5
6,8+0,2
Val-de-Marne
4 769−10,3
20,4−25,3
19,1+22,7
5,9+0,3
Val-d’Oise
3 378−9,1
21,0−31,2
8,4−26,7
6,0+0,2

Où le mètre carré est-il cher, et où décroche-t-il ? · communes

Afficher le code
log_map_auto("tt_carte_px", "px_25", tt_map_df, "libelle")
.mp1 <- build_map(tt_map_geo, "px_25", tooltip_lab = "Prix médian au m²", bins = tt_bins_px,
    bin_type = "level", label_col = tt_map_lblcol, interactive = TRUE, hover_key = TRUE,
    legend_drop = FALSE, show_counts = FALSE, n_top = 2, n_bottom = 1, show_values = FALSE,
    label_style = "text", titre = "Niveau : prix médian au m² · 2025",
    subtitle = sprintf("€/m² · %s", tt_map_unit_lbl), source_txt = NULL, legend_title = "€/m²",
    legend_keywidth = 0.5, legend_keyheight = 0.17,
    contour_geo = tt_map_contour, contour_lw = 0.4, contour_color = "#9a9a9a", dd = DD) + th_map_sm
girafe_legend(.mp1, tt_bins_px, width_svg = 4.4, height_svg = tt_fh, title = "€/m²", zoom = TRUE)
€/m²clic & ctrl-clic
Afficher le code
log_map_auto("tt_carte_pxev", "px_ev", tt_map_df, "libelle")
.mp2 <- build_map(tt_map_geo, "px_ev", tooltip_lab = "Évolution du prix", bins = tt_bins_pxev,
    bin_type = "divergent_asym", label_col = tt_map_lblcol, interactive = TRUE, hover_key = TRUE,
    legend_drop = FALSE, show_counts = FALSE, n_top = 2, n_bottom = 1, show_values = FALSE,
    label_style = "text", titre = "Dynamique : évolution du prix · 2022-2025",
    subtitle = sprintf("%% sur trois ans · %s", tt_map_unit_lbl), source_txt = NULL,
    legend_title = "%", legend_keywidth = 0.5, legend_keyheight = 0.17,
    contour_geo = tt_map_contour, contour_lw = 0.4, contour_color = "#9a9a9a", dd = DD) + th_map_sm
girafe_legend(.mp2, tt_bins_pxev, width_svg = 4.4, height_svg = tt_fh, title = "%", zoom = TRUE)
%clic & ctrl-clic

Lecture : les deux cartes opposent le niveau (à gauche) et sa dynamique (à droite), sur la même maille et la même échelle de classes. Survol pour le détail, clic sur une classe de la légende pour la filtrer.

Afficher le code
# Alsace-Moselle : le livre foncier remplace la publicité foncière → AUCUN prix DVF sur 57/67/68.
if (length(tt_am)) cat(sprintf(
  "::: {.note-lecture}\n⚠️ **Couverture DVF** : les départements %s relèvent du **livre foncier**
(droit local d'Alsace-Moselle) et sont absents de DVF — aucun prix n'y est calculable. Les
territoires concernés apparaissent en gris sur les cartes, et les agrégats de prix du territoire
sont établis hors de leur périmètre.\n:::\n\n", paste(tt_am, collapse = ", ")))
Afficher le code
# ENCADRÉ DE SYNTHÈSE — chiffres CALCULÉS à chaque render, jamais recopiés (un chiffre écrit à la
# main serait faux en silence au millésime suivant). Passé en `prose_cc` : un analyste peut le
# réécrire pour un territoire donné via le sidecar, ou l'enrichir en mode `+auto`.
.p_dir <- if (is.finite(tt_px_ev2225) && tt_px_ev2225 < 0) "corrige" else "tient"
.c_dir <- if (is.finite(tt_aut_ev24) && tt_aut_ev24 > 0) "amorce un rebond" else "continue de se replier"
cat(prose_cc("terr_synth", function() sprintf("::: {.encadre-synth}
[Ce qu'il faut retenir — %s]{.encadre-synth-titre}

- **Le prix médian %s** — %s € le m² en 2025, %s depuis 2022 (France : %s), après une hausse
de %s %% pendant la reprise 2019-2022. C'est le %dᵉ prix le plus élevé des %d %s.
- **L'ajustement passe par les volumes** — les ventes %s depuis 2022 (France : %s), à %s
mutations en 2025 : le marché se bloque avant de se déprécier.
- **La construction %s** — %s logements autorisés en 2025, %s par rapport à 2022 puis %s sur la
seule dernière année : la référence 2022 dit la chute, la référence 2024 dit l'inflexion en cours.
:::

",
  tt_lbl,
  .p_dir, fmt_fr(tt_px25, 0), .evn(tt_px_ev2225), .evn(fr_px_t2225),
  fmt_fr(abs(tt_px_ev1922), 0), tt_rk_px$r, tt_rk_px$n, tt_maille_pl,
  tt_phr_tr, .evn(fr_tr_t2225, 0), fmt_k(tt_tr25),
  .c_dir, fmt_k(tt_aut25), tt_phr_aut, tt_phr_aut24)))

Ce qu’il faut retenir — Seine-Saint-Denis

  • Le prix médian corrige — 3 980 € le m² en 2025, un recul de 8,2 % depuis 2022 (France : un recul de 3,3 %), après une hausse de 14 % pendant la reprise 2019-2022. C’est le 7ᵉ prix le plus élevé des 93 départements.
  • L’ajustement passe par les volumes — les ventes reculent de 31 % depuis 2022 (France : un recul de 21 %), à 12 k mutations en 2025 : le marché se bloque avant de se déprécier.
  • La construction amorce un rebond — 10 k logements autorisés en 2025, un recul de 24 % par rapport à 2022 puis une hausse de 52 % sur la seule dernière année : la référence 2022 dit la chute, la référence 2024 dit l’inflexion en cours.

Transactions : le marché tourne-t-il encore ?

Afficher le code
cat(prose_cc("terr_transactions", function() glue::glue(
  "[Le marché du territoire tourne à {fmt_fr(tt_me_ind$rota_25[1], 1)} ventes pour 1 000 ",
  "logements, contre {fmt_fr(stats::median(tt_all$rota_25, na.rm = TRUE), 1)} pour la médiane ",
  "des {tt_maille_pl}.]{{.insight}}\n\n",
  "La rotation rapporte les mutations de l'année au parc de logements : elle dit la LIQUIDITÉ ",
  "du marché, là où un volume brut ne dirait que la taille du territoire. Les ventes ",
  "{tt_phr_tr} depuis 2022 *(cartes ci-contre)* — c'est par ce canal que l'ajustement passe ",
  "d'abord, avant les prix affichés.\n\n"
)))

Le marché du territoire tourne à 17,1 ventes pour 1 000 logements, contre 21,3 pour la médiane des départements.

La rotation rapporte les mutations de l’année au parc de logements : elle dit la LIQUIDITÉ du marché, là où un volume brut ne dirait que la taille du territoire. Les ventes reculent de 31 % depuis 2022 (cartes ci-contre) — c’est par ce canal que l’ajustement passe d’abord, avant les prix affichés. Le mécanisme est le même partout : la remontée des taux ferme d’abord l’accès au crédit, ce qui assèche le nombre de transactions ; les prix affichés ne cèdent qu’ensuite, quand les vendeurs renoncent à attendre un retournement de conjoncture. Un territoire où les ventes s’effondrent sans que les prix bougent n’est donc pas un marché épargné, mais un marché bloqué — la correction y est différée, pas évitée.

Afficher le code
# ROTATION = ventes rapportées au parc (pour 1 000 logements). Le volume brut de transactions ne
# dit que la taille du territoire ; la rotation dit la LIQUIDITÉ du marché — combien de logements
# changent de main chaque année. Indicateur créé pour ce volet (260903).
log_map_auto("tt_carte_rota", "rota_25", tt_map_df, "libelle")
.mp3 <- build_map(tt_map_geo, "rota_25", tooltip_lab = "Ventes / 1 000 logements",
    bins = tt_bins_rota, bin_type = "level", label_col = tt_map_lblcol, interactive = TRUE,
    hover_key = TRUE, legend_drop = FALSE, show_counts = FALSE, n_top = 2, n_bottom = 1,
    show_values = FALSE, label_style = "text",
    titre = "Niveau : rotation du marché · 2025",
    subtitle = sprintf("ventes pour 1 000 logements · %s", tt_map_unit_lbl), source_txt = NULL,
    legend_title = "‰", legend_keywidth = 0.5, legend_keyheight = 0.17,
    contour_geo = tt_map_contour, contour_lw = 0.4, contour_color = "#9a9a9a", dd = DD) + th_map_sm
girafe_legend(.mp3, tt_bins_rota, width_svg = 4.4, height_svg = tt_fh, title = "‰", zoom = TRUE)
‰clic & ctrl-clic
Afficher le code
log_map_auto("tt_carte_trev", "rota_ev", tt_map_df, "libelle")
.mp4 <- build_map(tt_map_geo, "rota_ev", tooltip_lab = "Évolution des ventes", bins = tt_bins_trev,
    bin_type = "divergent_asym", label_col = tt_map_lblcol, interactive = TRUE, hover_key = TRUE,
    legend_drop = FALSE, show_counts = FALSE, n_top = 2, n_bottom = 1, show_values = FALSE,
    label_style = "text", titre = "Dynamique : évolution des ventes · 2022-2025",
    subtitle = sprintf("%% sur trois ans · %s", tt_map_unit_lbl), source_txt = NULL,
    legend_title = "%", legend_keywidth = 0.5, legend_keyheight = 0.17,
    contour_geo = tt_map_contour, contour_lw = 0.4, contour_color = "#9a9a9a", dd = DD) + th_map_sm
girafe_legend(.mp4, tt_bins_trev, width_svg = 4.4, height_svg = tt_fh, title = "%", zoom = TRUE)
%clic & ctrl-clic
Afficher le code
.dtr <- .cv_tr$df
plot_serie_pal(.dtr, x_col = "annee", y_col = "indice", group_col = "serie",
  pal_named = tt_pal, lty_named = tt_lty, lw_named = tt_lw,
  smooth = TRUE, hline_ref = 100, point_size = 1.1,
  end_inside = TRUE, lab_override = .endlab(.dtr),
  label_last_fn = function(y) sprintf("%+.0f %%", y - 100),
  bar_df = data.frame(annee = .str$annee, val = .str$nbtrans_glb),
  bar_name = "Mutations/an", bar_label_fn = fmt_k,
  title    = "Transactions : le canal de l'ajustement",
  subtitle = sprintf("Mutations · indice base 100 = %d · barres : volume du territoire — DV3F",
                     tt_base_tr),
  y_lab = NULL, x_breaks = seq(tt_base_tr, 2025, 2), caption = make_source("DV3F"))

Prix : la hausse est-elle enrayée ici aussi ?

Afficher le code
cat(prose_cc("terr_prix", function() glue::glue(
  "[Depuis 2022, **{params$territoire_focus}** voit son prix médian {tt_phr_px_inf} quand la ",
  "France enregistre {.evn(fr_px_t2225)}.]{{.insight}}\n\n",
  "Le prix médian s'établit à {fmt_fr(tt_px25, 0)} € le m², après une hausse de ",
  "{fmt_fr(abs(tt_px_ev1922), 0)} % pendant la reprise 2019-2022. La courbe rapporte cette ",
  "trajectoire à celle de son échelon parent et de la France *(figure ci-contre)*.\n\n"
)))

Depuis 2022, Seine-Saint-Denis voit son prix médian reculer de 8 % quand la France enregistre un recul de 3,3 %.

Le prix médian s’établit à 3 980 € le m², après une hausse de 14 % pendant la reprise 2019-2022. La courbe rapporte cette trajectoire à celle de son échelon parent et de la France (figure ci-contre). Deux facteurs propres au département amplifient ce blocage. Le Grand Paris Express y a nourri une anticipation de plus-value que la remontée des taux a interrompue net, laissant des vendeurs sur des prix de 2021. Et la part élevée de primo-accédants, plus sensibles au coût du crédit que des acquéreurs déjà propriétaires, y rend la demande particulièrement élastique aux taux.

Afficher le code
.dpx <- .cv_px$df
plot_serie_pal(.dpx, x_col = "annee", y_col = "indice", group_col = "serie",
  pal_named = tt_pal, lty_named = tt_lty, lw_named = tt_lw,
  smooth = TRUE, hline_ref = 100, point_size = 1.1,
  end_inside = TRUE, lab_override = .endlab(.dpx),
  label_last_fn = function(y) sprintf("%+.0f %%", y - 100),
  title    = "Prix : la trajectoire face aux références",
  subtitle = sprintf("Prix médian au m² · indice base 100 = %d — DV3F", tt_base_px),
  y_lab = NULL, x_breaks = seq(tt_base_px, 2025, 2), caption = make_source("DV3F"))

Le niveau de prix et la liquidité du marché vont-ils de pair à l’intérieur du territoire ?

Afficher le code
# SCATTER INTERNE : chaque point est une unité du territoire (commune ou EPCI), pas un
# territoire national. On regarde si les marchés chers sont aussi les plus liquides — la
# réponse n'a rien d'évident : un marché cher peut être bloqué par le prix, un marché
# abordable tourner vite.
.scp <- tt_map_df[is.finite(tt_map_df$px_25) & is.finite(tt_map_df$rota_25), ]
log_scatter_auto("tt_sc_prix_rota", .scp, "rota_25", "px_25", "libelle")
plot_scatter(.scp, x = "rota_25", y = "px_25",
  label_col = "libelle", n_labels = 8, label_mode = "extremes",
  ref_lines = "auto", ref_x = stats::median(.scp$rota_25, na.rm = TRUE),
  ref_y = stats::median(.scp$px_25, na.rm = TRUE), mean_lines = FALSE, engine = "ggplot2",
  title    = sprintf("Prix et liquidité du marché · %s", tt_map_unit_lbl),
  subtitle = sprintf("Rotation (horizontal) et prix médian (vertical) · 2025 · %s · repères = médianes du territoire", tt_map_unit_lbl),
  xlab = "Ventes pour 1 000 logements →", ylab = "Prix médian (€/m²)",
  caption = make_source("DV3F · INSEE RP"))

Parc et énergie : quel état du bâti ?

Afficher le code
cat(prose_cc("terr_parc", function() glue::glue(
  "[{fmt_fr(tt_vac, 1)} % du parc est vacant et {fmt_fr(tt_dpe, 1)} % des logements sont des ",
  "passoires énergétiques, contre {fmt_fr(KPI_LOG$vac, 1)} % et {fmt_fr(KPI_LOG$dpe_fg, 1)} % ",
  "au national.]{{.insight}}\n\n",
  "Vacance et performance énergétique sont les deux fragilités du parc existant : la première ",
  "mesure ce qui est sorti du marché, la seconde ce que le calendrier d'interdiction de ",
  "location va contraindre *(cartes ci-contre)*.\n\n"
)))

6,0 % du parc est vacant et 9,0 % des logements sont des passoires énergétiques, contre 7,8 % et 9,2 % au national.

Vacance et performance énergétique sont les deux fragilités du parc existant : la première mesure ce qui est sorti du marché, la seconde ce que le calendrier d’interdiction de location va contraindre (cartes ci-contre).

Afficher le code
log_map_auto("tt_carte_vac", "vac_22", tt_map_df, "libelle")
.m <- build_map(tt_map_geo, "vac_22", tooltip_lab = "Logements vacants", bins = tt_bins_vac,
    bin_type = "level", label_col = tt_map_lblcol, interactive = TRUE, hover_key = TRUE,
    legend_drop = FALSE, show_counts = FALSE, n_top = 2, n_bottom = 1, show_values = FALSE,
    label_style = "text", titre = "Logements vacants · 2023",
    subtitle = sprintf("%% du parc · INSEE RP 2023 · %s", tt_map_unit_lbl), source_txt = NULL,
    legend_title = "%", legend_keywidth = 0.5, legend_keyheight = 0.17,
    contour_geo = tt_map_contour, contour_lw = 0.4, contour_color = "#9a9a9a", dd = DD) + th_map_sm
girafe_legend(.m, tt_bins_vac, width_svg = 4.4, height_svg = tt_fh, title = "%", zoom = TRUE)
%clic & ctrl-clic
Afficher le code
log_map_auto("tt_carte_dpe", "dpe_fg", tt_map_df, "libelle")
.m <- build_map(tt_map_geo, "dpe_fg", tooltip_lab = "Passoires énergétiques F-G", bins = tt_bins_dpe,
    bin_type = "level", label_col = tt_map_lblcol, interactive = TRUE, hover_key = TRUE,
    legend_drop = FALSE, show_counts = FALSE, n_top = 2, n_bottom = 1, show_values = FALSE,
    label_style = "text", titre = "Passoires énergétiques F-G · 2026",
    subtitle = sprintf("%% des logements · DPE ADEME · %s", tt_map_unit_lbl), source_txt = NULL,
    legend_title = "%", legend_keywidth = 0.5, legend_keyheight = 0.17,
    contour_geo = tt_map_contour, contour_lw = 0.4, contour_color = "#9a9a9a", dd = DD) + th_map_sm
girafe_legend(.m, tt_bins_dpe, width_svg = 4.4, height_svg = tt_fh, title = "%", zoom = TRUE)
%clic & ctrl-clic
Afficher le code
# Miroir territorial du scatter national « prix × performance énergétique » : les marchés
# chers ont-ils ici aussi le parc le plus sain ? La relation nationale ne se reproduit pas
# partout, et c'est justement ce qu'on veut voir.
.scd <- tt_map_df[is.finite(tt_map_df$px_25) & is.finite(tt_map_df$dpe_fg), ]
log_scatter_auto("tt_sc_prix_dpe", .scd, "dpe_fg", "px_25", "libelle")
plot_scatter(.scd, x = "dpe_fg", y = "px_25",
  label_col = "libelle", n_labels = 8, label_mode = "extremes",
  ref_lines = "auto", ref_x = stats::median(.scd$dpe_fg, na.rm = TRUE),
  ref_y = stats::median(.scd$px_25, na.rm = TRUE), mean_lines = FALSE, engine = "ggplot2",
  title    = sprintf("Prix et performance énergétique · %s", tt_map_unit_lbl),
  subtitle = sprintf("Passoires F-G (horizontal) et prix médian (vertical) · %s · repères = médianes du territoire", tt_map_unit_lbl),
  xlab = "Part de passoires F-G (%) →", ylab = "Prix médian (€/m²)",
  caption = make_source("DV3F · ADEME DPE"))

Construction : l’offre future se reconstitue-t-elle ?

Afficher le code
cat(prose_cc("terr_construction", function() glue::glue(
  "[{fmt_k(tt_aut25)} logements autorisés en 2025, {tt_phr_aut} par rapport à 2022 puis ",
  "{tt_phr_aut24} sur la seule dernière année.]{{.insight}}\n\n",
  "Ce sont des logements **autorisés** — des permis délivrés, pas des chantiers : entre les ",
  "deux il y a un délai, et une part des permis n'est jamais mise en œuvre. Rapportés au parc, ",
  "ils disent l'effort de construction du territoire indépendamment de sa taille ",
  "*(cartes ci-contre)*.\n\n"
)))

10 k logements autorisés en 2025, un recul de 24 % par rapport à 2022 puis une hausse de 52 % sur la seule dernière année.

Ce sont des logements autorisés — des permis délivrés, pas des chantiers : entre les deux il y a un délai, et une part des permis n’est jamais mise en œuvre. Rapportés au parc, ils disent l’effort de construction du territoire indépendamment de sa taille (cartes ci-contre).

Afficher le code
log_map_auto("tt_carte_constr", "constr_25", tt_map_df, "libelle")
.m <- build_map(tt_map_geo, "constr_25", tooltip_lab = "Logements autorisés", bins = tt_bins_constr,
    bin_type = "level", label_col = tt_map_lblcol, interactive = TRUE, hover_key = TRUE,
    legend_drop = FALSE, show_counts = FALSE, n_top = 2, n_bottom = 1, show_values = FALSE,
    label_style = "text", titre = "Logements autorisés · 2025",
    subtitle = sprintf("pour 1 000 logements · SITADEL · %s", tt_map_unit_lbl), source_txt = NULL,
    legend_title = "‰", legend_keywidth = 0.5, legend_keyheight = 0.17,
    contour_geo = tt_map_contour, contour_lw = 0.4, contour_color = "#9a9a9a", dd = DD) + th_map_sm
girafe_legend(.m, tt_bins_constr, width_svg = 4.4, height_svg = tt_fh, title = "‰", zoom = TRUE)
‰clic & ctrl-clic
Afficher le code
log_map_auto("tt_carte_conev", "constr_dif", tt_map_df, "libelle")
.m <- build_map(tt_map_geo, "constr_dif", tooltip_lab = "Écart du rythme d'autorisation", bins = tt_bins_conev,
    bin_type = "divergent_asym", label_col = tt_map_lblcol, interactive = TRUE, hover_key = TRUE,
    legend_drop = FALSE, show_counts = FALSE, n_top = 2, n_bottom = 1, show_values = FALSE,
    label_style = "text", titre = "Dynamique : autorisations, 2023-25 contre 2020-22",
    subtitle = sprintf("points pour 1 000 logements · moyennes annuelles · %s", tt_map_unit_lbl), source_txt = NULL,
    legend_title = "pts", legend_keywidth = 0.5, legend_keyheight = 0.17,
    contour_geo = tt_map_contour, contour_lw = 0.4, contour_color = "#9a9a9a", dd = DD) + th_map_sm
girafe_legend(.m, tt_bins_conev, width_svg = 4.4, height_svg = tt_fh, title = "pts", zoom = TRUE)
ptsclic & ctrl-clic
Afficher le code
# Série SITADEL du territoire : le volume annuel d'autorisations sur toute la profondeur
# disponible. On montre le VOLUME BRUT plutôt qu'un indice — l'échelle du national n'a rien de
# comparable, et c'est le nombre de logements qui parle à un élu.
log_describe_ranking("tt_serie_constr", .sau, "annee", "logs_logaut")
plot_serie_pal(
  data.frame(serie = tt_lbl, annee = .sau$annee, indice = .sau$logs_logaut),
  x_col = "annee", y_col = "indice", group_col = "serie",
  pal_named = stats::setNames("#8a6d3b", tt_lbl), smooth = TRUE, point_size = 1.4,
  end_inside = TRUE,
  lab_override = stats::setNames(sprintf("%s · %s", tt_lbl, fmt_k(tt_aut25)), tt_lbl),
  label_last_fn = function(y) "",
  title    = "Logements autorisés : le volume annuel",
  subtitle = "Nombre de logements autorisés · SITADEL 3 (permis délivrés, pas commencés)",
  y_lab = NULL, x_breaks = seq(min(.sau$annee), 2025, 2), caption = make_source("SDES SITADEL"))

Position : Seine-Saint-Denis parmi ses pairs et dans la France

Afficher le code
cat(prose_cc("terr_position", function() glue::glue(
  "[Un facteur {fmt_fr(tt_map_ratio, 1)} sépare {tt_map_hi$libelle} ({fmt_fr(tt_map_hi$px_25, 0)} €/m²) ",
  "de {tt_map_lo$libelle} ({fmt_fr(tt_map_lo$px_25, 0)} €/m²) à l'intérieur du territoire.]{{.insight}}

",
  "Le tableau situe chaque indicateur dans **deux référentiels à la fois** : ",
  "{.pool_txt} d'abord, la France entière ensuite. Sous chaque niveau, une ligne donne la ",
  "**dynamique** — parce qu'un territoire cher qui décroche et un territoire abordable qui ",
  "rattrape ne se distinguent pas sur le seul niveau. Les flux sont rapportés au parc : un rang ",
  "sur un volume brut ne classerait que la taille du territoire.

"
)))

Un facteur 3,1 sépare Les Lilas (6 603 €/m²) de Clichy-sous-Bois (2 118 €/m²) à l’intérieur du territoire.

Le tableau situe chaque indicateur dans deux référentiels à la fois : les 8 départements d’Île-de-France d’abord, la France entière ensuite. Sous chaque niveau, une ligne donne la dynamique — parce qu’un territoire cher qui décroche et un territoire abordable qui rattrape ne se distinguent pas sur le seul niveau. Les flux sont rapportés au parc : un rang sur un volume brut ne classerait que la taille du territoire.

Afficher le code
log_table_auto("tt_rangs", tt_tbl)
gt_jumelles_rank(
  tt_tbl, focus_col = tt_lbl,
  pool_jum = if (tt_dual) tt_pool_jum else tt_pool_cat,
  pool_cat = tt_pool_cat, rank_ref_col = "CATREF",
  dual_pool = tt_dual, rang_pos = "after_value",
  # Pas d'effectif dans ces intitulés : le helper écrit déjà « rang / N » ligne par ligne, avec le N
  # RÉEL de chaque indicateur (tous les territoires n'ont pas toutes les valeurs). Un total figé en
  # en-tête contredisait ces N (« national · 96 » au-dessus de rangs sur 94).
  dual_labels = c(jum = if (tt_maille == "departement") "dans sa région" else "parmi ses pairs",
                  cat = "dans la France"),
  focus_label = tt_lbl, group_col = "bloc",
  ref_labels    = list(Jumelles = if (tt_dual) "Médiane régionale" else "Médiane", CATREF = "Médiane France"),
  ref_subtitles = list(Jumelles = "pairs", CATREF = tt_maille_pl),
  gauge_mode = "marker", metric = "rank", cible_w = 190,
  cible_sub = "valeur", taux_lbl = "valeur",
  title = sprintf("%s : position dans %s", tt_lbl,
                  if (tt_dual) "sa région et dans la France" else "la France"),
  source_note = sprintf("DV3F 2025 · INSEE RP 2023 · SDES SITADEL 2025 — rang 1 = valeur la plus élevée · %s",
                        tt_maille_pl))
Seine-Saint-Denis : position dans sa région et dans la France
Indicateurs
 
Seine-Saint-Denis
Panels de comparaison
valeur rangs ⓘ
voisins / national
dans sa région
médiane
dans la France
médiane
Prix
Prix médian au m² · 2025 € 3 980
5/9
8/94
3 915 1 984
évolution 2022-2025 % −8,2
3
85
−9,8 +0,69
Marché
Rotation · ventes / 1 000 logements ‰ 17,1
9
85
21,4 21,3
évolution 2022-2025 % −31,5
9
93
−28,0 −19,1
Construction
Autorisés · pour 1 000 logements ‰ 13,1
4
12
10,3 7,7
évolution 2022-2025 % −28,2
7
74
−15,6 −20,8
Parc
Logements vacants % 6,0
7
87
6,3 8,4
évolution 2017-2023 (pts) pts +0,21
6
14
+0,21 −0,41
DV3F 2025 · INSEE RP 2023 · SDES SITADEL 2025 — rang 1 = valeur la plus élevée · départements

Retournement : quelle ampleur de bascule entre les deux fenêtres ?

Afficher le code
cat(prose_cc("terr_retourn", function() glue::glue(
  "[Entre la reprise et l'ajustement, **{params$territoire_focus}** perd ",
  "{fmt_fr(abs(tt_bascule_me), 1)} points de rythme annuel sur les prix.]{{.insight}}

",
  "Les deux fenêtres de trois ans — 2019-2022 puis 2022-2025 — se lisent l'une contre l'autre : ",
  "la flèche part du rythme de la reprise et arrive à celui du retournement. Plus elle est ",
  "longue, plus la bascule est brutale *(figure ci-contre)*. Le référentiel est celui des pairs ",
  "{.pairs_txt} — comparer un territoire à l'ensemble national à cette échelle noierait le signal.

"
)))

Entre la reprise et l’ajustement, Seine-Saint-Denis perd 7,5 points de rythme annuel sur les prix.

Les deux fenêtres de trois ans — 2019-2022 puis 2022-2025 — se lisent l’une contre l’autre : la flèche part du rythme de la reprise et arrive à celui du retournement. Plus elle est longue, plus la bascule est brutale (figure ci-contre). Le référentiel est celui des pairs d’Île-de-France — comparer un territoire à l’ensemble national à cette échelle noierait le signal. Comparer deux fenêtres de trois ans neutralise l’effet de durée : ce qui reste est bien un changement de régime, pas un artefact de calendrier. Une bascule forte sur un marché cher signale une correction ; la même bascule sur un marché abordable traduit plutôt la fin d’un rattrapage.

Afficher le code
log_describe_ranking("tt_arrow_bascule", tt_arr, "libelle", "bascule")
plot_arrow(tt_arr, "p1", "p2",
  title    = sprintf("La bascule des prix entre reprise et retournement · %s", tt_lbl),
  subtitle = "Rythme annuel du prix médian : départ 2019-2022, arrivée 2022-2025 — DV3F",
  xlab = "Rythme annuel du prix médian (%/an)",
  # zéro = stabilité des prix, pas la France : le label par défaut du helper serait faux ici.
  zero_lab = "stabilité",
  # Note COURTE : le caption est aligné à droite et non replié — une phrase longue déborde du cadre
  # par la gauche. Le détail de lecture vit dans la `.note-lecture` juste sous la figure.
  note = "Rond = reprise 2019-2022 · pointe = retournement 2022-2025.",
  caption = make_source("DV3F"))

Lecture : chaque flèche va du rythme annuel de la reprise (2019-2022) à celui du retournement (2022-2025). Une flèche vers la gauche marque un freinage, vers la droite une accélération. Les deux fenêtres font trois ans chacune, donc leurs rythmes sont directement comparables.