Logement — synthèse nationale et Seine-Saint-Denis

L’essentiel en deux pages : le cadrage national, puis le même marché vu depuis un département

Synthèse
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.

Seine-Saint-Denis : le marché local à l’épreuve du retournement

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.

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.

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−24,4
6,0fr 7,8+0,2
Paris
9 706−9,5
20,5−14,7
1,0−38,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−42,2
5,9−0,4
Essonne
3 151−10,1
21,9−29,4
10,8−13,2
6,6+0,2
Hauts-de-Seine
6 534−10,2
21,9−23,4
14,6+14,6
6,8+0,2
Val-de-Marne
4 769−10,3
20,4−25,3
19,1+2,8
5,9+0,3
Val-d’Oise
3 378−9,1
21,0−31,2
8,4−31,8
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, simplify_min_n = 250, "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, values = tt_map_df$px_25, 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, simplify_min_n = 250, "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, values = tt_map_df$px_ev, title = "%", zoom = TRUE)
%clic & ctrl-clic
Afficher le code
if (tt_has_rota) cat(sprintf(
  "[Le marché tourne-t-il, et à quel rythme ? · %s]{.psect-block-title}\n\n", tt_map_unit_lbl))

Le marché tourne-t-il, et à quel rythme ? · communes

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, simplify_min_n = 250, "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, values = tt_map_df$rota_25, 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, simplify_min_n = 250, "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, values = tt_map_df$rota_ev, title = "%", zoom = TRUE)
%clic & ctrl-clic

Lecture : chaque paire oppose 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. Rotation = mutations de l’année rapportées au parc de logements du recensement 2023 : le parc évolue trop lentement pour que ce décalage change un classement.

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 = ", ")))