---
title: "Le marché du logement : tendances nationales et fractures territoriales"
subtitle: "Volet 3 — Prix, construction, vacance et accessibilité (2014-2024)"
author: "Vincent R."
date: last-modified
date-format: "YYYY"
categories: ["Rapport d'analyse"]
number-sections: false
---

<!-- Rapport volet Logement (rpt-plog-rapport-logement-260215)
     Type : RAPPORT (code hidden, insight-first, rédigé)
     Données : DVF 2014-2024 · LOVAC 2020-2024 · SITADEL 2011-2024 · ANIL 2022-2025 · INSEE RP
     Helpers : _setup-common.R → jcn-graph.R, jcn-table.R, jcn-map.R
     Config  : hérite de _quarto.yml (theme-html.scss, toc-right, echo:false)
     Structure : 3 parties — National (séries) · DEP (cartes/scatter) · Typologies AAV
-->

<!-- Style inline sécurité — override SCSS si besoin -->
<style>
.insight { color: #1a5276; font-weight: 600; }
.chapeau { font-size: 0.95em; font-weight: 400; color: #555; line-height: 1.6; max-width: 900px; }
.note-lecture { font-size: 0.7em; font-style: italic; color: #999; }
</style>

<!-- ============================================================
          Bloc 1 : Plan statique propre (bullet list, liens cliquables)
     Bloc 2 : R chunk auto-parse le .qmd, affiche structure + commentaires
     ============================================================ -->

::: {.callout-warning collapse="true"}
## Structure cible v2
Évolution prévue en 4 parties : **P0** Cadrage européen (Eurostat HPI) · **P1** Vente · **P2** Location (ANIL+LOVAC recombinés) · **P3** Territoires DEP+EPCI · **P4** Typologies AAV. Voir `plogement/README.md` pour le détail.
:::

::: {.callout-note collapse="true" icon=false}

```{r}
#| label: setup-main
#| include: false

# &s &SETUP_aaMAIN - Setup rapport logement v2

source("_setup-common.R")
library(sf)
library(tmap)
library(plotly)
library(ineq)
library(arrow)
source("C:/Users/vince/hh/pq/PDS/mutils/jrr/jcn-map.R")

tmap_mode("plot")

# &s &GEO_LOAD - Shapes DEP

shp_dep <- st_read(
  file.path(PROJECT_ROOT, "data/external/geo-shape/nodom_departement_2025.geojson"),
  quiet = TRUE
) %>% rename(code_geo = code_insee, lib_dep = nom_officiel)
# &e

# &s &DATA_LOAD - Chargement DEP + France + séries

dep <- read.csv(file.path(PROJECT_ROOT, "data/output/dbcln-dep-vf.csv"),
                stringsAsFactors = FALSE, fileEncoding = "UTF-8-BOM") %>%
  filter(!code %in% c("FR", "00FR", "ZZZZZ"))
N_DEP <- nrow(dep)

# France référence
fr_row <- read.csv(file.path(PROJECT_ROOT, "data/output/dbcln-dep-vf.csv"),
                   stringsAsFactors = FALSE, fileEncoding = "UTF-8-BOM") %>%
  filter(code %in% c("FR", "00FR"))
FR_LOG <- if (nrow(fr_row) > 0) as.list(fr_row[1, ]) else FR

# Enrichir libellés DEP depuis geojson
lib_dep <- shp_dep %>% st_drop_geometry() %>% select(code_geo, lib_dep)
dep <- dep %>%
  left_join(lib_dep, by = c("code" = "code_geo")) %>%
  mutate(libelle = coalesce(lib_dep, libelle, code)) %>%
  select(-lib_dep)

# Séries longues DVF France (parquet 2010-2024)
dvf_fr <- read_parquet(file.path(PROJECT_ROOT,
  "data/processed/dvf/dvf_prix_volumes_france_2010_2024.parquet"))

# SITADEL annuel agrégé France
sit_raw <- read.csv(file.path(PROJECT_ROOT,
  "data/processed/sitadel/sitadel_residentiel_annuel_communes.csv"),
  stringsAsFactors = FALSE, fileEncoding = "UTF-8-BOM")
sit_fr <- sit_raw %>%
  group_by(ANNEE) %>%
  summarise(log_com = sum(LOG_COM, na.rm = TRUE),
            log_aut = sum(LOG_AUT, na.rm = TRUE),
            sdp_com = sum(SDP_COM, na.rm = TRUE),
            .groups = "drop")

# TDC Master pour typologies AAV (Partie 3)
tdc <- read.csv(file.path(PROJECT_ROOT,
  "data/input-ref/tdc_geo_MASTER_communeARM_2025.csv"),
  sep = ";", stringsAsFactors = FALSE, fileEncoding = "UTF-8-BOM")

# Communes (pour Partie 3)
comm <- read.csv(file.path(PROJECT_ROOT, "data/output/dbcln-commARM-vf.csv"),
                 stringsAsFactors = FALSE, fileEncoding = "UTF-8-BOM") %>%
  filter(!code %in% c("FR", "00FR", "ZZZZZ"))
# &e

# &s &GEO_JOIN - Jointures data × shapes

map_dep <- shp_dep %>%
  left_join(dep, by = c("code_geo" = "code"), suffix = c("", ".data"))
# &e

# &s &EPCI_LOAD - EPCI data + shapes pour tabset interactif

shp_epci <- st_read(
  file.path(PROJECT_ROOT, "data/external/geo-shape/nodom_epci_2025.geojson"),
  quiet = TRUE
) %>% st_make_valid()

epci <- read.csv(file.path(PROJECT_ROOT, "data/output/dbcln-epci-vf.csv"),
                 stringsAsFactors = FALSE, fileEncoding = "UTF-8-BOM") %>%
  filter(!code %in% c("FR", "00FR", "ZZZZZ"))
N_EPCI <- nrow(epci)

map_epci <- shp_epci %>%
  left_join(epci, by = c("EPCI" = "code"), suffix = c("", ".data"))
# &e

# &s &KPI_COMPUTE - Métriques nationales et dérivées

# Prix DVF France
fr_px <- as.numeric(FR_LOG$logd_px2_global_24)
fr_px_mai <- as.numeric(FR_LOG$logd_px2q2_mai_24)
fr_px_appt <- as.numeric(FR_LOG$logd_px2q2_appt_24)
fr_pct_appt <- as.numeric(FR_LOG$logd_appt_trans_pct_24)

# Volumes transactions France
fr_trans <- as.numeric(FR_LOG$logd_trans_total_24)

# Évolutions
fr_evol_px <- as.numeric(FR_LOG$logd_px2_global_vevol_2224)
n_dep_baisse_px <- sum(dep$logd_px2_global_vevol_2224 < 0, na.rm = TRUE)
n_dep_baisse_trans <- sum(dep$logd_trans_vevol_2224 < 0, na.rm = TRUE)
med_evol_px <- median(dep$logd_px2_global_vevol_2224, na.rm = TRUE)
med_evol_trans <- median(dep$logd_trans_vevol_2224, na.rm = TRUE)

# Construction SITADEL
total_logcom <- sum(dep$logs_logcom_vol_2224, na.rm = TRUE)
total_sdp <- sum(dep$logs_sdpcom_vol_2224, na.rm = TRUE)
total_logaut <- sum(dep$logs_logaut_vol_2224, na.rm = TRUE)
med_txcom <- median(dep$logs_logcom_tx1000_2224, na.rm = TRUE)

# Variables dérivées par DEP
dep <- dep %>% mutate(
  surf_moy_logcom = ifelse(logs_logcom_vol_2224 > 0,
                           logs_sdpcom_vol_2224 / logs_logcom_vol_2224, NA),
  ratio_pipeline = ifelse(logs_logaut_vol_2224 > 0,
                          logs_logcom_vol_2224 / logs_logaut_vol_2224 * 100, NA)
)

# Surface moyenne nationale
surf_moy_nat <- total_sdp / total_logcom
ratio_pipeline_nat <- total_logcom / total_logaut * 100

# Vacance LOVAC
med_vac <- median(dep$logv_vac2ans_pct_24, na.rm = TRUE)
n_dep_vac_hausse <- sum(dep$logv_vac2ans_vdifp_2024 > 0, na.rm = TRUE)
cor_vac_sma <- cor(dep$logv_vac2ans_pct_24, dep$dm_sma_vtcam_1622, use = "complete.obs")

# Extrêmes
ratio_px <- round(max(dep$logd_px2_global_24, na.rm = TRUE) /
                  min(dep$logd_px2_global_24, na.rm = TRUE), 1)

# Concentration construction (Gini)
gini_constr <- ineq(dep$logs_logcom_vol_2224[!is.na(dep$logs_logcom_vol_2224) &
                    dep$logs_logcom_vol_2224 > 0], type = "Gini")

# Loyers ANIL
fr_loyer_app <- as.numeric(FR_LOG$logl_app_m2_25)
fr_loyer_mai <- as.numeric(FR_LOG$logl_mai_m2_25)
fr_loyer_app_evol <- as.numeric(FR_LOG$logl_app_evol_2225)
med_loyer_app <- median(dep$logl_app_m2_25, na.rm = TRUE)

# Indice 100 base 2014 (DVF séries)
base_yr <- 2014
dvf_base <- dvf_fr %>% filter(annee == base_yr)
px_base_mai <- dvf_base$pxm2_mai
px_base_apt <- dvf_base$pxm2_apt
trans_base_mai <- dvf_base$nbtrans_mai
trans_base_apt <- dvf_base$nbtrans_apt

dvf_idx <- dvf_fr %>%
  filter(annee >= base_yr) %>%
  mutate(
    idx_px_mai = pxm2_mai / px_base_mai * 100,
    idx_px_apt = pxm2_apt / px_base_apt * 100,
    idx_trans_mai = nbtrans_mai / trans_base_mai * 100,
    idx_trans_apt = nbtrans_apt / trans_base_apt * 100
  )

# SITADEL indice 100 base 2014
sit_base <- sit_fr %>% filter(ANNEE == base_yr)
sit_idx <- sit_fr %>%
  filter(ANNEE >= base_yr) %>%
  mutate(
    idx_logcom = log_com / sit_base$log_com * 100,
    idx_logaut = log_aut / sit_base$log_aut * 100
  )

# Pic et creux
pic_trans <- dvf_idx %>% slice_max(idx_trans_mai, n = 1)
creux_trans <- dvf_idx %>% filter(annee >= 2022) %>% slice_min(idx_trans_mai, n = 1)
chute_pct_trans <- round(100 - creux_trans$idx_trans_mai, 0)
pic_px_mai <- dvf_idx %>% slice_max(idx_px_mai, n = 1)

# Claude log
claude_log_init("rpt-plog-logement-v3", output_dir = file.path(PROJECT_ROOT, "reports/outputs"))
log_step("SETUP", n_dep = N_DEP, fr_px = fr_px, total_logcom = total_logcom)
# &e

# Breaks standardisés
breaks_px <- c(0, 1000, 1500, 2000, 2500, 3000, 4000, 6000, 12000)
breaks_vac <- c(0, 1, 2, 3, 4, 5, 7, 10, 20)

# &e
```

:::

# Plan du rapport {.unnumbered}

Ce rapport analyse le marché du logement comme **révélateur matériel des recompositions territoriales**. L'analyse s'organise en trois parties : le marché national et son retournement, les fractures départementales, et les dynamiques par type de territoire.

| Partie | Contenu | Échelle |
|--------|---------|---------|
| **1** | Le marché national : tendances et retournement | France (séries 2014-2024) |
| **2** | Les territoires face au marché : qui gagne, qui perd ? | 96 départements |
| **3** | Le logement selon le type de territoire | Communes agrégées par AAV |

::: {.callout-note collapse="false"}
## Hypothèses testées

**H4** — La pression immobilière est un proxy de l'attractivité résidentielle.

**H6** — Le cycle foncier (boom–correction) prédit les dynamiques territoriales.

**Sources** : DVF 2014-2024 · LOVAC 2020-2024 · SITADEL 2011-2024 · ANIL 2022-2025 · INSEE RP 2016-2022
:::

---

# Partie 1 — Le marché national : tendances et retournement

<!-- cld: P1 = France séries longues DVF+SITADEL+ANIL. TODO: série SITADEL barres annuelles, encadré Friggit -->

[**De 2014 à 2021, les prix ont bondi de `r fmt_fr(pic_px_mai$idx_px_mai - 100, 0)` % (maisons) tandis que les transactions culminaient en 2021 (`r fmt_fr(pic_trans$idx_trans_mai, 0)` base 100). Depuis 2022, les volumes s'effondrent (−`r chute_pct_trans` pts) mais les prix résistent. Le marché ajuste d'abord par les quantités.**]{.insight}

::: {.chapeau}
Le marché français du logement a bouclé un cycle complet en dix ans. Les prix au m² ont culminé en 2022 à un indice de 126,2 (base 100 en 2014), portés par des taux historiquement bas et un effet post-COVID. Le retournement se lit d'abord dans les volumes : 93 départements sur 96 enregistrent un recul des transactions entre 2022 et 2024. La construction neuve plonge à 79,6 % de son niveau 2014. Les loyers, eux, poursuivent une progression silencieuse (+11,6 % en trois ans pour les appartements), creusant l'écart avec des prix à l'achat en voie de stabilisation.
:::

## Le cycle immobilier en indice 100

```{r}
#| label: plt-indice100-prix
#| fig-height: 6

# Prix m² maisons + appts indice 100
dvf_long_px <- dvf_idx %>%
  select(annee, Maisons = idx_px_mai, Appartements = idx_px_apt) %>%
  tidyr::pivot_longer(-annee, names_to = "type", values_to = "indice")

ggplot(dvf_long_px, aes(x = annee, y = indice, color = type)) +
  geom_hline(yintercept = 100, linetype = "dashed", color = col_gray) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 2) +
  annotate("rect", xmin = 2019.5, xmax = 2020.5, ymin = -Inf, ymax = Inf,
           alpha = 0.08, fill = col_gray) +
  annotate("text", x = 2020, y = 97, label = "COVID", size = 3, color = col_gray) +
  annotate("rect", xmin = 2021.5, xmax = 2024.5, ymin = -Inf, ymax = Inf,
           alpha = 0.06, fill = col_red) +
  annotate("text", x = 2023, y = 97, label = "Hausse taux", size = 3, color = col_red) +
  scale_color_manual(values = c("Maisons" = col_cyan, "Appartements" = col_orange)) +
  scale_x_continuous(breaks = 2014:2024) +
  labs(
    title = "Hausse 2014-2022 puis amorce de correction",
    subtitle = "Indice 100 = 2014 | Prix médian m² DVF France",
    x = NULL, y = "Indice (base 100 = 2014)",
    color = NULL,
    caption = make_source("DVF 2014-2024")
  )
```

::: {.note-lecture}
Les maisons ont davantage accéléré que les appartements depuis 2020 (effet « fuite de la densité » post-COVID). La correction 2022-2024 reste modérée sur les prix (< 5 %) : le marché français ajuste d'abord par les volumes, pas par les prix.
:::

## Transactions et construction : le double effondrement

Le double effondrement des volumes est le fait marquant de la période 2022-2024. Les transactions reculent de 19 points d'indice par rapport au pic de 2021, avec un effondrement particulièrement brutal en Île-de-France : la Seine-Saint-Denis (−17,4 %), les Yvelines (−17,0 %) et le Val-d'Oise (−16,7 %) subissent les plus fortes corrections. La construction neuve, mesurée par les logements commencés, tombe à 79,6 % de son niveau 2014. Seuls les territoires ruraux à faible activité — Ardennes (−6,3 %), Indre (−6,7 %) — échappent partiellement à la chute, faute d'avoir connu le boom précédent.

```{r}
#| label: plt-volumes-construction
#| fig-height: 6

# Transactions DVF + construction SITADEL — indices 100
trans_idx <- dvf_idx %>%
  select(annee, `Transactions maisons` = idx_trans_mai, `Transactions appts` = idx_trans_apt)
sit_plot <- sit_idx %>%
  select(annee = ANNEE, `Logements commencés` = idx_logcom)

vol_merged <- trans_idx %>%
  left_join(sit_plot, by = "annee") %>%
  tidyr::pivot_longer(-annee, names_to = "série", values_to = "indice")

ggplot(vol_merged, aes(x = annee, y = indice, color = série)) +
  geom_hline(yintercept = 100, linetype = "dashed", color = col_gray) +
  geom_line(linewidth = 1) +
  geom_point(size = 1.8) +
  scale_color_manual(values = c(
    "Transactions maisons" = col_cyan,
    "Transactions appts" = col_orange,
    "Logements commencés" = col_green
  )) +
  scale_x_continuous(breaks = 2014:2024) +
  labs(
    title = "Double effondrement : transactions et construction depuis 2022",
    subtitle = "Indice 100 = 2014 | France métropolitaine",
    x = NULL, y = "Indice (base 100 = 2014)",
    color = NULL,
    caption = make_source("DVF", "SITADEL 2014-2024")
  )
```

::: {.note-lecture}
Les transactions retrouvent leur niveau de 2014 après avoir culminé en 2019-2021. La construction neuve chute encore plus vite : en 2024, les logements commencés sont à `r fmt_fr(sit_idx %>% filter(ANNEE == 2024) %>% pull(idx_logcom), 0)` % de leur niveau 2014.
:::

## Loyers et prix : deux marchés qui divergent

Les marchés de la vente et de la location divergent nettement depuis 2022. Les prix à l'achat amorcent un tassement tandis que les loyers continuent d'accélérer, portés par le report de la demande locative. L'écart est maximal dans les métropoles : à Paris, le loyer atteint 31,5 EUR/m² quand la Creuse reste à 8,3 EUR/m² — un facteur 3,8 entre extrêmes, bien moindre que le facteur 12,6 observé sur les prix à l'achat. Le ratio prix/loyer annuel de 15,9 années confirme le décrochage structurel entre les deux marchés.

En 2025, le loyer médian France atteint `r fmt_fr(fr_loyer_app, 1)` EUR/m² pour les appartements (+`r fmt_fr(fr_loyer_app_evol, 1)` % en 3 ans), tandis que les prix à l'achat se stabilisent à `r fmt_fr(fr_px, 0)` EUR/m². Le ratio prix/loyer — `r fmt_fr(as.numeric(FR_LOG$logd_px2_global_24) / (fr_loyer_app * 12), 0)` années — signale un marché de l'achat structurellement cher.

---

# Partie 2 — Les territoires face au marché

<!-- cld: P2 = 96 DEP. TODO: carte ratio prix/loyer, passer scatter en plotly interactif -->

[**Facteur `r ratio_px` entre les prix extrêmes départementaux. `r n_dep_baisse_px` DEP en baisse de prix 2022-2024, `r n_dep_baisse_trans` en baisse de transactions. La géographie oppose un arc atlantique-méditerranéen tendu à une diagonale continentale en déprise.**]{.insight}

::: {.chapeau}
La carte des prix dessine une France coupée en deux. Paris culmine à 10 199 EUR/m², la Creuse descend à 811 EUR/m² — un facteur 12,6. La construction se concentre sur l'arc atlantique : la Vendée (28,9 ‰), les Landes (28,0 ‰) et la Charente-Maritime (24,6 ‰) construisent six fois plus que Paris (2,2 ‰). La vacance longue durée, en miroir, marque les territoires en déprise : la Nièvre (7,0 %), l'Allier (5,7 %) et la Meuse (5,3 %) concentrent un parc inoccupé structurel.
:::

## Vue d'ensemble : prix, construction, vacance

Le triptyque prix–construction–vacance se lit en négatif : les départements les plus chers (médiane 1 982 EUR/m², CV = 0,57) sont aussi les plus actifs en construction et les moins touchés par la vacance. La Vendée cumule le taux de construction le plus élevé (28,9 ‰) et la vacance la plus basse (1,0 %). À l'opposé, la Nièvre affiche la vacance maximale (7,0 %) et une construction quasi nulle (3,8 ‰). La corrélation construction × croissance démographique atteint r = 0,78 (R² = 0,61) : on construit là où la population augmente, pas là où le parc vieillit.

::: {.panel-tabset}

### Départements (96)

:::: {.column-page}
::: {layout-ncol=3}

```{r}
#| label: plt-leaflet-prix-dep
#| fig-height: 6

make_carte_france(
  geo = map_dep, var = "logd_px2_global_24",
  titre = "Prix m² (EUR)", titre_carte = "Prix m² — DVF 2024",
  source = make_source("DVF"),
  palette = pal_urbn_seq_blue,
  bins = c(0, 1000, 1500, 2000, 2500, 3000, 4000, 6000, 12000),
  label_col = "lib_dep", suffix = " EUR",
  popup_vars = c("Prix m²" = "logd_px2_global_24",
                 "Évol 22-24 (%)" = "logd_px2_global_vevol_2224"),
  popup_title_col = "lib_dep", tiles = "none"
)
```

```{r}
#| label: plt-leaflet-construction-dep
#| fig-height: 6

make_carte_france(
  geo = map_dep, var = "logs_logcom_tx1000_2224",
  titre = "Construction / 1 000 hab", titre_carte = "Construction — SITADEL 22-24",
  source = make_source("SITADEL 22-24"),
  palette = pal_urbn_seq_green,
  bins = c(0, 2, 4, 6, 8, 10, 15, 25),
  label_col = "lib_dep", suffix = " ‰",
  popup_vars = c("Taux constr." = "logs_logcom_tx1000_2224",
                 "Logements commencés" = "logs_logcom_vol_2224"),
  popup_title_col = "lib_dep", tiles = "none"
)
```

```{r}
#| label: plt-leaflet-vacance-dep
#| fig-height: 6

make_carte_france(
  geo = map_dep, var = "logv_vac2ans_pct_24",
  titre = "Vacance 2+ ans (%)", titre_carte = "Vacance 2+ ans — LOVAC 2024",
  source = make_source("LOVAC"),
  palette = rev(pal_urbn_seq_green),
  bins = c(0, 1, 2, 3, 4, 5, 7, 10, 20),
  label_col = "lib_dep", suffix = " %",
  popup_vars = c("Vacance 2+ans (%)" = "logv_vac2ans_pct_24",
                 "Évol 20-24 (pts)" = "logv_vac2ans_vdifp_2024"),
  popup_title_col = "lib_dep", tiles = "none"
)
```

:::
::::

### EPCI (1254)

:::: {.column-page}
::: {layout-ncol=3}

```{r}
#| label: plt-leaflet-prix-epci
#| fig-height: 6

make_carte_france(
  geo = map_epci, var = "logd_px2_global_24",
  titre = "Prix m² (EUR)", titre_carte = "Prix m² — DVF 2024",
  source = make_source("DVF"),
  palette = pal_urbn_seq_blue,
  bins = c(0, 1000, 1500, 2000, 2500, 3000, 4000, 6000, 12000),
  label_col = "EPCIlib", suffix = " EUR",
  popup_vars = c("Prix m²" = "logd_px2_global_24",
                 "Évol 22-24 (%)" = "logd_px2_global_vevol_2224"),
  popup_title_col = "EPCIlib", tiles = "none"
)
```

```{r}
#| label: plt-leaflet-construction-epci
#| fig-height: 6

make_carte_france(
  geo = map_epci, var = "logs_logcom_tx1000_2224",
  titre = "Construction / 1 000 hab", titre_carte = "Construction — SITADEL 22-24",
  source = make_source("SITADEL 22-24"),
  palette = pal_urbn_seq_green,
  bins = c(0, 2, 4, 6, 8, 10, 15, 25),
  label_col = "EPCIlib", suffix = " ‰",
  popup_vars = c("Taux constr." = "logs_logcom_tx1000_2224",
                 "Logements commencés" = "logs_logcom_vol_2224"),
  popup_title_col = "EPCIlib", tiles = "none"
)
```

```{r}
#| label: plt-leaflet-vacance-epci
#| fig-height: 6

make_carte_france(
  geo = map_epci, var = "logv_vac2ans_pct_24",
  titre = "Vacance 2+ ans (%)", titre_carte = "Vacance 2+ ans — LOVAC 2024",
  source = make_source("LOVAC"),
  palette = rev(pal_urbn_seq_green),
  bins = c(0, 1, 2, 3, 4, 5, 7, 10, 20),
  label_col = "EPCIlib", suffix = " %",
  popup_vars = c("Vacance 2+ans (%)" = "logv_vac2ans_pct_24",
                 "Évol 20-24 (pts)" = "logv_vac2ans_vdifp_2024"),
  popup_title_col = "EPCIlib", tiles = "none"
)
```

:::
::::

:::

::: {.note-lecture}
Les 3 cartes se lisent ensemble : les territoires chers (bleu foncé) sont aussi ceux qui construisent le plus (vert foncé) et où la vacance est la plus faible. L'inverse vaut pour la diagonale continentale (Centre, Massif central, Nord-Est). Le tabset DEP/EPCI permet de passer d'une lecture macro (96 DEP) à une lecture fine (1 245 EPCI).
:::

### Carte interactive : écart à la moyenne France (POC OJS)

```{r}
#| label: ojs-data-prep
#| include: false

# &s &OJS_PREP - Prépare données DEP pour bloc OJS (écart à la moyenne)

dep_ojs <- dep %>%
  transmute(
    code, libelle,
    px = logd_px2_global_24,
    px_ecart = logd_px2_global_24 - fr_px,
    constr = logs_logcom_tx1000_2224,
    constr_ecart = logs_logcom_tx1000_2224 - med_txcom,
    vac = logv_vac2ans_pct_24,
    vac_ecart = logv_vac2ans_pct_24 - med_vac
  )

# GeoJSON string pour OJS
dep_geojson_str <- geojsonsf::sf_geojson(shp_dep)

ojs_define(dep_ojs_data = dep_ojs, dep_geo_str = dep_geojson_str)
# &e
```

```{ojs}
//| label: ojs-carte-ecart
//| echo: false

viewof indic_ojs = Inputs.radio(
  ["Prix m²", "Construction", "Vacance"],
  {value: "Prix m²", label: "Indicateur (écart France)"}
)
```

```{ojs}
//| label: ojs-carte-render
//| echo: false

geo = JSON.parse(dep_geo_str)
data_ojs = transpose(dep_ojs_data)
dataMap = new Map(data_ojs.map(d => [d.code, d]))

varKey = indic_ojs === "Prix m²" ? "px_ecart" :
         indic_ojs === "Construction" ? "constr_ecart" : "vac_ecart"

valKey = indic_ojs === "Prix m²" ? "px" :
         indic_ojs === "Construction" ? "constr" : "vac"

unitLabel = indic_ojs === "Prix m²" ? " EUR/m²" :
            indic_ojs === "Construction" ? " ‰ hab" : " %"

Plot.plot({
  projection: {type: "mercator", domain: geo},
  color: {
    type: "diverging", scheme: "RdBu", symmetric: true, legend: true,
    label: indic_ojs + " (écart moy. France)"
  },
  marks: [
    Plot.geo(geo, {
      fill: d => {
        const r = dataMap.get(d.properties.code_geo);
        return r ? r[varKey] : undefined;
      },
      stroke: "#ccc", strokeWidth: 0.3,
      tip: true,
      title: d => {
        const r = dataMap.get(d.properties.code_geo);
        if (!r) return "";
        const ecart = r[varKey];
        const signe = ecart >= 0 ? "+" : "";
        return `${r.libelle}\nValeur : ${r[valKey]?.toFixed(1)}${unitLabel}\nÉcart France : ${signe}${ecart?.toFixed(1)}`;
      }
    })
  ],
  width: 700, height: 650,
  style: {fontFamily: "Inter, sans-serif", fontSize: "12px"}
})
```

::: {.note-lecture}
Carte expérimentale OJS — les écarts à la moyenne France permettent de visualiser les départements au-dessus (bleu) et en dessous (rouge) du niveau national. La palette est symétrique autour de zéro.
:::

## Le retournement post-2021

Le retournement 2022-2024 frappe inégalement les territoires. Côté prix, 16 départements seulement sont en baisse — tous en Île-de-France ou dans le Nord : Paris (−3,6 %), les Hauts-de-Seine (−2,4 %), la Seine-Saint-Denis (−1,7 %). À l'inverse, la Lozère (+6,8 %), les Côtes-d'Armor (+6,5 %) et la Haute-Loire (+6,3 %) poursuivent leur rattrapage. Le scatter boom/correction (r = −0,41) confirme un mécanisme de retour à la moyenne : les départements qui avaient le plus monté entre 2014 et 2019 corrigent le plus. Les outliers — Côtes-d'Armor, Finistère, Morbihan — échappent à ce schéma, portés par l'attractivité résidentielle de la façade bretonne.

:::: {.column-page}

::: {layout-ncol=2}

```{r}
#| label: plt-carte-evol-prix
#| fig-width: 6.5
#| fig-height: 5.5
#| out-width: "100%"

tm_shape(map_dep) +
  tm_polygons(fill = "logd_px2_global_vevol_2224",
    fill.scale = tm_scale_intervals(
      breaks = c(-15, -10, -5, -2, 0, 2, 5, 10, 15),
      values = make_pal(pal_urbn_div, 8), midpoint = NA),
    fill.legend = tm_legend_clean("% 22-24"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("Évolution prix 2022-2024") +
  tm_source(make_source("DVF"))
```

```{r}
#| label: plt-carte-evol-trans
#| fig-width: 6.5
#| fig-height: 5.5
#| out-width: "100%"

tm_shape(map_dep) +
  tm_polygons(fill = "logd_trans_vevol_2224",
    fill.scale = tm_scale_intervals(
      breaks = c(-40, -30, -20, -10, 0, 10, 20, 30, 40),
      values = make_pal(pal_urbn_div, 8), midpoint = NA),
    fill.legend = tm_legend_clean("% 22-24"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("Évolution transactions 2022-2024") +
  tm_source(make_source("DVF"))
```

:::

::::

::: {.note-lecture}
L'IDF et les marchés chers corrigent le plus en prix. La province résiste mieux sur les prix mais subit le recul des volumes. Le retournement est massif (médiane transactions : `r fmt_sign(med_evol_trans)` %) mais inégal.
:::

## Boom puis correction ?

::: {.panel-tabset}

### Scatter accélération / décélération

```{r}
#| label: plt-scatter-boom-correction
#| fig-height: 7

dep_evol <- dep %>%
  filter(!is.na(logd_px2_global_vevol_1419), !is.na(logd_px2_global_vevol_2224))

scatter_quadrants(
  dep_evol,
  x = "logd_px2_global_vevol_1419", y = "logd_px2_global_vevol_2224",
  title = "Boom 2014-2019 vs correction 2022-2024",
  subtitle = glue("{nrow(dep_evol)} DEP | Ceux qui ont le plus monté corrigent-ils le plus ?"),
  xlab = "Hausse prix 2014-2019 (%)",
  ylab = "Évolution prix 2022-2024 (%)",
  hline = 0, vline = 0,
  quad_labels = c("Hausse continue", "Stagnant → hausse",
                  "Double stagnation", "Boom → correction")
) +
  labs(caption = make_source("DVF"))
```

::: {.note-lecture}
Quadrant bas-droite = départements qui avaient le plus monté et qui corrigent maintenant. Le lien boom→correction est attendu (retour à la moyenne) mais pas systématique.
:::

### Top/Bottom évolutions

```{r}
#| label: plt-bar-evol-trans

plot_bar_ranked(
  dep %>% filter(!is.na(logd_trans_vevol_2224)),
  var = "logd_trans_vevol_2224", label_col = "libelle",
  n_show = 15,
  title = "Recul des transactions 2022-2024",
  subtitle = glue("Médiane = {fmt_sign(med_evol_trans)} % | {n_dep_baisse_trans}/{N_DEP} DEP en baisse"),
  xlab = "Évolution transactions (%)",
  caption = make_source("DVF")
)
```

### Top/Bottom prix

```{r}
#| label: plt-bar-prix-dep

plot_bar_ranked(
  dep, var = "logd_px2_global_24", label_col = "libelle",
  n_show = 15,
  title = glue("Facteur {ratio_px} entre extrêmes"),
  subtitle = "Top/Bottom 15 départements — prix m² DVF 2024",
  xlab = "Prix m² (EUR)",
  caption = make_source("DVF 2024")
)
```

:::

<!-- vvr: Accessibilité = enrichir avec ratio prix/loyer (nb années) en carte DEP -->

## Loyers et accessibilité par département

:::: {.column-page}

::: {layout-ncol=2}

```{r}
#| label: plt-carte-loyer-appt
#| fig-width: 6.5
#| fig-height: 5.5
#| out-width: "100%"

tm_shape(map_dep) +
  tm_polygons(fill = "logl_app_m2_25",
    fill.scale = tm_scale_intervals(
      breaks = c(0, 8, 10, 12, 14, 16, 20, 30, 50),
      values = make_pal(pal_urbn_seq_blue, 8)),
    fill.legend = tm_legend_clean("EUR/m²"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("Loyer appart — ANIL 2025") +
  tm_source(make_source("ANIL"))
```

```{r}
#| label: plt-carte-loyer-evol
#| fig-width: 6.5
#| fig-height: 5.5
#| out-width: "100%"

tm_shape(map_dep) +
  tm_polygons(fill = "logl_app_evol_2225",
    fill.scale = tm_scale_intervals(
      breaks = c(-5, 0, 5, 8, 10, 12, 15, 20, 30),
      values = make_pal(pal_urbn_div, 8), midpoint = NA),
    fill.legend = tm_legend_clean("% 22-25"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("Évolution loyers 2022-2025") +
  tm_source(make_source("ANIL"))
```

:::

::::

::: {.note-lecture}
Les loyers sont structurellement plus stables que les prix à l'achat, mais l'accélération post-COVID est réelle : la médiane d'évolution départementale atteint `r fmt_fr(median(dep$logl_app_evol_2225, na.rm = TRUE), 1)` % en 3 ans. Les marchés tendus (IDF, littoral) restent les plus chers.
:::

<!-- vvr: Scatter prix×loyer = candidat conversion plotly interactif (hover DEP) -->

### Scatter prix × loyer

```{r}
#| label: plt-scatter-prix-loyer
#| fig-height: 7

dep_loyer <- dep %>%
  filter(!is.na(logd_px2_global_24), !is.na(logl_app_m2_25))
labs_loyer <- label_top_bottom(dep_loyer, "logl_app_m2_25", n = 10)

ggplot(dep_loyer, aes(x = logl_app_m2_25, y = logd_px2_global_24)) +
  geom_point(aes(size = P22_POP,
                 color = logd_px2_global_vevol_2224),
             alpha = 0.7) +
  geom_text_repel(
    data = dep_loyer %>% filter(libelle %in% labs_loyer),
    aes(label = libelle), size = 2.8, max.overlaps = 20,
    color = col_spacegray, segment.color = col_gray
  ) +
  scale_size_continuous(range = c(1.5, 8), guide = "none") +
  scale_color_gradient2(low = col_red, mid = col_gray, high = col_green,
                        midpoint = 0, name = "Évol. prix\n22-24 (%)") +
  scale_y_continuous(labels = label_comma(big.mark = "\u202f")) +
  labs(
    title = "Prix à l'achat vs loyer : deux marchés corrélés mais distincts",
    subtitle = glue("{nrow(dep_loyer)} DEP | Couleur = évolution prix 22-24 | Taille = population"),
    x = "Loyer appart (EUR/m²) — ANIL 2025",
    y = "Prix achat m² — DVF 2024",
    caption = make_source("DVF", "ANIL")
  )
```

::: {.note-lecture}
La corrélation prix×loyer est forte mais imparfaite : les départements en rouge (baisse des prix 2022-2024) sont surtout ceux où le prix d'achat était très élevé relativement au loyer (IDF notamment). Les loyers ne corrigent pas.
:::

## Construction et concentration

::: {.panel-tabset}

### Carte et taux

:::: {.column-page}

::: {layout-ncol=2}

```{r}
#| label: plt-carte-construction-p2
#| fig-width: 6.5
#| fig-height: 5.5
#| out-width: "100%"

tm_shape(map_dep) +
  tm_polygons(fill = "logs_logcom_tx1000_2224",
    fill.scale = tm_scale_intervals(n = 7,
      values = make_pal(pal_urbn_seq_green, 7)),
    fill.legend = tm_legend_clean("log/1000 hab"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("Logements commencés / 1 000 hab") +
  tm_source(make_source("SITADEL 22-24"))
```

```{r}
#| label: plt-carte-surface-moy
#| fig-width: 6.5
#| fig-height: 5.5
#| out-width: "100%"

map_dep_surf <- map_dep %>%
  mutate(surf_moy_logcom = ifelse(logs_logcom_vol_2224 > 0,
    logs_sdpcom_vol_2224 / logs_logcom_vol_2224, NA))

tm_shape(map_dep_surf) +
  tm_polygons(fill = "surf_moy_logcom",
    fill.scale = tm_scale_intervals(
      breaks = c(0, 50, 60, 70, 80, 90, 100, 120, 200),
      values = make_pal(pal_urbn_div, 8)),
    fill.legend = tm_legend_clean("m² / logement"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("Surface moyenne par logement commencé") +
  tm_source(make_source("SITADEL 22-24"))
```

:::

::::

::: {.note-lecture}
Zones urbaines denses : collectif petit (< 70 m²). Zones rurales : pavillonnaire (> 100 m²). Moyenne nationale : `r fmt_fr(surf_moy_nat, 0)` m² par logement.
:::

### Lorenz (concentration)

```{r}
#| label: plt-lorenz-construction
#| fig-height: 6.5

constr_vals <- dep$logs_logcom_vol_2224[!is.na(dep$logs_logcom_vol_2224) &
               dep$logs_logcom_vol_2224 > 0]
constr_sorted <- sort(constr_vals)
n_c <- length(constr_sorted)
cum_share <- cumsum(constr_sorted) / sum(constr_sorted)
pop_share <- seq_len(n_c) / n_c
lorenz_df <- data.frame(pop = c(0, pop_share), constr = c(0, cum_share))

p80_idx <- which.min(abs(pop_share - 0.80))
constr_at_80 <- cum_share[p80_idx] * 100

ggplot(lorenz_df, aes(x = pop, y = constr)) +
  geom_line(color = col_cyan, linewidth = 1.2) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = col_gray) +
  geom_segment(aes(x = 0.80, xend = 0.80, y = 0, yend = cum_share[p80_idx]),
               linetype = "dotted", color = col_orange) +
  geom_segment(aes(x = 0, xend = 0.80, y = cum_share[p80_idx], yend = cum_share[p80_idx]),
               linetype = "dotted", color = col_orange) +
  annotate("text", x = 0.55, y = cum_share[p80_idx] + 0.04,
           label = glue("80% des DEP = {fmt_pct(constr_at_80, 0)} de la construction"),
           size = 3.5, color = col_orange, fontface = "bold") +
  annotate("text", x = 0.35, y = 0.85,
           label = glue("Gini = {fmt_fr(gini_constr, 3)}"),
           size = 4, color = col_cyan, fontface = "bold") +
  scale_x_continuous(labels = percent_format(), expand = c(0, 0.01)) +
  scale_y_continuous(labels = percent_format(), expand = c(0, 0.01)) +
  labs(title = "Construction neuve : forte concentration territoriale",
       subtitle = "Courbe de Lorenz — logements commencés par département",
       x = "Part cumulée des départements",
       y = "Part cumulée des logements commencés",
       caption = make_source("SITADEL 22-24"))
```

### Construction × démographie

```{r}
#| label: plt-scatter-construction-pop
#| fig-height: 7

scatter_quadrants(
  dep %>% filter(!is.na(logs_logcom_tx1000_2224)),
  x = "dm_pop_vtcam_1622", y = "logs_logcom_tx1000_2224",
  title = "On construit où la population croît",
  subtitle = glue("r = {fmt_fr(cor(dep$dm_pop_vtcam_1622, dep$logs_logcom_tx1000_2224, use='complete.obs'), 2)} | {N_DEP} DEP"),
  xlab = "TCAM population 2016-2022 (%/an)",
  ylab = "Logements commencés / 1 000 hab",
  hline = med_txcom, vline = 0,
  quad_labels = c("Croissant\n+ construit", "Déclinant\n+ construit",
                  "Déclinant\n- construit", "Croissant\n- construit")
) +
  labs(caption = make_source("SITADEL", "INSEE RP"))
```

:::

<!-- cld: Vacance = signal fort de déprise. r=-0.45 vacance×SMA. Carte + scatter + bar ranked -->

## Vacance structurelle et déprise

[**Vacance 2+ ans : médiane `r fmt_pct(med_vac)` par département. `r n_dep_vac_hausse` DEP voient leur vacance augmenter 2020-2024. La corrélation vacance × SMA = `r fmt_fr(cor_vac_sma, 2)` confirme le lien entre déclin démographique et abandon du parc.**]{.insight}

La vacance de longue durée (plus de 2 ans) touche inégalement le territoire : le ratio entre le 9e et le 1er décile atteint 2,6 (CV = 0,38). La Nièvre (7,0 %), la Haute-Corse (6,0 %) et l'Allier (5,7 %) concentrent les taux les plus élevés. Fait notable : 12 départements voient leur vacance augmenter entre 2020 et 2024, et ce sont tous des territoires d'Île-de-France — Paris (+1,2 pt), la Seine-Saint-Denis (+1,1 pt), le Val-de-Marne (+1,0 pt). À l'inverse, la Creuse (−6,1 pts) et la Charente (−3,4 pts) voient leur vacance reculer fortement, possiblement par un effet de destruction nette du parc ou de conversion en résidences secondaires.

:::: {.column-page}

::: {layout-ncol=2}

```{r}
#| label: plt-carte-vacance
#| fig-width: 6.5
#| fig-height: 5.5
#| out-width: "100%"

tm_shape(map_dep) +
  tm_polygons(fill = "logv_vac2ans_pct_24",
    fill.scale = tm_scale_intervals(breaks = breaks_vac,
      values = rev(make_pal(pal_urbn_seq_green, 8))),
    fill.legend = tm_legend_clean("% vac 2+ans"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("Vacance 2+ ans — LOVAC 2024") +
  tm_source(make_source("LOVAC"))
```

```{r}
#| label: plt-carte-vacance-evol
#| fig-width: 6.5
#| fig-height: 5.5
#| out-width: "100%"

tm_shape(map_dep) +
  tm_polygons(fill = "logv_vac2ans_vdifp_2024",
    fill.scale = tm_scale_intervals(n = 7, style = "quantile",
      values = make_pal(pal_urbn_div, 7)),
    fill.legend = tm_legend_clean("pts 20-24"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("Évolution vacance 2020-2024 (pts)") +
  tm_source(make_source("LOVAC"))
```

:::

::::

::: {.note-lecture}
Le Nord-Est et le Massif central concentrent vacance élevée et aggravation. La corrélation vacance × SMA confirme que la vacance est un thermomètre de la déprise résidentielle.
:::

::: {.panel-tabset}

### Scatter vacance × SMA

```{r}
#| label: plt-scatter-vacance-sma
#| fig-height: 7

scatter_quadrants(
  dep, x = "dm_sma_vtcam_1622", y = "logv_vac2ans_pct_24",
  title = "Déclin démographique = vacance longue durée",
  subtitle = glue("r = {fmt_fr(cor_vac_sma, 2)} | LOVAC 2+ ans vs SMA 16-22 | {N_DEP} DEP"),
  xlab = "SMA TCAM 16-22 (%/an)", ylab = "Vacance 2+ ans (%)",
  hline = med_vac, vline = 0,
  quad_labels = c("Attractif + tendu", "Attractif + détendu",
                  "Déclin + détendu", "Déclin + tendu")
) +
  labs(caption = make_source("LOVAC", "INSEE RP"))
```

### Top/Bottom vacance

```{r}
#| label: plt-bar-vacance

plot_bar_ranked(
  dep, var = "logv_vac2ans_pct_24", label_col = "libelle",
  n_show = 15,
  title = "Vacance 2+ ans — Top/Bottom 15 DEP",
  subtitle = glue("LOVAC 2024 | Médiane = {fmt_pct(med_vac)}"),
  xlab = "Vacance 2+ ans (%)",
  caption = make_source("LOVAC 2024")
)
```

:::

---

# Partie 3 — Le logement selon le type de territoire

<!-- cld: P3 = communes agrégées par CATEAAV2020 (5 types) + TAAV2017 (6 tailles). TODO: scatter pôle vs couronne, facets AAV -->

```{r}
#| label: setup-partie3
#| include: false

# &s &AAV_AGGREGATE - Agrégation communes par catégorie AAV

# Joindre TDC pour récupérer CATEAAV2020 et TAAV2017
comm_aav <- comm %>%
  left_join(
    tdc %>% select(CODGEO, CATEAAV2020, TAAV2017, TAAV2017lib, TDAAV2017lib),
    by = c("code" = "CODGEO")
  ) %>%
  filter(!is.na(CATEAAV2020))

# Labels propres CATEAAV
comm_aav <- comm_aav %>%
  mutate(
    type_aav = case_when(
      CATEAAV2020 == 11 ~ "Pôle principal",
      CATEAAV2020 == 12 ~ "Banlieue",
      CATEAAV2020 == 13 ~ "Couronne secondaire",
      CATEAAV2020 == 20 ~ "Couronne périurbaine",
      CATEAAV2020 == 30 ~ "Hors attraction",
      TRUE ~ "Autre"
    ),
    type_aav = factor(type_aav, levels = c(
      "Pôle principal", "Banlieue", "Couronne secondaire",
      "Couronne périurbaine", "Hors attraction"
    ))
  )

# Labels taille AAV
comm_aav <- comm_aav %>%
  mutate(
    taille_aav = case_when(
      TAAV2017 == 0 ~ "Hors attraction",
      TAAV2017 == 1 ~ "< 50k hab",
      TAAV2017 == 2 ~ "50-200k hab",
      TAAV2017 == 3 ~ "200-700k hab",
      TAAV2017 == 4 ~ "≥ 700k hab (hors Paris)",
      TAAV2017 == 5 ~ "Paris",
      TRUE ~ "Autre"
    ),
    taille_aav = factor(taille_aav, levels = c(
      "Paris", "≥ 700k hab (hors Paris)", "200-700k hab",
      "50-200k hab", "< 50k hab", "Hors attraction"
    ))
  )

# Variables à agréger (moyennes pondérées par population)
pop_col <- "P22_POP"

# Fonction d'agrégation pondérée
agg_weighted <- function(df, group_var) {
  df %>%
    filter(!is.na(.data[[pop_col]]) & .data[[pop_col]] > 0) %>%
    group_by(.data[[group_var]]) %>%
    summarise(
      n_comm = n(),
      pop = sum(.data[[pop_col]], na.rm = TRUE),
      # Prix DVF (moyennes pondérées)
      px_mai = weighted.mean(logd_px2q2_mai_24, .data[[pop_col]], na.rm = TRUE),
      px_appt = weighted.mean(logd_px2q2_appt_24, .data[[pop_col]], na.rm = TRUE),
      px_global = weighted.mean(logd_px2_global_24, .data[[pop_col]], na.rm = TRUE),
      # Évolutions prix
      evol_px_2224 = weighted.mean(logd_px2_global_vevol_2224, .data[[pop_col]], na.rm = TRUE),
      evol_px_1419 = weighted.mean(logd_px2_global_vevol_1419, .data[[pop_col]], na.rm = TRUE),
      # Vacance
      vac_pct = weighted.mean(logv_vac2ans_pct_24, .data[[pop_col]], na.rm = TRUE),
      vac_evol = weighted.mean(logv_vac2ans_vdifp_2024, .data[[pop_col]], na.rm = TRUE),
      # Construction
      constr_total = sum(logs_logcom_vol_2224, na.rm = TRUE),
      constr_tx = constr_total / (pop / 1000),
      # Loyers
      loyer_app = weighted.mean(logl_app_m2_25, .data[[pop_col]], na.rm = TRUE),
      loyer_mai = weighted.mean(logl_mai_m2_25, .data[[pop_col]], na.rm = TRUE),
      # Démographie
      sma = weighted.mean(dm_sma_vtcam_1622, .data[[pop_col]], na.rm = TRUE),
      pop_tcam = weighted.mean(dm_pop_vtcam_1622, .data[[pop_col]], na.rm = TRUE),
      .groups = "drop"
    )
}

# Agréger par catégorie position (5 types)
agg_cateaav <- agg_weighted(comm_aav, "type_aav")

# Agréger par taille AAV (6 types)
agg_taav <- agg_weighted(comm_aav, "taille_aav")

N_COMM_AAV <- nrow(comm_aav)
# &e
```

[**Le gradient centre-périphérie structure le marché : les pôles principaux affichent des prix `r fmt_fr(agg_cateaav$px_global[agg_cateaav$type_aav == "Pôle principal"] / agg_cateaav$px_global[agg_cateaav$type_aav == "Hors attraction"], 1)`× supérieurs aux communes hors attraction, mais aussi la vacance la plus faible. La couronne périurbaine concentre `r fmt_fr(agg_cateaav$n_comm[agg_cateaav$type_aav == "Couronne périurbaine"] / N_COMM_AAV * 100, 0)` % des communes mais un marché de plus en plus tendu.**]{.insight}

::: {.chapeau}
Le passage à l'échelle des aires d'attraction révèle un gradient centre-périphérie systématique. Les pôles principaux concentrent les prix les plus élevés et la vacance la plus faible. Les communes hors attraction cumulent prix bas, vacance élevée et solde migratoire négatif — le signal clair d'une déprise résidentielle. La couronne périurbaine, qui rassemble la majorité des communes, présente un profil intermédiaire avec une construction encore dynamique mais des prix en tension croissante.
:::

## Prix par position dans l'aire d'attraction

```{r}
#| label: plt-bar-prix-cateaav
#| fig-height: 6

# Couleurs par type
cols_aav <- c(
  "Pôle principal" = col_cyan,
  "Banlieue" = "#46abdb",
  "Couronne secondaire" = col_orange,
  "Couronne périurbaine" = col_green,
  "Hors attraction" = col_gray
)

ggplot(agg_cateaav, aes(x = type_aav, y = px_global, fill = type_aav)) +
  geom_col(width = 0.7) +
  geom_text(aes(label = paste0(fmt_fr(px_global, 0), " €")),
            vjust = -0.5, size = 3.5, fontface = "bold", color = col_spacegray) +
  scale_fill_manual(values = cols_aav, guide = "none") +
  scale_y_continuous(labels = label_comma(big.mark = "\u202f"), expand = expansion(mult = c(0, 0.15))) +
  labs(
    title = "Le gradient centre-périphérie des prix immobiliers",
    subtitle = glue("{N_COMM_AAV} communes | Prix m² moyen pondéré par population | DVF 2024"),
    x = NULL, y = "Prix m² (EUR)",
    caption = make_source("DVF", "INSEE AAV 2020")
  )
```

## Profils par catégorie AAV

```{r}
#| label: tbl-profils-cateaav

# Tableau synthétique par catégorie
tbl_cateaav <- agg_cateaav %>%
  select(
    type_aav, n_comm, pop,
    px_global, evol_px_2224, loyer_app,
    constr_tx, vac_pct, sma
  ) %>%
  mutate(pop = pop / 1e6)

rt_styled(
  tbl_cateaav,
  title = "Profils logement par position dans l'aire d'attraction",
  source = make_source("DVF", "LOVAC", "SITADEL", "ANIL", "INSEE AAV 2020"),
  columns = list(
    type_aav = colDef(name = "Type", minWidth = 160, style = list(fontWeight = "500")),
    n_comm = col_num("Communes", digits = 0),
    pop = col_num("Pop (M)", digits = 2),
    px_global = col_num("Prix m²", digits = 0),
    evol_px_2224 = col_variation(tbl_cateaav, "evol_px_2224", "Évol. prix", "% 22-24"),
    loyer_app = col_num("Loyer appt", digits = 1),
    constr_tx = col_num("Constr./1000h", digits = 1),
    vac_pct = col_num("Vacance %", digits = 2),
    sma = col_variation(tbl_cateaav, "sma", "SMA", "TCAM 16-22")
  )
)
```

::: {.note-lecture}
Les pôles principaux ont les prix les plus élevés et la vacance la plus basse. Les communes hors attraction cumulent vacance élevée, prix bas et SMA négatif : c'est le signal de déprise résidentielle. La couronne périurbaine affiche un profil intermédiaire avec une construction dynamique.
:::

## Prix et vacance par taille d'aire

```{r}
#| label: plt-facet-taille-aav
#| fig-height: 7
#| fig-width: 10

# Scatter taille AAV : prix vs vacance
agg_taav_plot <- agg_taav %>%
  filter(!is.na(px_global), !is.na(vac_pct))

ggplot(agg_taav_plot, aes(x = vac_pct, y = px_global, size = pop / 1e6)) +
  geom_point(aes(color = taille_aav), alpha = 0.8) +
  geom_text_repel(aes(label = taille_aav), size = 3.2,
                  max.overlaps = 10, color = col_spacegray) +
  scale_size_continuous(range = c(3, 15), name = "Pop (M)") +
  scale_color_manual(values = c(
    "Paris" = "#0a4c6a",
    "≥ 700k hab (hors Paris)" = col_cyan,
    "200-700k hab" = "#46abdb",
    "50-200k hab" = col_green,
    "< 50k hab" = col_orange,
    "Hors attraction" = col_red
  ), guide = "none") +
  scale_y_continuous(labels = label_comma(big.mark = "\u202f")) +
  labs(
    title = "Plus l'aire est grande, plus le prix est élevé et la vacance faible",
    subtitle = "6 catégories de taille AAV | Taille = population totale",
    x = "Vacance 2+ ans (%)", y = "Prix m² (EUR)",
    caption = make_source("DVF", "LOVAC", "INSEE AAV 2020")
  )
```

## Évolution prix : qui accélère, qui corrige ?

```{r}
#| label: plt-evol-cateaav
#| fig-height: 6

evol_long <- agg_cateaav %>%
  select(type_aav, `2014-2019` = evol_px_1419, `2022-2024` = evol_px_2224) %>%
  tidyr::pivot_longer(-type_aav, names_to = "période", values_to = "evolution")

ggplot(evol_long, aes(x = type_aav, y = evolution, fill = période)) +
  geom_col(position = "dodge", width = 0.7) +
  geom_hline(yintercept = 0, color = col_gray) +
  scale_fill_manual(values = c("2014-2019" = col_cyan, "2022-2024" = col_orange)) +
  labs(
    title = "Les pôles principaux : boom le plus fort, correction la plus nette",
    subtitle = "Évolution prix m² par type de territoire | DVF moyennes pondérées",
    x = NULL, y = "Évolution prix (%)",
    fill = "Période",
    caption = make_source("DVF", "INSEE AAV 2020")
  )
```

::: {.note-lecture}
Le pattern boom→correction est le plus marqué dans les pôles principaux. Les communes hors attraction ont moins monté mais ne corrigent pas non plus : le marché y est structurellement atone (peu de transactions).
:::

## Tableau synthèse par taille d'aire

```{r}
#| label: tbl-profils-taav

tbl_taav <- agg_taav %>%
  select(
    taille_aav, n_comm, pop,
    px_global, evol_px_2224, loyer_app,
    constr_tx, vac_pct, pop_tcam
  ) %>%
  mutate(pop = pop / 1e6)

rt_styled(
  tbl_taav,
  title = "Profils logement par taille d'aire d'attraction",
  source = make_source("DVF", "LOVAC", "SITADEL", "ANIL", "INSEE AAV 2020"),
  columns = list(
    taille_aav = colDef(name = "Taille AAV", minWidth = 180, style = list(fontWeight = "500")),
    n_comm = col_num("Communes", digits = 0),
    pop = col_num("Pop (M)", digits = 2),
    px_global = col_num("Prix m²", digits = 0),
    evol_px_2224 = col_variation(tbl_taav, "evol_px_2224", "Évol. prix", "% 22-24"),
    loyer_app = col_num("Loyer appt", digits = 1),
    constr_tx = col_num("Constr./1000h", digits = 1),
    vac_pct = col_num("Vacance %", digits = 2),
    pop_tcam = col_variation(tbl_taav, "pop_tcam", "Pop TCAM", "% 16-22")
  )
)
```

---

<!-- vvr: Synthèse = vérifier chiffres inline R après render, ajouter lien vers dashboard si pertinent -->

# Synthèse

::: {.chapeau}
Le marché du logement français combine trois dynamiques simultanées : un retournement cyclique des volumes (−19 pts de transactions depuis le pic), une fracture géographique persistante (facteur 12,6 entre prix extrêmes) et une divergence structurelle entre vente et location. La construction neuve, concentrée sur l'arc atlantique (Gini = 0,480), ne répond plus qu'aux territoires déjà attractifs. Les 12 départements en hausse de vacance — tous franciliens — signalent un retournement inédit du marché le plus tendu du pays.
:::

::: {.callout-tip}
## 5 constats principaux

**1. Un cycle complet en 10 ans** — Hausse 2014-2021 (+`r fmt_fr(pic_px_mai$idx_px_mai - 100, 0)` % maisons) puis correction modérée sur les prix, brutale sur les volumes (−`r chute_pct_trans` pts de transactions).

**2. Un marché à deux vitesses** — Facteur `r ratio_px` entre les prix extrêmes départementaux. L'arc atlantique-méditerranéen concentre prix élevés et construction active.

**3. Le logement neuf en chute libre** — `r fmt_fr(total_logcom / 1000, 0)`k logements commencés 22-24, en baisse de `r fmt_fr(100 - sit_idx %>% filter(ANNEE == 2024) %>% pull(idx_logcom), 0)` % par rapport à 2014. Gini construction = `r fmt_fr(gini_constr, 3)`.

**4. La vacance comme thermomètre de déprise** — r(vacance, SMA) = `r fmt_fr(cor_vac_sma, 2)`. Les `r n_dep_vac_hausse` DEP en hausse de vacance dessinent la carte de la dévitalisation.

**5. Le gradient centre-périphérie structure tout** — Les pôles principaux ont des prix `r fmt_fr(agg_cateaav$px_global[agg_cateaav$type_aav == "Pôle principal"] / agg_cateaav$px_global[agg_cateaav$type_aav == "Hors attraction"], 1)`× supérieurs aux communes hors attraction, mais aussi la vacance la plus faible et les corrections les plus nettes.
:::

### Bilan hypothèses

| Hypothèse | Résultat |
|-----------|----------|
| **H4** : Prix = proxy attractivité | **Partiellement** — le prix mélange rente foncière et attractivité courante (r(prix,SMA)≈0.28) |
| **H6** : Cycle foncier prédictif | **Confirmé partiellement** — le scatter boom→correction montre un retour à la moyenne, surtout dans les pôles |

::: {.grey-section}
**Variables manquantes** pour la phase 2 : séries DVF infra-annuelles par trimestre, indices composites (idxlogtens, idxgentri), accessibilité revenus×prix (Filosofi), BDNB ancienneté du parc.
:::

---

# Annexe — Données détaillées {.appendix}

:::: {.column-page}

```{r}
#| label: tbl-synthese-dep

synth <- dep %>%
  summarise(
    across(
      c(logd_px2_global_24, logd_px2_global_vevol_2224, logd_trans_vevol_2224,
        logs_logcom_tx1000_2224, logv_vac2ans_pct_24, logl_app_m2_25),
      list(med = ~median(.x, na.rm = TRUE),
           q10 = ~quantile(.x, 0.10, na.rm = TRUE),
           q90 = ~quantile(.x, 0.90, na.rm = TRUE)),
      .names = "{.col}__{.fn}")
  ) %>%
  tidyr::pivot_longer(everything(), names_to = c("var", "stat"), names_sep = "__") %>%
  tidyr::pivot_wider(names_from = stat, values_from = value) %>%
  mutate(
    label = case_when(
      var == "logd_px2_global_24" ~ "Prix m² (EUR)",
      var == "logd_px2_global_vevol_2224" ~ "Évol. prix 22-24 (%)",
      var == "logd_trans_vevol_2224" ~ "Évol. transactions 22-24 (%)",
      var == "logs_logcom_tx1000_2224" ~ "Construction / 1 000 hab",
      var == "logv_vac2ans_pct_24" ~ "Vacance 2+ ans (%)",
      var == "logl_app_m2_25" ~ "Loyer appt (EUR/m²)"
    ),
    ecart = q90 - q10
  ) %>%
  select(label, med, q10, q90, ecart)

rt_styled(
  synth,
  title = "Tableau récapitulatif — distributions départementales",
  source = make_source("DVF", "SITADEL", "LOVAC", "ANIL"),
  columns = list(
    label = colDef(name = "Indicateur", minWidth = 180, style = list(fontWeight = "500")),
    med = col_num("Médiane", digits = 1),
    q10 = col_num("P10", digits = 1),
    q90 = col_num("P90", digits = 1),
    ecart = colDef(
      header = header_unit("Écart P90-P10", "dispersion"),
      cell = bar_simple(max(synth$ecart, na.rm = TRUE), fill = col_cyan, bar_width = 60),
      minWidth = 150
    )
  )
)
```

::::

```{r}
#| label: log-save
#| include: false

# &s &LOG_RESULTATS - Logging enrichi (auto-profile + plot descriptions)

# Métriques manuelles clés
log_result("fr_px", fr_px, interpret = glue("Prix France = {fmt_fr(fr_px, 0)} EUR/m²"))
log_result("cor_vac_sma", cor_vac_sma, interpret = glue("Vacance x SMA = {fmt_fr(cor_vac_sma, 2)}"))
log_result("gini_construction", gini_constr, interpret = glue("Gini construction = {fmt_fr(gini_constr, 3)}"))
log_result("surf_moy_nat", surf_moy_nat, interpret = glue("Surface moy = {fmt_fr(surf_moy_nat, 0)} m²"))
log_result("ratio_px_extremes", ratio_px, interpret = glue("Ratio prix extrêmes = {ratio_px}"))
log_result("chute_transactions", chute_pct_trans, interpret = glue("Chute transactions depuis pic = -{chute_pct_trans} pts"))
log_result("pic_px_maisons", pic_px_mai$idx_px_mai, interpret = glue("Pic indice maisons = {fmt_fr(pic_px_mai$idx_px_mai, 0)} (année {pic_px_mai$annee})"))
log_result("n_dep_baisse_px", n_dep_baisse_px, interpret = glue("{n_dep_baisse_px}/{N_DEP} DEP en baisse de prix 22-24"))
log_result("n_dep_baisse_trans", n_dep_baisse_trans, interpret = glue("{n_dep_baisse_trans}/{N_DEP} DEP en baisse de transactions 22-24"))
log_result("med_evol_px", med_evol_px, interpret = glue("Médiane évol prix DEP = {fmt_sign(med_evol_px)} %"))
log_result("med_evol_trans", med_evol_trans, interpret = glue("Médiane évol transactions DEP = {fmt_sign(med_evol_trans)} %"))
log_result("fr_loyer_app", fr_loyer_app, interpret = glue("Loyer appt France = {fmt_fr(fr_loyer_app, 1)} EUR/m²"))
log_result("fr_loyer_app_evol", fr_loyer_app_evol, interpret = glue("Évol loyer appt 22-25 = +{fmt_fr(fr_loyer_app_evol, 1)} %"))

# Auto-profile DEP (distributions + corrélations sur indicateurs clés)
log_auto_profile(dep,
  cols = c("logd_px2_global_24", "logd_px2_global_vevol_2224", "logd_trans_vevol_2224",
           "logs_logcom_tx1000_2224", "logv_vac2ans_pct_24", "logv_vac2ans_vdifp_2024",
           "logl_app_m2_25", "logl_app_evol_2225", "dm_sma_vtcam_1622", "dm_pop_vtcam_1622"),
  label_col = "libelle", n_top = 5
)

# Descriptions cartes
log_map_auto("carte_prix_dep", "logd_px2_global_24", dep, "libelle")
log_map_auto("carte_vacance_dep", "logv_vac2ans_pct_24", dep, "libelle")
log_map_auto("carte_construction_dep", "logs_logcom_tx1000_2224", dep, "libelle")
log_map_auto("carte_loyer_dep", "logl_app_m2_25", dep, "libelle")
log_map_auto("carte_evol_prix_dep", "logd_px2_global_vevol_2224", dep, "libelle")

# Descriptions scatter
log_scatter_auto("scatter_boom_correction", dep,
  "logd_px2_global_vevol_1419", "logd_px2_global_vevol_2224", "libelle")
log_scatter_auto("scatter_prix_loyer", dep,
  "logl_app_m2_25", "logd_px2_global_24", "libelle")
log_scatter_auto("scatter_vacance_sma", dep,
  "dm_sma_vtcam_1622", "logv_vac2ans_pct_24", "libelle")
log_scatter_auto("scatter_construction_pop", dep,
  "dm_pop_vtcam_1622", "logs_logcom_tx1000_2224", "libelle")

# Lorenz
log_plot_description("lorenz_construction", "lorenz",
  variables = "logs_logcom_vol_2224",
  n_points = sum(!is.na(dep$logs_logcom_vol_2224) & dep$logs_logcom_vol_2224 > 0),
  pattern = glue("Gini={fmt_fr(gini_constr, 3)}, 80% DEP = ~{fmt_fr(100 - gini_constr * 50, 0)}% construction"))

# Rankings enrichis (describe_ranking v3)
log_describe_ranking("bar_evol_trans", dep, "libelle", "logd_trans_vevol_2224")
log_describe_ranking("bar_prix", dep, "libelle", "logd_px2_global_24")
log_describe_ranking("bar_vacance", dep, "libelle", "logv_vac2ans_pct_24")
log_describe_ranking("bar_construction", dep, "libelle", "logs_logcom_tx1000_2224")

# Détails enrichis non affichés (_detail_ convention)
log_result("_detail_cycle_national", list(
  pic_annee = pic_px_mai$annee,
  pic_indice = round(pic_px_mai$idx_px_mai, 1),
  chute_pts_trans = chute_pct_trans,
  idx_constr_2024 = round(sit_idx %>% filter(ANNEE == 2024) %>% pull(idx_logcom), 1),
  fr_loyer_mai = fr_loyer_mai,
  ratio_prix_loyer = round(as.numeric(FR_LOG$logd_px2_global_24) / (fr_loyer_app * 12), 1)
))

log_result("_detail_geo_fractures", list(
  ratio_prix_extremes = ratio_px,
  n_dep_baisse_px = n_dep_baisse_px,
  n_dep_baisse_trans = n_dep_baisse_trans,
  cor_prix_sma = round(cor(dep$logd_px2_global_24, dep$dm_sma_vtcam_1622, use = "complete.obs"), 2),
  cor_prix_constr = round(cor(dep$logd_px2_global_24, dep$logs_logcom_tx1000_2224, use = "complete.obs"), 2),
  med_loyer_evol = round(median(dep$logl_app_evol_2225, na.rm = TRUE), 1)
))

log_result("_detail_vacance_dispersion", list(
  cv_vacance = round(sd(dep$logv_vac2ans_pct_24, na.rm = TRUE) / mean(dep$logv_vac2ans_pct_24, na.rm = TRUE), 2),
  ratio_p90_p10_vac = round(quantile(dep$logv_vac2ans_pct_24, 0.9, na.rm = TRUE) /
                            quantile(dep$logv_vac2ans_pct_24, 0.1, na.rm = TRUE), 1),
  n_dep_vac_hausse = n_dep_vac_hausse
))

# &e

claude_log_save()
```
