---
title: "Dynamiques démographiques & Attractivité résidentielle"
subtitle: "Volet 1 — Croissance, flux migratoires et profils territoriaux"
author: "Vincent R."
date: last-modified
date-format: "D MMMM YYYY"
categories: ["Rapport d'analyse"]
number-sections: false
format:
  html:
    code-fold: true
    code-summary: "Code"
execute:
  echo: true
  warning: false
  message: false
---

<!-- Rapport volet Démographie (rpt-pdemog-rapport-demogresid_260217)
     Type : RAPPORT (code-fold, insight-first, rédigé)
     Données : INSEE RP 2011-2022 · MIGCOM 2016-2022 · dbcln DEP + commARM
     Helpers : _setup-common.R → jcn-graph.R, jcn-table.R, jcn-map.R, jcn-claude-log.R
     Config  : hérite de _quarto.yml (theme-html.scss, toc-right)
     Structure : Introduction + 5 parties (H2) — P1 ZE · P2 Trajectoires · P3 DEP · P4 OD · P5 Communes + Synthèse + Annexe
     Convention : gdl-rgen-CONVENTION-REDAC-tpl v1.2
-->

<!-- Style inline sécurité — override SCSS si besoin -->
<style>
.insight { color: #1a5276; font-weight: 600; font-size: 1.02em; line-height: 1.5; }
.note-lecture { font-size: 0.82em; font-style: italic; color: #666; margin-top: 0.5rem; border-left: 3px solid #ddd; padding-left: 10px; }
</style>

<!-- ============================================================
          ============================================================ -->

::: {.callout-note collapse="true" icon=false}


## Introduction

Chaque année, 7,3 millions de Français changent de commune de résidence — soit 11 % de la population. Ces flux migratoires, combinés aux dynamiques naturelles (naissances, décès), recomposent silencieusement la carte démographique du pays. Le recensement 2022 confirme un ralentissement quasi-généralisé : la croissance annuelle moyenne de la population nationale est passée de +0,5 % à +0,3 % entre les deux dernières périodes intercensitaires.

**Les flux migratoires compensent-ils le déficit naturel croissant, et cette compensation varie-t-elle selon le type de territoire ?**

Ce rapport analyse les dynamiques démographiques sur la période 2011-2022, à trois échelles emboîtées : 287 zones d'emploi (croissance et compensation), 96 départements (géographie des migrations par catégorie de population) et ~34 000 communes agrégées par type de territoire (gradient urbain-rural). Les données mobilisées sont le recensement de la population (RP 2011-2022) et les fichiers détail MIGCOM (2016, 2022) pour les flux migratoires par âge et catégorie socioprofessionnelle. Trois hypothèses structurent l'analyse :

- **H1** — L'attractivité migratoire ne compense pas le déficit naturel dans les ZE vieillissantes
- **H2** — Les cadres et les retraités migrent vers des territoires distincts
- **H3** — Le gradient urbain-rural structure les dynamiques démographiques

::: {.callout-note collapse="true"}
## Méthodologie

**Sources** : INSEE RP 2011-2022 (populations légales, soldes naturels), MIGCOM 2016 et 2022 (flux migratoires détaillés par âge, CSP, commune d'origine/destination). **Périmètre** : France métropolitaine hors DOM (287 ZE, 96 départements, ~34 000 communes). **Indicateurs** : TCAM population, solde naturel (SN) et solde migratoire apparent (SMA) en taux annuels, taux de rotation (TR = (entrants + sortants) / pop), indice de vieillissement (IV = 60+ / 0-14 × 100), taux migratoire interne (TMI) par catégorie. **Tests** : Kruskal-Wallis sur 4 indicateurs × 4 types TYPO4pers, taille d'effet par eta² = H/(N−1). **Limites** : MIGCOM ne couvre que les migrations internes (pas l'international) ; le SMA est un résidu (variation pop − SN), pas un flux observé ; rupture de série MIGCOM 2011 (passage quinquennal → annuel).
:::


```{r}
#| label: setup
#| include: false

# &s &SETUP_aaMAIN - Setup rapport démographie v2

# &s &PACKAGES - Helpers + libs supplémentaires
source("_setup-common.R")

suppressPackageStartupMessages({
  library(sf)
  library(tmap)
})
source("C:/Users/vince/hh/pq/PDS/mutils/jrr/jcn-map.R")

tmap_mode("plot")
# &e

# &s &CLAUDE_LOG - Logging résultats inter-sessions
claude_log_init("pdemog-demogresid",
                output_dir = file.path(PROJECT_ROOT, "reports/outputs"))
log_step("SETUP", n_ze = N_ZE, n_cols = ncol(ze))
# &e

# &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 + enrichissement
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 DEP
fr_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"))
FR_DEP <- if (nrow(fr_dep) > 0) as.list(fr_dep[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)
# &e

# &s &GEO_JOIN - Jointure data × shapes DEP
map_dep <- shp_dep %>%
  left_join(dep, by = c("code_geo" = "code"), suffix = c("", ".data"))
# &e

# &s &KPI_P1 - Métriques ZE partie 1
tcam_t1 <- ze$dm_pop_vtcam_1116
tcam_t2 <- ze$dm_pop_vtcam_1622

n_decline_t1 <- sum(tcam_t1 < 0, na.rm = TRUE)
n_decline_t2 <- sum(tcam_t2 < 0, na.rm = TRUE)
pct_decline_t2 <- round(100 * n_decline_t2 / N_ZE, 0)

fr_tcam_t1 <- as.numeric(FR$dm_pop_vtcam_1116)
fr_tcam_t2 <- as.numeric(FR$dm_pop_vtcam_1622)

n_ralenti <- sum(ze$dm_pop_vtcam_1622 < ze$dm_pop_vtcam_1116, na.rm = TRUE)
pct_ralenti <- round(100 * n_ralenti / N_ZE, 0)

# Quadrants SN × SMA
ze <- ze %>%
  mutate(
    quad_snsma = case_when(
      dm_sn_vtcam_1622 >= 0 & dm_sma_vtcam_1622 >= 0 ~ "SN+ SMA+ (Double moteur)",
      dm_sn_vtcam_1622 < 0  & dm_sma_vtcam_1622 >= 0 ~ "SN- SMA+ (Compensation)",
      dm_sn_vtcam_1622 < 0  & dm_sma_vtcam_1622 < 0  ~ "SN- SMA- (Double déficit)",
      dm_sn_vtcam_1622 >= 0 & dm_sma_vtcam_1622 < 0  ~ "SN+ SMA- (Fuite)",
      TRUE ~ NA_character_
    )
  )

quad_counts <- ze %>% count(quad_snsma) %>% arrange(desc(n))
n_compensation <- sum(ze$quad_snsma == "SN- SMA+ (Compensation)", na.rm = TRUE)
n_double_deficit <- sum(ze$quad_snsma == "SN- SMA- (Double déficit)", na.rm = TRUE)

# Test H1
ze_vieilles <- ze %>% filter(dmv_iv_ind_22 > 100)
n_vieilles <- nrow(ze_vieilles)
n_compense_vieilles <- sum(ze_vieilles$dm_sma_vtcam_1622 > abs(ze_vieilles$dm_sn_vtcam_1622),
                           na.rm = TRUE)
pct_compense <- round(100 * n_compense_vieilles / n_vieilles, 0)
n_sma_pos_vieilles <- sum(ze_vieilles$dm_sma_vtcam_1622 > 0, na.rm = TRUE)
# &e

# &s &KPI_P2 - Métriques DEP partie 2
med_sma_dep <- median(dep$dm_sma_vtcam_1622, na.rm = TRUE)
n_dep_attractifs <- sum(dep$dm_sma_vtcam_1622 > 0, na.rm = TRUE)
range_tmi_cadre <- range(dep$dmf_tmi_cscadre_22, na.rm = TRUE)
ratio_tmi_cadre <- round(range_tmi_cadre[2] / max(abs(range_tmi_cadre[1]), 0.01), 1)

cor_tr_sma_dep <- cor(dep$dmf_tr_pct_22, dep$dm_sma_vtcam_1622, use = "complete.obs")
cor_cad_65_dep <- cor(dep$dmf_tmi_cscadre_22, dep$dmf_tmi_a65p_22, use = "complete.obs")

log_step("KPI_COMPUTED",
         pct_decline_t2 = pct_decline_t2,
         n_compensation = n_compensation,
         n_dep_attractifs = n_dep_attractifs)
# &e

# &s &TRAJ_SMA - Classification trajectoire SMA 6 types (T1=11-16, T2=16-22)
ze <- ze %>%
  mutate(
    sma_diff = dm_sma_vtcam_1622 - dm_sma_vtcam_1116,
    trajectoire = case_when(
      dm_sma_vtcam_1116 > 0 & dm_sma_vtcam_1622 > 0 & sma_diff > 0  ~ "Attractif renforcé",
      dm_sma_vtcam_1116 > 0 & dm_sma_vtcam_1622 > 0 & sma_diff <= 0 ~ "Attractif en tassement",
      dm_sma_vtcam_1116 <= 0 & dm_sma_vtcam_1622 > 0                 ~ "Retournement positif",
      dm_sma_vtcam_1116 > 0 & dm_sma_vtcam_1622 <= 0                 ~ "Retournement négatif",
      dm_sma_vtcam_1116 <= 0 & dm_sma_vtcam_1622 <= 0 & sma_diff > 0  ~ "Déclin atténué",
      dm_sma_vtcam_1116 <= 0 & dm_sma_vtcam_1622 <= 0 & sma_diff <= 0 ~ "Déclin accentué",
      TRUE ~ "Autre"
    ),
    trajectoire = factor(trajectoire, levels = c(
      "Attractif renforcé", "Attractif en tassement",
      "Retournement positif", "Retournement négatif",
      "Déclin atténué", "Déclin accentué"
    ))
  )

traj_counts <- ze %>% count(trajectoire, .drop = FALSE) %>% arrange(trajectoire)
n_renforce <- traj_counts$n[traj_counts$trajectoire == "Attractif renforcé"]
n_tassement <- traj_counts$n[traj_counts$trajectoire == "Attractif en tassement"]
n_retpos <- traj_counts$n[traj_counts$trajectoire == "Retournement positif"]
n_retneg <- traj_counts$n[traj_counts$trajectoire == "Retournement négatif"]
n_declin_att <- traj_counts$n[traj_counts$trajectoire == "Déclin atténué"]
n_declin_acc <- traj_counts$n[traj_counts$trajectoire == "Déclin accentué"]
pct_renforce <- round(100 * n_renforce / N_ZE, 0)
pct_retournement <- round(100 * (n_retpos + n_retneg) / N_ZE, 0)

cor_sma_t1t2 <- cor(ze$dm_sma_vtcam_1116, ze$dm_sma_vtcam_1622, use = "complete.obs")

# Couleurs trajectoire (vert→orange→rouge)
traj_colors <- c(
  "Attractif renforcé" = col_green,
  "Attractif en tassement" = col_cyan,
  "Retournement positif" = "#73bfe2",
  "Retournement négatif" = col_yellow,
  "Déclin atténué" = col_spacegray,
  "Déclin accentué" = col_red
)
# &e

# &e &SETUP_aaMAIN
```


## Partie 1 — La France ralentit : croissance et compensation

<!-- vvr: P1 = 288 ZE, données existantes setup-common. Nettoyage du rpt-10 existant -->

::: {.kpi-strip}
::: {.kpi-item}
[`r pct_decline_t2`%]{.kpi-value}
[ZE en déclin 16-22]{.kpi-label}
:::
::: {.kpi-item}
[`r fmt_sign(fr_tcam_t2)`%]{.kpi-value}
[TCAM France]{.kpi-label}
:::
::: {.kpi-item}
[`r pct_ralenti`%]{.kpi-value}
[ZE qui ralentissent]{.kpi-label}
:::
:::

::: {.chapeau}
- **Ralentissement** : le TCAM national passe de `r fmt_sign(fr_tcam_t1)`% à `r fmt_sign(fr_tcam_t2)`% entre les deux périodes, et `r pct_ralenti`% des ZE décélèrent ou déclinent davantage
- **Compensation** : dans `r n_compensation` ZE, le solde migratoire compense partiellement un déficit naturel croissant — mais cette substitution reste insuffisante dans la majorité des territoires vieillissants
- **Fragilité** : `r n_double_deficit` ZE cumulent solde naturel et solde migratoire négatifs, concentrées dans la diagonale continentale
:::


### Le déclin démographique s'accélère entre 2016 et 2022 {#qe11}

[**`r n_decline_t2` zones d'emploi sur `r N_ZE` perdent de la population entre 2016 et 2022**, contre `r n_decline_t1` sur la période précédente. Le ralentissement est quasi-généralisé : `r pct_ralenti`% des ZE croissent moins vite ou déclinent davantage.]{.insight}
La corrélation entre les TCAM des deux périodes reste forte (r = 0,85) : les zones d'emploi en croissance restent globalement en croissance, celles en déclin continuent de décliner. Mais la pente est inférieure à 1 (0,83) — signe d'une convergence par le bas. Le TCAM moyen passe de **+0,27 %/an** à **+0,16 %/an** sur l'ensemble des 287 ZE *(figure ci-dessous)*.

Sur ces 287 zones d'emploi, 119 cumulent un déclin aux deux périodes (quadrant bas-gauche du scatter), tandis que 27 basculent de la croissance au déclin. Seules 26 ZE accélèrent entre T1 et T2 — presque toutes des métropoles du sud et de l'ouest.

```{r}
#| label: plt-tcam-distrib
#| fig-cap: "Distribution du TCAM population par période"
#| fig-height: 5

ze_long <- ze %>%
  select(libelle, dm_pop_vtcam_1116, dm_pop_vtcam_1622) %>%
  pivot_longer(cols = starts_with("dm_pop_vtcam"),
               names_to = "periode", values_to = "tcam") %>%
  mutate(periode = ifelse(grepl("1116", periode), "2011-2016", "2016-2022"))

ggplot(ze_long, aes(x = tcam, fill = periode)) +
  geom_histogram(bins = 35, alpha = 0.6, position = "identity", color = "white") +
  geom_vline(xintercept = 0, linetype = "solid", color = "#333", linewidth = 0.5) +
  geom_vline(xintercept = fr_tcam_t1, linetype = "dashed", color = col_cyan, linewidth = 0.7) +
  geom_vline(xintercept = fr_tcam_t2, linetype = "dashed", color = col_spacegray, linewidth = 0.7) +
  scale_fill_manual(values = c("2011-2016" = col_cyan, "2016-2022" = col_spacegray)) +
  labs(x = "TCAM population (%/an)", y = "Nombre de ZE", fill = "Période",
       caption = make_source("INSEE RP")) +
  annotate("text", x = fr_tcam_t1 + 0.05, y = Inf, label = "FR 11-16",
           vjust = 2, hjust = 0, size = 3, color = col_cyan) +
  annotate("text", x = fr_tcam_t2 + 0.05, y = Inf, label = "FR 16-22",
           vjust = 3.5, hjust = 0, size = 3, color = col_spacegray)
```

```{r}
#| label: plt-tcam-trajectory
#| fig-cap: "Trajectoire TCAM T1 vs T2 — Accélération ou décrochage ?"
#| fig-height: 6

plot_scatter_plotly(ze,
  x = "dm_pop_vtcam_1116", y = "dm_pop_vtcam_1622",
  size_col = "P22_POP", n_labels = 12,
  ref_lines = "diagonal",
  quadrant_labels = c("Accélération", "", "", "Ralentissement"),
  xlab = "TCAM population 2011-2016 (%/an)",
  ylab = "TCAM population 2016-2022 (%/an)",
  caption = make_source("INSEE RP"),
  height = 550)
```

```{r}
#| label: tbl-tcam-topbot
#| tbl-cap: "Top et bottom 15 — TCAM population 2016-2022"

tb <- table_top_bottom(ze, "dm_pop_vtcam_1622", n = 15,
                       extra_cols = c("dm_pop_vtcam_1116", "P22_POP"))

tb_data <- bind_rows(
  tb$top %>% mutate(groupe = "Top 15"),
  tb$bottom %>% mutate(groupe = "Bottom 15")
)

rt_styled(
  tb_data,
  groupBy = "groupe",
  height = 500,
  columns = list(
    rang = colDef(name = "#", width = 45),
    libelle = colDef(name = "Zone d'emploi", minWidth = 160),
    dm_pop_vtcam_1622 = col_variation(tb_data, "dm_pop_vtcam_1622",
                                       label = "TCAM 16-22", unit = "%/an"),
    dm_pop_vtcam_1116 = col_variation(tb_data, "dm_pop_vtcam_1116",
                                       label = "TCAM 11-16", unit = "%/an"),
    P22_POP = col_pop(tb_data, "P22_POP", label = "Pop. 2022"),
    groupe = colDef(show = FALSE)
  )
)
```

Les extrêmes confirment la polarisation territoriale. En tête, Toulouse (+1,29 %/an), Montpellier (+1,19 %) et Bordeaux (+1,05 %) affichent un TCAM supérieur à +1 %/an sur les deux périodes — tirées par l'emploi métropolitain et l'attractivité résidentielle. En queue de classement, les zones d'emploi rurales du quart nord-est (Bar-le-Duc, Vitry-le-François, Saint-Dié) perdent entre **−0,5 et −0,9 %/an**. Le décrochage est structurel : les 15 ZE les plus dynamiques gagnent en moyenne 3 fois plus que la croissance nationale, tandis que les 15 dernières perdent 4 fois plus vite.

Ce ralentissement généralisé résulte-t-il d'un déficit naturel croissant, et le solde migratoire parvient-il à compenser ?


### Le SMA compense partiellement le déficit naturel {#qe12}

[**`r n_compensation` ZE compensent un déficit naturel par l'attractivité migratoire** (quadrant SN- SMA+), soit le groupe le plus fréquent. Mais **`r n_double_deficit` ZE cumulent les deux déficits** — territoires en voie de dévitalisation.]{.insight}
Le scatter SN × SMA dessine quatre profils distincts *(figure ci-dessous)*. La corrélation négative (r = −0,32) traduit une substitution partielle : les territoires en déficit naturel attirent des migrants, mais cette compensation reste rarement suffisante pour inverser le déclin. Le quadrant dominant est la compensation (90 ZE, SN- SMA+), à parité avec le profil de fuite (90 ZE, SN+ SMA-) — un quasi-équilibre qui reflète le balancier entre métropoles productrices de naturel et territoires résidentiels vieillissants.

Parmi les `r n_double_deficit` ZE en double déficit, les territoires de la diagonale continentale dominent — zones rurales vieillissantes (Meuse, Nièvre, Creuse, Cantal) où ni la natalité ni l'attractivité ne parviennent à stabiliser la population. À l'opposé, 52 ZE combinent solde naturel et solde migratoire positifs (double moteur), concentrées dans le Grand Ouest (Nantes, Rennes, Bordeaux) et quelques pôles alpins.

```{r}
#| label: plt-sn-sma
#| fig-cap: "Diagramme SN × SMA — Les 4 profils démographiques"
#| fig-height: 7

scatter_quadrants(
  ze, x = "dm_sn_vtcam_1622", y = "dm_sma_vtcam_1622",
  title = "Solde naturel vs. solde migratoire apparent (2016-2022)",
  subtitle = "Chaque point = 1 ZE, taille = population",
  xlab = "Solde naturel TCAM (%/an)",
  ylab = "SMA TCAM (%/an)",
  quad_labels = c("Double moteur", "Compensation", "Double déficit", "Fuite")
)
```

```{r}
#| label: tbl-quadrants
#| tbl-cap: "Répartition des ZE par quadrant SN × SMA"

rt_styled(
  quad_counts %>% rename(`Profil` = quad_snsma, `Nb ZE` = n),
  columns = list(
    `Profil` = colDef(minWidth = 220),
    `Nb ZE` = col_num("Nb ZE")
  ),
  fullWidth = FALSE
)
```

La répartition par quadrants éclaire les mécanismes sous-jacents. Le profil de fuite (SN+ SMA-) regroupe 90 ZE — essentiellement des pôles urbains qui produisent du naturel mais perdent des résidents au profit de leur couronne. Ce flux centrifuge alimente les 90 ZE en compensation (SN- SMA+), où l'arrivée de nouveaux résidents masque temporairement le vieillissement structurel. La table ci-dessus confirme que les deux profils dominants sont symétriques : le même nombre de ZE gagne ce que l'autre perd.

#### Test H1 : compensation dans les ZE vieillissantes

::: {.callout-warning}
## Test H1 — L'attractivité migratoire ne compense pas le déficit naturel dans les ZE vieillissantes

Parmi les **`r n_vieilles` ZE vieillissantes** (IV > 100), **`r n_sma_pos_vieilles`** ont un SMA positif — elles attirent plus qu'elles ne perdent. Mais la compensation est insuffisante : seulement **`r pct_compense`%** ont un SMA qui dépasse leur déficit naturel en valeur absolue.

**H1 partiellement confirmée** : le vieillissement reste le facteur dominant du déclin — l'attractivité migratoire ralentit le déclin sans l'inverser.
:::

Le bilan macro masque des trajectoires divergentes. Si le ralentissement est quasi-généralisé, certaines ZE renforcent leur attractivité tandis que d'autres décrochent — la dynamique entre les deux périodes révèle ces destins contrastés.


## Partie 2 — Trajectoires migratoires : qui résiste entre T1 et T2 ?

<!-- vvr: P2 = trajectoires SMA T1→T2 (NOUVEAU, données ZE existantes) -->
<!-- cld: Classification 6 types, scatter SMA T1×T2, barres trajectoire. Données calculées dans setup TRAJ_SMA -->

::: {.kpi-strip}
::: {.kpi-item}
[`r pct_renforce`%]{.kpi-value}
[ZE en renforcement]{.kpi-label}
:::
::: {.kpi-item}
[`r n_retpos + n_retneg`]{.kpi-value}
[ZE en retournement]{.kpi-label}
:::
::: {.kpi-item}
[`r n_declin_acc`]{.kpi-value}
[ZE en déclin accentué]{.kpi-label}
:::
:::

::: {.chapeau}
- **Persistance** : la corrélation SMA entre T1 et T2 est forte (r = `r fmt_fr(cor_sma_t1t2, 2)`) — les territoires attractifs le restent, les territoires répulsifs aussi
- **Renforcement** : `r n_renforce` ZE voient leur SMA positif s'améliorer encore, essentiellement dans le Grand Ouest et le littoral méditerranéen
- **Décrochage** : `r n_declin_acc` ZE s'enfoncent dans le déclin migratoire, concentrées dans la diagonale continentale et le quart nord-est industriel
:::


### Plus d'un quart des ZE renforcent leur attractivité migratoire

[**`r n_renforce` ZE sur `r N_ZE` combinent un SMA positif aux deux périodes et une trajectoire ascendante**, contre `r n_declin_acc` en déclin accentué. Les retournements — positifs (`r n_retpos`) ou négatifs (`r n_retneg`) — concernent `r pct_retournement`% des ZE.]{.insight}
La classification en six trajectoires migratoires *(figure ci-dessous)* révèle une géographie des destins contrastés. Les deux profils dominants — attractif renforcé et déclin accentué — représentent ensemble plus de la moitié des ZE. Le premier concentre les zones littorales, périurbaines et certains pôles du Grand Ouest. Le second regroupe les territoires de la diagonale continentale, du quart nord-est et des bassins industriels en reconversion.

Les retournements sont plus rares mais signalent des dynamiques en cours. Les `r n_retpos` retournements positifs (passage de SMA- à SMA+) indiquent des territoires qui retrouvent une attractivité — souvent des zones de proximité métropolitaine ou des communes rurales bénéficiant du desserrement résidentiel post-COVID. Les `r n_retneg` retournements négatifs (passage de SMA+ à SMA-) signalent au contraire un essoufflement, parfois dans des zones touristiques dont l'attractivité structurelle s'effrite.

```{r}
#| label: plt-traj-bar
#| fig-cap: "Trajectoires migratoires des ZE — Classification en 6 types"
#| fig-height: 5.5

traj_plot <- traj_counts %>%
  mutate(
    pct = round(100 * n / sum(n), 1),
    label = glue("{n} ({pct}%)")
  )

ggplot(traj_plot, aes(x = reorder(trajectoire, n), y = n, fill = trajectoire)) +
  geom_col(width = 0.7) +
  geom_text(aes(label = label), hjust = -0.1, size = 3.2, color = col_spacegray) +
  coord_flip(clip = "off") +
  scale_fill_manual(values = traj_colors, guide = "none") +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
  labs(x = NULL, y = "Nombre de ZE",
       caption = make_source("INSEE RP"))
```

```{r}
#| label: tbl-traj-detail
#| tbl-cap: "Profils moyens par trajectoire migratoire"

traj_profil <- ze %>%
  filter(!is.na(trajectoire)) %>%
  group_by(trajectoire) %>%
  summarise(
    n = n(),
    sma_t1 = weighted.mean(dm_sma_vtcam_1116, P22_POP, na.rm = TRUE),
    sma_t2 = weighted.mean(dm_sma_vtcam_1622, P22_POP, na.rm = TRUE),
    delta = sma_t2 - sma_t1,
    pop_tcam = weighted.mean(dm_pop_vtcam_1622, P22_POP, na.rm = TRUE),
    iv = weighted.mean(dmv_iv_ind_22, P22_POP, na.rm = TRUE),
    .groups = "drop"
  )

rt_styled(
  traj_profil,
  title = "Indicateurs moyens pondérés par type de trajectoire",
  source = make_source("INSEE RP"),
  columns = list(
    trajectoire = colDef(name = "Trajectoire", minWidth = 180, style = list(fontWeight = "500")),
    n = col_num("N"),
    sma_t1 = col_variation(traj_profil, "sma_t1", label = "SMA T1", unit = "%/an"),
    sma_t2 = col_variation(traj_profil, "sma_t2", label = "SMA T2", unit = "%/an"),
    delta = col_variation(traj_profil, "delta", label = "Delta", unit = "pts"),
    pop_tcam = col_variation(traj_profil, "pop_tcam", label = "TCAM pop", unit = "%/an"),
    iv = col_num("IV", digits = 0)
  )
)
```

::: {.note-lecture}
Le SMA (solde migratoire apparent) est un résidu : variation de population − solde naturel. Il intègre les migrations internes, internationales et les ajustements de comptage. La classification en 6 types repose sur le signe du SMA et son évolution entre T1 (2011-2016) et T2 (2016-2022).
:::

Le tableau confirme la cohérence des profils. Les ZE en renforcement affichent un TCAM population nettement positif et un indice de vieillissement modéré — elles cumulent dynamisme naturel et attractivité migratoire. À l'opposé, les ZE en déclin accentué présentent l'IV le plus élevé et le TCAM population le plus négatif. Les retournements positifs se distinguent par un delta SMA important (passage de négatif à positif), souvent associé à un vieillissement intermédiaire.


### L'attractivité migratoire persiste entre les deux périodes

[**La corrélation entre SMA T1 et SMA T2 est de `r fmt_fr(cor_sma_t1t2, 2)`** — les territoires attractifs le restent, les territoires répulsifs aussi. Mais la pente inférieure à 1 indique une convergence : les SMA extrêmes se rapprochent de la moyenne.]{.insight}
```{r}
#| label: plt-scatter-sma-t1t2
#| fig-cap: "SMA T1 vs SMA T2 — Persistance et convergence de l'attractivité migratoire"
#| fig-height: 7

plot_scatter_plotly(ze,
  x = "dm_sma_vtcam_1116", y = "dm_sma_vtcam_1622",
  size_col = "P22_POP", color_col = "trajectoire",
  palette = unname(traj_colors), n_labels = 10,
  ref_lines = "diagonal",
  quadrant_labels = c("Renforcement", "", "", "Tassement"),
  xlab = "SMA TCAM 2011-2016 (%/an)",
  ylab = "SMA TCAM 2016-2022 (%/an)",
  caption = make_source("INSEE RP"),
  height = 600)
```

Le scatter SMA T1 × T2 révèle une structure en quatre cadrans. Le quadrant haut-droit (attractif renforcé + tassement) concentre les ZE du Grand Ouest et du littoral, tandis que le quadrant bas-gauche (déclin atténué + accentué) regroupe la diagonale continentale. Les retournements se lisent dans les quadrants haut-gauche (positifs) et bas-droit (négatifs). La pente inférieure à 1 signifie que les ZE aux SMA extrêmes convergent vers la moyenne — les très attractifs le deviennent un peu moins, les très répulsifs un peu moins aussi. Ce mouvement de convergence est cohérent avec celui observé sur le TCAM population *(cf. partie 1)*.

Ces trajectoires dessinent une géographie des destins migratoires. Mais à l'échelle départementale, quels territoires captent ces flux, et les cadres migrent-ils vers les mêmes destinations que les retraités ?


## Partie 3 — Les profils migratoires départementaux

<!-- vvr: P3 (ex-P2) = 96 DEP, cartes tmap + scatter TMI -->
<!-- cld: Données DEP chargées dans setup. Cartes divergentes pour SMA/SN, séquentielle pour IV -->

::: {.kpi-strip}
::: {.kpi-item}
[`r fmt_sign(med_sma_dep, 2)`%]{.kpi-value}
[Médiane SMA (DEP)]{.kpi-label}
:::
::: {.kpi-item}
[`r n_dep_attractifs`]{.kpi-value}
[DEP attractifs]{.kpi-label}
:::
::: {.kpi-item}
[`r fmt_fr(range_tmi_cadre[2] - range_tmi_cadre[1], 1)` pts]{.kpi-value}
[Amplitude TMI cadres]{.kpi-label}
:::
:::

::: {.chapeau}
- **Géographie** : `r n_dep_attractifs` départements sur 96 affichent un SMA positif, avec un arc atlantique-méditerranéen qui capte l'essentiel des flux entrants tandis que la diagonale continentale se vide
- **Brassage** : la corrélation entre taux de rotation et SMA est quasi-nulle (r = `r fmt_fr(cor_tr_sma_dep, 2)`) — un fort brassage résidentiel ne garantit pas l'attractivité nette
- **Profils** : les départements attractifs captent à la fois cadres et retraités (r = `r fmt_fr(cor_cad_65_dep, 2)`) — le clivage n'est pas entre catégories mais entre territoires
:::


### L'arc atlantique-méditerranéen capte les flux, la diagonale se vide

[**`r n_dep_attractifs` départements sur 96 affichent un SMA positif**, mais la dispersion est considérable : de −1,18 %/an (Paris) à +1,36 %/an (Corse-du-Sud). Le solde naturel dessine une géographie inversée — les départements franciliens cumulent le SN le plus élevé et le SMA le plus négatif.]{.insight}
```{r}
#| label: prep-breaks-dep
#| include: false

# Breaks cartes DEP
breaks_sma <- c(-Inf, -0.6, -0.3, -0.1, 0, 0.1, 0.3, 0.6, Inf)
breaks_sn <- c(-Inf, -0.4, -0.2, -0.1, 0, 0.1, 0.2, 0.4, Inf)
breaks_iv <- c(0, 60, 80, 100, 120, 140, 160, Inf)
```

:::: {.column-page}

::: {layout-ncol=3}

```{r}
#| label: plt-carte-sma
#| fig-height: 5
#| fig-width: 5
#| out-width: "100%"
#| warning: false

tm_shape(map_dep) +
  tm_polygons(fill = "dm_sma_vtcam_1622",
    fill.scale = tm_scale_intervals(breaks = breaks_sma,
      values = make_pal(rev(pal_urbn_div), 8)),
    fill.legend = tm_legend_clean("%/an"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("SMA TCAM 2016-2022") +
  tm_source(make_source("INSEE RP"))
```

```{r}
#| label: plt-carte-sn
#| fig-height: 5
#| fig-width: 5
#| out-width: "100%"
#| warning: false

tm_shape(map_dep) +
  tm_polygons(fill = "dm_sn_vtcam_1622",
    fill.scale = tm_scale_intervals(breaks = breaks_sn,
      values = make_pal(rev(pal_urbn_div), 8)),
    fill.legend = tm_legend_clean("%/an"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("Solde naturel TCAM 2016-2022") +
  tm_source(make_source("INSEE RP"))
```

```{r}
#| label: plt-carte-iv
#| fig-height: 5
#| fig-width: 5
#| out-width: "100%"
#| warning: false

tm_shape(map_dep) +
  tm_polygons(fill = "dmv_iv_ind_22",
    fill.scale = tm_scale_intervals(breaks = breaks_iv,
      values = make_pal(pal_urbn_seq_magenta, 7)),
    fill.legend = tm_legend_clean("IV"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("Indice de vieillissement 2022") +
  tm_source(make_source("INSEE RP"))
```

:::

::::

::: {.note-lecture}
Les 3 cartes se lisent ensemble : le SMA (à gauche) dessine l'arc attractif littoral ; le SN (centre) révèle le déficit naturel structurel de la diagonale ; l'IV (droite) confirme le vieillissement des territoires en déclin. Les départements combinant SMA- et SN- sont les plus fragiles.
:::

Le triptyque confirme la dualité territoriale française. Le SMA départemental est positif dans 68 des 96 départements, avec une médiane de +0,21 %/an. La Corse-du-Sud (+1,36 %/an) et les Landes (+1,22 %/an) dominent le classement, tandis que Paris (−1,18 %/an) se distingue par une hémorragie migratoire massive. Le solde naturel dessine une géographie inversée : les départements franciliens affichent les SN les plus élevés (Seine-Saint-Denis : +1,14 %/an) quand la Creuse (−1,0 %/an) et le Cantal (−0,73 %/an) cumulent déficit naturel et vieillissement. L'indice de vieillissement varie d'un facteur 4, de 43,6 en Seine-Saint-Denis à 176,6 dans la Creuse — confirmant le lien structurel entre SN négatif et vieillissement avancé (r = −0,97 au niveau départemental).

Ces cartes dessinent des profils régionaux nets. Mais un département très mobile — où beaucoup de gens arrivent *et* partent — est-il pour autant attractif ?


### Fort brassage résidentiel ne signifie pas forte attractivité nette

[**La corrélation entre taux de rotation et SMA est quasi-nulle** (r = −0,12, p = 0,26) — statistiquement non significative. Paris illustre le découplage : TR le plus élevé de France (9,96 %) mais SMA le plus négatif (−1,18 %/an).]{.insight}
::: {.panel-tabset}

### Scatter TR × SMA

```{r}
#| label: plt-scatter-tr-sma
#| fig-cap: "Taux de rotation vs SMA — Brassage ≠ attractivité"
#| fig-height: 6

med_tr_dep <- median(dep$dmf_tr_pct_22, na.rm = TRUE)

plot_scatter_plotly(dep,
  x = "dmf_tr_pct_22", y = "dm_sma_vtcam_1622",
  size_col = "P22_POP", n_labels = 10,
  ref_lines = "none",
  xlab = "Taux de rotation (%)",
  ylab = "SMA TCAM 2016-2022 (%/an)",
  caption = make_source("MIGCOM", "INSEE RP"),
  height = 550) |>
  plotly::layout(shapes = list(
    list(type = "line", x0 = 0, x1 = 1, y0 = 0, y1 = 0,
         xref = "paper", line = list(color = "#ccc", width = 1, dash = "dash"), layer = "below"),
    list(type = "line", x0 = med_tr_dep, x1 = med_tr_dep, y0 = 0, y1 = 1,
         yref = "paper", line = list(color = "#ccc", width = 1, dash = "dash"), layer = "below")
  ))
```

### Bar SMA top/bottom

```{r}
#| label: plt-bar-sma-dep
#| fig-cap: "Top et bottom 15 — SMA départemental"
#| fig-height: 7

plot_bar_ranked(dep, var = "dm_sma_vtcam_1622", label_col = "libelle",
                n = 15, fill_bot = col_spacegray,
                title = "Attractivité migratoire nette par département",
                subtitle = "SMA TCAM 2016-2022 (%/an)",
                caption = make_source("INSEE RP"))
```

:::

La corrélation entre taux de rotation et SMA est quasi-nulle (r = −0,12, p = 0,26) — statistiquement non significative. Ce résultat invalide l'intuition selon laquelle les départements les plus brassés seraient les plus attractifs. Paris illustre ce découplage : avec un TR de 9,96 % (le plus élevé de France), le département affiche le SMA le plus négatif (−1,18 %/an). À l'inverse, la Corse-du-Sud combine un TR modéré et le SMA le plus élevé (+1,36 %/an). Le brassage reflète la mobilité résidentielle globale — entrées et sorties confondues — tandis que le SMA ne capture que le solde net.

Si le brassage ne prédit pas l'attractivité nette, la composition des flux — par catégorie socioprofessionnelle et par âge — révèle-t-elle des logiques migratoires différentes ?


### Les cadres et les retraités ne migrent pas vers les mêmes territoires

[**Contrairement à l'hypothèse initiale, la corrélation TMI cadres × TMI 65+ est positive** (r = 0,61) : les départements qui attirent des cadres attirent aussi des retraités. Le clivage n'est pas cadres *vs.* retraités, mais territoires globalement attractifs *vs.* globalement répulsifs.]{.insight}

:::: {.column-page}

::: {layout-ncol=2}

```{r}
#| label: plt-carte-tmi-cadre
#| fig-height: 6
#| fig-width: 6
#| out-width: "100%"
#| warning: false

breaks_tmi <- c(-Inf, -4, -2, -1, 0, 1, 2, 4, Inf)

tm_shape(map_dep) +
  tm_polygons(fill = "dmf_tmi_cscadre_22",
    fill.scale = tm_scale_intervals(breaks = breaks_tmi,
      values = make_pal(rev(pal_urbn_div), 8)),
    fill.legend = tm_legend_clean("pts"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("TMI Cadres 2022") +
  tm_source(make_source("MIGCOM"))
```

```{r}
#| label: plt-carte-tmi-65p
#| fig-height: 6
#| fig-width: 6
#| out-width: "100%"
#| warning: false

tm_shape(map_dep) +
  tm_polygons(fill = "dmf_tmi_a65p_22",
    fill.scale = tm_scale_intervals(breaks = breaks_tmi,
      values = make_pal(rev(pal_urbn_div), 8)),
    fill.legend = tm_legend_clean("pts"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("TMI 65 ans et plus 2022") +
  tm_source(make_source("MIGCOM"))
```

:::

::::

::: {.note-lecture}
Le TMI (taux migratoire interne) mesure le gain ou la perte nette d'une catégorie de population par rapport au stock de cette catégorie. Un TMI cadres de +2 signifie que le département gagne 2 cadres nets pour 1 000 cadres résidents.
:::

```{r}
#| label: plt-scatter-cadre-65p
#| fig-cap: "TMI cadres vs TMI 65+ — Deux géographies migratoires"
#| fig-height: 7

plot_scatter_plotly(dep,
  x = "dmf_tmi_cscadre_22", y = "dmf_tmi_a65p_22",
  size_col = "P22_POP", n_labels = 10,
  ref_lines = "auto",
  quadrant_labels = c("Perd cadres, gagne 65+", "Gagne les deux",
                      "Perd les deux", "Gagne cadres, perd 65+"),
  xlab = "TMI Cadres (points)",
  ylab = "TMI 65+ (points)",
  caption = make_source("MIGCOM"),
  height = 600)
```

```{r}
#| label: tbl-tmi-dep
#| tbl-cap: "TMI par catégorie — synthèse départementale"

tmi_synth <- dep %>%
  summarise(
    across(
      c(dmf_tmi_cscadre_22, dmf_tmi_csouvrier_22, dmf_tmi_a65p_22, dmf_tmi_a2529_22),
      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}")
  ) %>%
  pivot_longer(everything(), names_to = c("var", "stat"), names_sep = "__") %>%
  pivot_wider(names_from = stat, values_from = value) %>%
  mutate(
    label = case_when(
      grepl("cadre", var) ~ "TMI Cadres",
      grepl("ouvrier", var) ~ "TMI Ouvriers",
      grepl("a65p", var) ~ "TMI 65+",
      grepl("a2529", var) ~ "TMI 25-29 ans"
    ),
    amplitude = q90 - q10
  ) %>%
  select(label, med, q10, q90, amplitude)

rt_styled(
  tmi_synth,
  title = "Distributions TMI départementales (points)",
  source = make_source("MIGCOM"),
  columns = list(
    label = colDef(name = "Catégorie", minWidth = 140, style = list(fontWeight = "500")),
    med = col_variation(tmi_synth, "med", label = "Médiane", unit = "pts", digits = 1),
    q10 = col_num("P10", digits = 1),
    q90 = col_num("P90", digits = 1),
    amplitude = col_num("Amplitude", digits = 1)
  )
)
```

Le scatter TMI cadres × TMI 65+ (r = 0,61) révèle une corrélation positive, pas négative comme on pourrait l'attendre. Les départements qui attirent des cadres attirent aussi des retraités — la Charente-Maritime (+2,73 pts cadres, +1,01 pts 65+), le Lot (+2,61, +0,88) et les Landes (+1,22, +1,03) cumulent les deux. À l'opposé, Paris (−2,59 cadres, −1,57 retraités) perd massivement sur les deux catégories. Les 11 départements en quadrant haut-gauche (TMI cadres négatif, TMI 65+ positif) correspondent au profil "silver economy" attendu, mais ils restent minoritaires. L'amplitude TMI cadres (5,3 pts entre min et max) est 2,4 fois plus large que celle des 65+ (2,6 pts) — les cadres sont plus géographiquement sélectifs dans leurs choix migratoires.

::: {.callout-important}
## Bilan H2

**H2 nuancée** : La corrélation TMI cadres × TMI 65+ au niveau départemental est de **`r fmt_fr(cor_cad_65_dep, 2)`** — positive, pas nulle. Les territoires les plus attractifs captent à la fois cadres et retraités. Les deux géographies ne sont pas opposées mais hiérarchisées : quelques départements captent tout, d'autres perdent tout. Le clivage majeur est entre départements attractifs globalement et départements répulsifs globalement.
:::

L'échelle départementale révèle la géographie des profils migratoires, mais pas encore les origines et destinations des flux. D'où viennent ces migrants, vers quels territoires se dirigent-ils, et à quelle distance ?


## Partie 4 — Origines-Destinations : d'où viennent-ils, où vont-ils ?

<!-- vvr: P4 = matrice OD MIGCOM fichier détail, Sankey type_terr, choroplèthe DEP, butterfly top paires -->
<!-- cld: Données produites par sp02-calc-migcom-od.py. 4 vues : sankey, solde, top_flux, matrice parquet -->

```{r}
#| label: setup-partie4
#| include: false

# &s &OD_DATA_LOAD - Chargement vues OD MIGCOM

od_sankey <- read.csv(file.path(PROJECT_ROOT, "data/processed/migcom/vue_sankey_typeterr.csv"),
                      stringsAsFactors = FALSE, fileEncoding = "UTF-8-BOM")
od_solde <- read.csv(file.path(PROJECT_ROOT, "data/processed/migcom/vue_solde_dep.csv"),
                     stringsAsFactors = FALSE, fileEncoding = "UTF-8-BOM")
od_top <- read.csv(file.path(PROJECT_ROOT, "data/processed/migcom/vue_top_flux_dep.csv"),
                   stringsAsFactors = FALSE, fileEncoding = "UTF-8-BOM")

# Filtrer 2022
sk22 <- od_sankey %>% filter(annee == 2022)
solde22 <- od_solde %>% filter(annee == 2022)
top22 <- od_top %>% filter(annee == 2022)

# Métriques KPI
flux_pole_cour <- sk22 %>% filter(type_orig == "Pôle", type_dest == "Couronne") %>% pull(flux_pond)
flux_cour_pole <- sk22 %>% filter(type_orig == "Couronne", type_dest == "Pôle") %>% pull(flux_pond)
solde_net_pole_cour <- flux_pole_cour - flux_cour_pole
n_paires_od <- nrow(top22)
dep_top_solde <- solde22 %>% slice_max(solde_net, n = 1)
dep_bot_solde <- solde22 %>% slice_min(solde_net, n = 1)

# Enrichir libellés DEP
solde22 <- solde22 %>%
  left_join(lib_dep, by = c("dep" = "code_geo")) %>%
  mutate(libelle = coalesce(lib_dep, dep))

# Enrichir top flux
top22 <- top22 %>%
  left_join(lib_dep %>% rename(lib_orig = lib_dep), by = c("dep_orig" = "code_geo")) %>%
  left_join(lib_dep %>% rename(lib_dest = lib_dep), by = c("dep_dest" = "code_geo")) %>%
  mutate(lib_orig = coalesce(lib_orig, dep_orig),
         lib_dest = coalesce(lib_dest, dep_dest))

# Solde net par paire (pour butterfly)
solde_paires <- top22 %>%
  select(dep_orig, dep_dest, flux_pond, lib_orig, lib_dest) %>%
  inner_join(
    top22 %>% select(dep_orig, dep_dest, flux_pond) %>%
      rename(dep_orig2 = dep_dest, dep_dest2 = dep_orig, flux_retour = flux_pond),
    by = c("dep_orig" = "dep_orig2", "dep_dest" = "dep_dest2")
  ) %>%
  mutate(solde_net = flux_pond - flux_retour) %>%
  filter(dep_orig < dep_dest) %>%
  arrange(desc(abs(solde_net)))

# Matrice Sankey : noeud-link
sk_nodes <- data.frame(
  name = c("Pôle (orig.)", "Couronne (orig.)", "Hors AAV (orig.)",
           "Pôle (dest.)", "Couronne (dest.)", "Hors AAV (dest.)"),
  stringsAsFactors = FALSE
)

node_map_orig <- c("Pôle" = 0, "Couronne" = 1, "Hors AAV" = 2)
node_map_dest <- c("Pôle" = 3, "Couronne" = 4, "Hors AAV" = 5)

sk_links <- sk22 %>%
  mutate(
    source = node_map_orig[type_orig],
    target = node_map_dest[type_dest],
    value = round(flux_pond)
  ) %>%
  select(source, target, value)

# Log métriques
log_result("flux_pole_couronne_2022", flux_pole_cour,
           interpret = glue("{fmt_k(flux_pole_cour)} migrants Pole->Couronne 2022"))
log_result("solde_net_pole_cour", solde_net_pole_cour,
           interpret = glue("Solde net pole->couronne = {fmt_k(solde_net_pole_cour)} (couronnes captent)"))
log_result("dep_max_solde_od", dep_top_solde$dep,
           interpret = glue("DEP top solde OD = {dep_top_solde$dep} (+{fmt_k(dep_top_solde$solde_net)})"))
log_result("dep_min_solde_od", dep_bot_solde$dep,
           interpret = glue("DEP pire solde OD = {dep_bot_solde$dep} ({fmt_k(dep_bot_solde$solde_net)})"))
log_result("n_paires_od_dep", n_paires_od,
           interpret = glue("{fmt_k(n_paires_od)} paires DEP x DEP (seuil >=30)"))

# Sankey résumé
log_result("sankey_typeterr_2022", sk22 %>% select(type_orig, type_dest, flux_pond),
           interpret = paste(sk22$type_orig, "->", sk22$type_dest, ":", fmt_k(sk22$flux_pond), collapse = " | "))

# &e
```

::: {.kpi-strip}
::: {.kpi-item}
[`r fmt_k(solde_net_pole_cour)`]{.kpi-value}
[Solde net pôle-couronne]{.kpi-label}
:::
::: {.kpi-item}
[`r fmt_k(abs(dep_bot_solde$solde_net))`]{.kpi-value}
[Perte nette Paris]{.kpi-label}
:::
::: {.kpi-item}
[`r fmt_k(n_paires_od)`]{.kpi-value}
[Paires DEP significatives]{.kpi-label}
:::
:::

::: {.chapeau}
- **Flux centrifuges** : les pôles envoient `r fmt_k(flux_pole_cour)` résidents vers les couronnes mais n'en reçoivent que `r fmt_k(flux_cour_pole)` en retour, soit un solde net de `r fmt_k(solde_net_pole_cour)` en faveur des couronnes
- **Polarisation** : Paris concentre à lui seul plus de la moitié des pertes migratoires inter-départementales, avec un solde net de `r fmt_k(dep_bot_solde$solde_net)` résidents
- **Flux intra-type dominants** : la majorité des migrations se font *au sein* du même type de territoire (pôle-pôle, couronne-couronne), pas entre types
:::


### Les couronnes captent les flux centrifuges des pôles

[**Les pôles perdent `r fmt_k(solde_net_pole_cour)` résidents nets au profit des couronnes chaque année.** Mais le flux dominant reste intra-pôle (`r fmt_k(sk22$flux_pond[sk22$type_orig == "Pôle" & sk22$type_dest == "Pôle"])` migrants) : la déconcentration est un processus marginal en volume, massif en solde net.]{.insight}
```{r}
#| label: plt-sankey-typeterr
#| fig-cap: "Flux migratoires entre types de territoire (2022)"
#| fig-height: 5

suppressPackageStartupMessages(library(plotly))

fig <- plot_ly(
  type = "sankey",
  orientation = "h",
  node = list(
    label = sk_nodes$name,
    color = c(col_cyan, col_darkblue, col_green,
              col_cyan, col_darkblue, col_green),
    pad = 15,
    thickness = 20
  ),
  link = list(
    source = sk_links$source,
    target = sk_links$target,
    value = sk_links$value,
    color = c(
      paste0(col_cyan, "66"),    # Pole->Pole
      paste0(col_cyan, "66"),    # Pole->Couronne
      paste0(col_cyan, "44"),    # Pole->Hors
      paste0(col_darkblue, "66"),  # Cour->Pole
      paste0(col_darkblue, "66"),  # Cour->Cour
      paste0(col_darkblue, "44"),  # Cour->Hors
      paste0(col_green, "44"),   # Hors->Pole
      paste0(col_green, "44"),   # Hors->Cour
      paste0(col_green, "44")    # Hors->Hors
    )
  )
)

fig <- fig %>%
  plotly::layout(font = list(family = .font_family, size = 11))

fig
```

::: {.note-lecture}
Le diagramme Sankey représente les flux migratoires inter-communaux (IRAN 3-5) entre trois types de territoire AAV : pôle (commune-centre + pôle secondaire), couronne et hors AAV. L'épaisseur des liens est proportionnelle au volume pondéré (IPONDI). Les flux intra-type (pôle-pôle, couronne-couronne) sont les plus épais car la majorité des migrations se font au sein du même type de territoire. Source : MIGCOM 2022, fichier détail.
:::

Le Sankey confirme le gradient centrifuge décrit par Davezies-Talandier. Les flux pôle vers couronne (729k) dépassent nettement le retour couronne vers pôle (590k), soit un solde net de 139k en faveur des couronnes. Mais ce flux centrifuge est un processus marginal rapporté au stock : les 729k sortants des pôles représentent moins de 4 % de leur population, contre 1 132k migrants intra-pôle. La périurbanisation est un flux net modeste en proportion, mais cumulé année après année, il remodèle la géographie résidentielle.

Le flux vers le rural hors AAV est plus modeste mais asymétrique : 82k sortants des pôles et 121k sortants des couronnes, contre 76k et 99k en retour. Le rural attire un filet net positif depuis les deux autres types.


### Paris concentre la moitié des pertes migratoires inter-départementales

[**Paris perd `r fmt_k(abs(dep_bot_solde$solde_net))` résidents nets par an par échange inter-départemental** — plus que les 4 départements suivants réunis. Les départements du littoral ouest et sud captent l'essentiel de ces flux.]{.insight}

:::: {.column-page}

::: {layout-ncol=2}

```{r}
#| label: plt-carte-od-entrants
#| fig-height: 6
#| fig-width: 6
#| out-width: "100%"
#| warning: false

map_solde <- shp_dep %>%
  left_join(solde22, by = c("code_geo" = "dep"))

breaks_flux <- c(-Inf, -10000, -5000, -1000, 0, 1000, 5000, 10000, Inf)

tm_shape(map_solde) +
  tm_polygons(fill = "nb_entrants",
    fill.scale = tm_scale_intervals(
      breaks = quantile(solde22$nb_entrants, probs = seq(0, 1, length.out = 8), na.rm = TRUE),
      values = make_pal(pal_urbn_seq_blue, 7)),
    fill.legend = tm_legend_clean("individus"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("Entrants inter-DEP (2022)") +
  tm_source(make_source("MIGCOM"))
```

```{r}
#| label: plt-carte-od-sortants
#| fig-height: 6
#| fig-width: 6
#| out-width: "100%"
#| warning: false

tm_shape(map_solde) +
  tm_polygons(fill = "nb_sortants",
    fill.scale = tm_scale_intervals(
      breaks = quantile(solde22$nb_sortants, probs = seq(0, 1, length.out = 8), na.rm = TRUE),
      values = make_pal(pal_urbn_seq_magenta, 7)),
    fill.legend = tm_legend_clean("individus"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("Sortants inter-DEP (2022)") +
  tm_source(make_source("MIGCOM"))
```

:::

::::

::: {.note-lecture}
Cartes en volumes absolus (entrants/sortants inter-départementaux pondérés IPONDI). Les départements les plus peuplés concentrent mécaniquement les plus gros volumes. Comparer avec la carte choroplèthe de solde net ci-dessous pour isoler l'attractivité nette du simple effet de taille.
:::

```{r}
#| label: plt-carte-od-solde
#| fig-cap: "Solde migratoire net inter-départemental (2022)"
#| fig-height: 6
#| fig-width: 7
#| out-width: "80%"
#| warning: false

breaks_solde <- c(-Inf, -10000, -5000, -2000, 0, 2000, 5000, 10000, Inf)

tm_shape(map_solde) +
  tm_polygons(fill = "solde_net",
    fill.scale = tm_scale_intervals(breaks = breaks_solde,
      values = make_pal(rev(pal_urbn_div), 8)),
    fill.legend = tm_legend_clean("individus"),
    col = "grey70", lwd = 0.3) +
  tm_urbn("Solde migratoire net inter-DEP (2022)", "Bleu = gain net, rouge = perte nette") +
  tm_source(make_source("MIGCOM"))
```

La carte du solde net confirme la géographie observée sur le SMA départemental *(cf. P3)* : l'arc atlantique-méditerranéen capte les flux, l'Île-de-France et la diagonale continentale en perdent. Paris domine les pertes avec `r fmt_k(abs(dep_bot_solde$solde_net))` résidents nets, suivi de la Seine-Saint-Denis et des Hauts-de-Seine. Les gains se répartissent sur un arc plus large, de la Charente-Maritime au Var en passant par l'Ille-et-Vilaine.


### Les 10 premières paires de flux concentrent un quart des échanges

[**Le flux Paris vers Hauts-de-Seine est le plus important de France** (`r fmt_k(top22$flux_pond[1])` individus/an), suivi de Paris vers Val-de-Marne et Paris vers Seine-Saint-Denis. Les 10 premières paires concentrent un quart des flux inter-départementaux.]{.insight}
```{r}
#| label: plt-butterfly-od
#| fig-cap: "Top 15 paires de flux inter-départementaux (2022)"
#| fig-height: 7

top15 <- top22 %>%
  mutate(paire = paste(lib_orig, "\u2192", lib_dest)) %>%
  head(15)

ggplot(top15, aes(x = reorder(paire, flux_pond), y = flux_pond)) +
  geom_col(fill = col_cyan, width = 0.7) +
  geom_text(aes(label = scales::comma(round(flux_pond))), hjust = -0.1, size = 3, color = "#444") +
  coord_flip(clip = "off") +
  scale_y_continuous(labels = scales::comma, expand = expansion(mult = c(0, 0.2))) +
  labs(x = NULL, y = "Individus/an",
       caption = make_source("MIGCOM"))
```

```{r}
#| label: tbl-top-solde-dep
#| tbl-cap: "Top et bottom 10 — Solde migratoire inter-DEP (2022)"

tb_solde <- bind_rows(
  solde22 %>% slice_max(solde_net, n = 10) %>% mutate(groupe = "Top 10 (gains)"),
  solde22 %>% slice_min(solde_net, n = 10) %>% mutate(groupe = "Bottom 10 (pertes)")
)

rt_styled(
  tb_solde %>% select(groupe, libelle, nb_entrants, nb_sortants, solde_net, ratio_es),
  groupBy = "groupe",
  height = 500,
  columns = list(
    groupe = colDef(show = FALSE),
    libelle = colDef(name = "Departement", minWidth = 160, style = list(fontWeight = "500")),
    nb_entrants = col_num("Entrants"),
    nb_sortants = col_num("Sortants"),
    solde_net = col_variation(tb_solde, "solde_net", label = "Solde net"),
    ratio_es = col_num("Ratio E/S", digits = 2)
  )
)
```

Les flux inter-départementaux sont dominés par le système francilien. Les 5 premières paires impliquent toutes Paris (75) comme origine ou destination — le déversement vers la petite et grande couronne structure l'essentiel des mouvements. Au-delà de l'Île-de-France, les paires Nord-Pas-de-Calais (59-62) et Rhône-Ain/Isère (69-38/01) reflètent les échanges entre métropoles et leur périphérie.

Le tableau des soldes nets confirme l'asymétrie : les 10 départements les plus perdants sont tous franciliens ou limitrophes, tandis que les gagnants sont dispersés sur le littoral et l'Ouest. Le ratio entrants/sortants amplifie le signal — un ratio < 1 signifie que le département perd plus qu'il ne reçoit.

Ces flux entre départements dessinent une géographie des échanges. Mais le type de territoire — pôle, couronne, rural — structure-t-il les profils démographiques au-delà des seuls flux OD ?


## Partie 5 — Le gradient urbain-rural

<!-- vvr: P5 (ex-P3) = communes agrégées par TYPO4pers. Tester KW sur SMA, TR, SN, IV -->
<!-- cld: TYPO4pers = Poles/Cour.urb./Rur.periurb./Rur.auton. — 4 types -->

```{r}
#| label: setup-partie3
#| include: false

# &s &TYPO_AGGREGATE - Agrégation communes par TYPO4pers

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

# Charger TYPO4pers
typo <- read.csv(
  file.path(PROJECT_ROOT, "data/external/geo-tdc/ptypo-dens-rural/tdc_geo_typo_communes_2025.csv"),
  sep = ";", stringsAsFactors = FALSE, fileEncoding = "UTF-8-BOM"
)

comm_typo <- comm %>%
  left_join(typo %>% mutate(CODGEO = as.character(CODGEO)) %>% select(CODGEO, TYPO4p),
            by = c("code" = "CODGEO")) %>%
  filter(!is.na(TYPO4p))

# Labels TYPO4pers
comm_typo <- comm_typo %>%
  mutate(
    type_terr = case_when(
      TYPO4p == 1 ~ "Pôles",
      TYPO4p == 2 ~ "Couronnes urbaines",
      TYPO4p == 3 ~ "Rural périurbain",
      TYPO4p == 4 ~ "Rural autonome",
      TRUE ~ "Autre"
    ),
    type_terr = factor(type_terr, levels = c(
      "Pôles", "Couronnes urbaines", "Rural périurbain", "Rural autonome"
    ))
  )

N_COMM_TYPO <- nrow(comm_typo)

# Variables à agréger (moyennes pondérées par population)
pop_col <- "P22_POP"

agg_weighted_demog <- 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),
      pop_tcam = weighted.mean(dm_pop_vtcam_1622, .data[[pop_col]], na.rm = TRUE),
      sma = weighted.mean(dm_sma_vtcam_1622, .data[[pop_col]], na.rm = TRUE),
      sn = weighted.mean(dm_sn_vtcam_1622, .data[[pop_col]], na.rm = TRUE),
      tr = weighted.mean(dmf_tr_pct_22, .data[[pop_col]], na.rm = TRUE),
      iv = weighted.mean(dmv_iv_ind_22, .data[[pop_col]], na.rm = TRUE),
      pe = weighted.mean(dmf_pe_pct_22, .data[[pop_col]], na.rm = TRUE),
      ps = weighted.mean(dmf_ps_pct_22, .data[[pop_col]], na.rm = TRUE),
      tmi_cadre = weighted.mean(dmf_tmi_cscadre_22, .data[[pop_col]], na.rm = TRUE),
      tmi_65p = weighted.mean(dmf_tmi_a65p_22, .data[[pop_col]], na.rm = TRUE),
      .groups = "drop"
    )
}

agg_typo4 <- agg_weighted_demog(comm_typo, "type_terr")

# Gradient SMA
sma_range <- range(agg_typo4$sma, na.rm = TRUE)

# &s &BIPERIODE - Comparaison bi-période SMA/Pop/SN par type_terr

fr_sma_t1 <- as.numeric(FR$dm_sma_vtcam_1116)
fr_sma_t2 <- as.numeric(FR$dm_sma_vtcam_1622)
fr_pop_t1 <- as.numeric(FR$dm_pop_vtcam_1116)
fr_pop_t2 <- as.numeric(FR$dm_pop_vtcam_1622)

agg_biperiode <- comm_typo %>%
  filter(!is.na(P22_POP) & P22_POP > 0) %>%
  group_by(type_terr) %>%
  summarise(
    n_comm = n(),
    pop = sum(P22_POP, na.rm = TRUE),
    sma_t1 = weighted.mean(dm_sma_vtcam_1116, P22_POP, na.rm = TRUE),
    sma_t2 = weighted.mean(dm_sma_vtcam_1622, P22_POP, na.rm = TRUE),
    pop_tcam_t1 = weighted.mean(dm_pop_vtcam_1116, P22_POP, na.rm = TRUE),
    pop_tcam_t2 = weighted.mean(dm_pop_vtcam_1622, P22_POP, na.rm = TRUE),
    sn_t1 = weighted.mean(dm_sn_vtcam_1116, P22_POP, na.rm = TRUE),
    sn_t2 = weighted.mean(dm_sn_vtcam_1622, P22_POP, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  mutate(
    sma_delta = sma_t2 - sma_t1,
    pop_delta = pop_tcam_t2 - pop_tcam_t1,
    sn_delta = sn_t2 - sn_t1,
    sma_accel = sma_delta > 0
  )

type_max_accel <- as.character(agg_biperiode$type_terr[which.max(agg_biperiode$sma_delta)])
delta_max_accel <- max(agg_biperiode$sma_delta)
type_max_decel <- as.character(agg_biperiode$type_terr[which.min(agg_biperiode$sma_delta)])
delta_max_decel <- min(agg_biperiode$sma_delta)
n_types_accel <- sum(agg_biperiode$sma_accel)

# &e

# &e
```

::: {.kpi-strip}
::: {.kpi-item}
[`r fmt_k(N_COMM_TYPO)`]{.kpi-value}
[communes classées]{.kpi-label}
:::
::: {.kpi-item}
[`r fmt_k(sum(agg_typo4$pop))`]{.kpi-value}
[population totale]{.kpi-label}
:::
::: {.kpi-item}
[`r fmt_fr(sma_range[2] - sma_range[1], 2)` pts]{.kpi-value}
[gradient SMA max-min]{.kpi-label}
:::
:::

::: {.chapeau}
- **Pôles** : seul type de territoire en perte migratoire nette (SMA négatif), malgré un solde naturel positif et la moitié de la population nationale
- **Couronnes** : principal bénéficiaire des flux centrifuges, avec le SMA le plus élevé des quatre types de territoire
- **Rural** : le rural autonome cumule attractivité migratoire et vieillissement avancé (IV = 159) — il attire sans se régénérer
:::


### Le gradient urbain-rural structure les dynamiques démographiques

[**Seuls les pôles affichent un SMA négatif** (−0,10 %/an) parmi les 4 types de territoire. Les couronnes urbaines captent les flux les plus importants (+0,57 %/an), suivies du rural autonome (+0,50 %/an). Le rural autonome cumule attractivité migratoire et vieillissement avancé (IV = 159).]{.insight}
```{r}
#| label: plt-bar-sma-typo
#| fig-cap: "SMA moyen pondéré par type de territoire"
#| fig-height: 5

ggplot(agg_typo4, aes(x = reorder(type_terr, sma), y = sma, fill = sma > 0)) +
  geom_col(width = 0.6) +
  geom_hline(yintercept = 0, color = "#333", linewidth = 0.5) +
  coord_flip() +
  scale_fill_manual(values = c("TRUE" = col_cyan, "FALSE" = col_spacegray), guide = "none") +
  labs(x = NULL, y = "SMA TCAM (%/an)",
       caption = make_source("INSEE RP", "MIGCOM"))
```

```{r}
#| label: tbl-profils-typo4
#| tbl-cap: "Profils démographiques par type de territoire"

tbl_typo <- agg_typo4 %>%
  mutate(pop_pct = pop / sum(pop) * 100) %>%
  select(type_terr, n_comm, pop_pct, pop_tcam, sma, sn, tr, iv, tmi_cadre, tmi_65p)

rt_styled(
  tbl_typo,
  title = "8 indicateurs × 4 types de territoire",
  source = make_source("INSEE RP", "MIGCOM"),
  columns = list(
    type_terr = colDef(name = "Type", minWidth = 150, style = list(fontWeight = "500")),
    n_comm = col_num("Communes"),
    pop_pct = col_num("% pop", digits = 1),
    pop_tcam = col_variation(tbl_typo, "pop_tcam", label = "TCAM pop", unit = "%/an"),
    sma = col_variation(tbl_typo, "sma", label = "SMA", unit = "%/an"),
    sn = col_variation(tbl_typo, "sn", label = "SN", unit = "%/an"),
    tr = col_num("TR", digits = 1),
    iv = col_num("IV", digits = 0),
    tmi_cadre = col_variation(tbl_typo, "tmi_cadre", label = "TMI Cadres", unit = "pts", digits = 1),
    tmi_65p = col_variation(tbl_typo, "tmi_65p", label = "TMI 65+", unit = "pts", digits = 1)
  )
)
```

Le tableau confirme un gradient net. Les pôles concentrent la population et le solde naturel positif (+0,39 %/an) mais perdent en migration nette (SMA = −0,10 %/an) — seul type de territoire à afficher un SMA négatif. Les couronnes urbaines captent les flux les plus importants (SMA = +0,57 %/an) tout en conservant un SN positif : elles constituent le moteur de la périurbanisation. Le rural autonome cumule les contrastes : SMA le deuxième plus élevé (+0,50 %/an) mais SN le plus déficitaire (−0,54 %/an) et indice de vieillissement à 159. Ces communes attirent massivement des cadres (TMI = +2,81 pts) et des retraités (TMI 65+ = +0,71 pts) sans pour autant régénérer leur dynamique naturelle.

Ce profil statique masque cependant les évolutions récentes. Le gradient a-t-il bougé entre les deux périodes intercensitaires, et tous les types de territoire suivent-ils la même trajectoire ?


### Le rural autonome accélère, le rural périurbain décroche

[**`r n_types_accel` types de territoire sur 4 voient leur SMA s'améliorer entre T1 et T2.** `r type_max_accel` enregistre la plus forte accélération (`r paste0(ifelse(delta_max_accel > 0, "+", ""), fmt_fr(delta_max_accel, 2))` pts), tandis que `r type_max_decel` affiche le repli le plus marqué (`r fmt_fr(delta_max_decel, 2)` pts).]{.insight}
::: {.panel-tabset}

#### SMA (attractivité migratoire)

```{r}
#| label: plt-arrow-sma-typo
#| fig-cap: "Arrow chart — Évolution du SMA par type de territoire entre T1 et T2"
#| fig-height: 4.5

ggplot(agg_biperiode, aes(y = reorder(type_terr, sma_t2))) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "#666", linewidth = 0.5) +
  geom_vline(xintercept = fr_sma_t1, linetype = "dotted", color = col_cyan, alpha = 0.6) +
  geom_vline(xintercept = fr_sma_t2, linetype = "dotted", color = col_spacegray, alpha = 0.6) +
  geom_segment(
    aes(x = sma_t1, xend = sma_t2, yend = reorder(type_terr, sma_t2),
        color = sma_accel),
    linewidth = 2,
    arrow = arrow(length = unit(0.2, "cm"), type = "closed")
  ) +
  geom_point(aes(x = sma_t1), shape = 21, fill = "white", size = 4, stroke = 1.2, color = "#888") +
  geom_text(
    aes(x = pmax(sma_t1, sma_t2) + 0.03,
        label = paste0(ifelse(sma_delta > 0, "+", ""), fmt_fr(sma_delta, 2))),
    hjust = 0, size = 3.3, fontface = "bold", color = "#444"
  ) +
  scale_color_manual(values = c("TRUE" = col_green, "FALSE" = col_spacegray), guide = "none") +
  scale_x_continuous(expand = expansion(mult = c(0.05, 0.18))) +
  labs(
    title = "Les couronnes et le rural accélèrent, les pôles freinent",
    subtitle = "SMA TCAM (%/an) — cercle vide = T1 (11-16), pointe flèche = T2 (16-22)",
    x = "SMA TCAM (%/an)", y = NULL,
    caption = make_source("INSEE RP")
  ) +
  annotate("text", x = fr_sma_t1 + 0.005, y = 4.4, label = paste0("Fr. T1 ", fmt_fr(fr_sma_t1, 2)),
           size = 2.5, color = col_cyan, hjust = 0) +
  annotate("text", x = fr_sma_t2 + 0.005, y = 4.4, label = paste0("Fr. T2 ", fmt_fr(fr_sma_t2, 2)),
           size = 2.5, color = col_spacegray, hjust = 0)
```

#### Population (TCAM)

```{r}
#| label: plt-arrow-pop-typo
#| fig-cap: "Arrow chart — Évolution du TCAM population par type de territoire"
#| fig-height: 4.5

ggplot(agg_biperiode, aes(y = reorder(type_terr, pop_tcam_t2))) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "#666", linewidth = 0.5) +
  geom_vline(xintercept = fr_pop_t1, linetype = "dotted", color = col_cyan, alpha = 0.6) +
  geom_vline(xintercept = fr_pop_t2, linetype = "dotted", color = col_spacegray, alpha = 0.6) +
  geom_segment(
    aes(x = pop_tcam_t1, xend = pop_tcam_t2, yend = reorder(type_terr, pop_tcam_t2),
        color = pop_delta > 0),
    linewidth = 2,
    arrow = arrow(length = unit(0.2, "cm"), type = "closed")
  ) +
  geom_point(aes(x = pop_tcam_t1), shape = 21, fill = "white", size = 4, stroke = 1.2, color = "#888") +
  geom_text(
    aes(x = pmax(pop_tcam_t1, pop_tcam_t2) + 0.02,
        label = paste0(ifelse(pop_delta > 0, "+", ""), fmt_fr(pop_delta, 2))),
    hjust = 0, size = 3.3, fontface = "bold", color = "#444"
  ) +
  scale_color_manual(values = c("TRUE" = col_green, "FALSE" = col_spacegray), guide = "none") +
  scale_x_continuous(expand = expansion(mult = c(0.05, 0.18))) +
  labs(
    title = "Tous les types de territoire ralentissent en population",
    subtitle = "TCAM population (%/an) — cercle vide = T1 (11-16), pointe flèche = T2 (16-22)",
    x = "TCAM population (%/an)", y = NULL,
    caption = make_source("INSEE RP")
  ) +
  annotate("text", x = fr_pop_t1 + 0.005, y = 4.4, label = paste0("Fr. T1 ", fmt_fr(fr_pop_t1, 2)),
           size = 2.5, color = col_cyan, hjust = 0) +
  annotate("text", x = fr_pop_t2 + 0.005, y = 4.4, label = paste0("Fr. T2 ", fmt_fr(fr_pop_t2, 2)),
           size = 2.5, color = col_spacegray, hjust = 0)
```

#### Solde naturel

```{r}
#| label: plt-arrow-sn-typo
#| fig-cap: "Arrow chart — Évolution du SN par type de territoire"
#| fig-height: 4.5

ggplot(agg_biperiode, aes(y = reorder(type_terr, sn_t2))) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "#666", linewidth = 0.5) +
  geom_segment(
    aes(x = sn_t1, xend = sn_t2, yend = reorder(type_terr, sn_t2),
        color = sn_delta > 0),
    linewidth = 2,
    arrow = arrow(length = unit(0.2, "cm"), type = "closed")
  ) +
  geom_point(aes(x = sn_t1), shape = 21, fill = "white", size = 4, stroke = 1.2, color = "#888") +
  geom_text(
    aes(x = pmax(sn_t1, sn_t2) + 0.02,
        label = paste0(ifelse(sn_delta > 0, "+", ""), fmt_fr(sn_delta, 2))),
    hjust = 0, size = 3.3, fontface = "bold", color = "#444"
  ) +
  scale_color_manual(values = c("TRUE" = col_green, "FALSE" = col_spacegray), guide = "none") +
  scale_x_continuous(expand = expansion(mult = c(0.05, 0.18))) +
  labs(
    title = "Le solde naturel se dégrade dans tous les types de territoire",
    subtitle = "SN TCAM (%/an) — cercle vide = T1 (11-16), pointe flèche = T2 (16-22)",
    x = "SN TCAM (%/an)", y = NULL,
    caption = make_source("INSEE RP")
  )
```

:::

```{r}
#| label: tbl-biperiode-typo
#| tbl-cap: "Comparaison bi-période par type de territoire — SMA, TCAM pop, SN"

tbl_bp <- agg_biperiode %>%
  mutate(pop_pct = pop / sum(pop) * 100) %>%
  select(type_terr, n_comm, pop_pct, sma_t1, sma_t2, sma_delta, pop_tcam_t1, pop_tcam_t2, pop_delta)

rt_styled(
  tbl_bp,
  title = "Qui accélère, qui freine ? Comparaison T1 (11-16) vs T2 (16-22)",
  source = make_source("INSEE RP"),
  columns = list(
    type_terr = colDef(name = "Type", minWidth = 150, style = list(fontWeight = "500")),
    n_comm = col_num("Communes"),
    pop_pct = col_num("% pop", digits = 1),
    sma_t1 = col_variation(tbl_bp, "sma_t1", label = "SMA T1", unit = "%/an"),
    sma_t2 = col_variation(tbl_bp, "sma_t2", label = "SMA T2", unit = "%/an"),
    sma_delta = col_variation(tbl_bp, "sma_delta", label = "Δ SMA", unit = "pts"),
    pop_tcam_t1 = col_variation(tbl_bp, "pop_tcam_t1", label = "Pop T1", unit = "%/an"),
    pop_tcam_t2 = col_variation(tbl_bp, "pop_tcam_t2", label = "Pop T2", unit = "%/an"),
    pop_delta = col_variation(tbl_bp, "pop_delta", label = "Δ Pop", unit = "pts")
  )
)
```

::: {.note-lecture}
Les moyennes sont pondérées par la population communale 2022. Le delta (Δ) mesure la variation en points de pourcentage entre T1 (2011-2016) et T2 (2016-2022). Un delta SMA positif signifie que l'attractivité migratoire s'améliore.
:::

Les arrow charts révèlent des trajectoires divergentes entre types de territoire. Le **rural autonome** enregistre l'accélération SMA la plus forte (+0,15 pts), passant de +0,35 à +0,50 %/an — il dépasse désormais les couronnes en rythme de gain. Paradoxe : cette attractivité croissante ne compense pas le déficit naturel, et la population de ces communes bascule en déclin (TCAM population de +0,06 à −0,04 %/an). Les **pôles** améliorent aussi leur SMA (+0,13 pts) mais restent le seul type en perte migratoire nette (−0,10 %/an en T2) — l'hémorragie ralentit sans s'inverser.

À l'inverse, le **rural périurbain** est le seul type à voir son attractivité migratoire se dégrader (−0,12 pts). Son SMA passe de +0,41 à +0,29 %/an, et sa croissance démographique chute de moitié (+0,66 à +0,33 %/an). Ce recul contraste avec la dynamique des couronnes urbaines, qui restent le moteur de la périurbanisation (SMA le plus élevé à +0,57 %/an, +0,08 pts d'accélération). Le **solde naturel** se dégrade partout, mais l'ampleur varie : les pôles perdent peu (−0,04 pts) tandis que le rural périurbain décroche nettement (−0,33 pts en TCAM population).

La lecture croisée des trois onglets fait apparaître un mouvement structurel : la déconcentration résidentielle saute un échelon. Le rural autonome — longtemps éloigné des flux périurbains — capte désormais davantage que le rural de proximité. Ce basculement est cohérent avec les observations POPSU/PUCA sur la déconcentration post-2020, mais il s'agit d'un signal à confirmer sur la prochaine période intercensitaire.

Ces profils moyens sont-ils statistiquement robustes, ou la variabilité intra-type masque-t-elle les écarts entre types ?


### Le type de territoire explique surtout le vieillissement et la natalité

[**Le type de territoire explique 10 % de la variabilité du vieillissement** entre communes, mais seulement 1,3 % de celle du SMA. Les flux migratoires dépendent davantage de facteurs locaux que du gradient urbain-rural.]{.insight}
```{r}
#| label: kw-tests
#| include: false

# &s &KW_TESTS - Kruskal-Wallis sur 4 indicateurs

kw_vars <- c("dm_sma_vtcam_1622", "dmf_tr_pct_22", "dm_sn_vtcam_1622", "dmv_iv_ind_22")
kw_labels <- c("SMA TCAM", "Taux de rotation", "SN TCAM", "Indice de vieillissement")

kw_results <- lapply(seq_along(kw_vars), function(i) {
  v <- kw_vars[i]
  df_kw <- comm_typo %>% filter(!is.na(.data[[v]]), !is.na(type_terr))
  test <- kruskal.test(reformulate("type_terr", v), data = df_kw)
  n_obs <- nrow(df_kw)
  eta2 <- as.numeric(test$statistic) / (n_obs - 1)
  tibble(
    indicateur = kw_labels[i],
    variable = v,
    chi2 = round(as.numeric(test$statistic), 1),
    p_value = test$p.value,
    p_label = ifelse(test$p.value < 0.001, "< 0.001",
              formatC(test$p.value, format = "f", digits = 4)),
    eta2 = round(eta2, 4),
    n = n_obs,
    significatif = ifelse(test$p.value < 0.05, "Oui", "Non")
  )
})

kw_df <- bind_rows(kw_results)

# &e
```

```{r}
#| label: tbl-kw-results
#| tbl-cap: "Tests de Kruskal-Wallis — Différences entre 4 types de territoire"

rt_styled(
  kw_df %>% select(indicateur, chi2, p_label, eta2, n, significatif),
  title = "Le type de territoire explique-t-il les dynamiques ?",
  source = make_source("INSEE RP", "MIGCOM"),
  columns = list(
    indicateur = colDef(name = "Indicateur", minWidth = 180, style = list(fontWeight = "500")),
    chi2 = col_num("Chi²", digits = 1),
    p_label = colDef(name = "p-value", minWidth = 90),
    eta2 = col_num("eta²", digits = 4),
    n = col_num("N"),
    significatif = colDef(name = "Significatif",
      style = function(value) {
        if (value == "Oui") list(color = col_green, fontWeight = "600")
        else list(color = col_red)
      })
  )
)
```

::: {.note-lecture}
Le test de Kruskal-Wallis est très significatif attendu avec N > 30 000 communes. L'enjeu est l'eta² (effet de taille) : un eta² > 0,01 indique un effet faible mais détectable, > 0,06 un effet modéré, > 0,14 un effet large. Interpréter la significativité statistique à la lumière de la taille de l'effet.
:::

Les quatre tests sont significatifs (p < 0,001) — attendu avec N > 30 000 communes. L'enjeu est la taille de l'effet. Le vieillissement et le solde naturel montrent des effets modérés : le type de territoire explique respectivement 10 % et 7,8 % de leur variabilité entre communes. En revanche, le SMA (1,3 %) et le taux de rotation (1,0 %) sont faiblement liés au type de territoire — la variabilité intra-type domine largement. Le gradient urbain-rural structure la démographie naturelle, mais il prédit mal les flux migratoires nets.

::: {.panel-tabset}

### SMA par type

```{r}
#| label: plt-boxplot-sma
#| fig-cap: "Distribution du SMA par type de territoire"
#| fig-height: 5

ggplot(comm_typo %>% filter(!is.na(dm_sma_vtcam_1622)),
       aes(x = type_terr, y = dm_sma_vtcam_1622, fill = type_terr)) +
  geom_boxplot(alpha = 0.7, outlier.size = 0.5, outlier.alpha = 0.3) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "#333") +
  scale_fill_manual(values = c(col_cyan, col_magenta, col_green, col_yellow), guide = "none") +
  coord_cartesian(ylim = quantile(comm_typo$dm_sma_vtcam_1622, c(0.01, 0.99), na.rm = TRUE)) +
  labs(x = NULL, y = "SMA TCAM (%/an)",
       caption = make_source("INSEE RP"))
```

### TR par type

```{r}
#| label: plt-boxplot-tr
#| fig-cap: "Distribution du taux de rotation par type"
#| fig-height: 5

ggplot(comm_typo %>% filter(!is.na(dmf_tr_pct_22)),
       aes(x = type_terr, y = dmf_tr_pct_22, fill = type_terr)) +
  geom_boxplot(alpha = 0.7, outlier.size = 0.5, outlier.alpha = 0.3) +
  scale_fill_manual(values = c(col_cyan, col_magenta, col_green, col_yellow), guide = "none") +
  coord_cartesian(ylim = quantile(comm_typo$dmf_tr_pct_22, c(0.01, 0.99), na.rm = TRUE)) +
  labs(
       x = NULL, y = "Taux de rotation (%)",
       caption = make_source("MIGCOM"))
```

### IV par type

```{r}
#| label: plt-boxplot-iv
#| fig-cap: "Distribution de l'indice de vieillissement par type"
#| fig-height: 5

ggplot(comm_typo %>% filter(!is.na(dmv_iv_ind_22)),
       aes(x = type_terr, y = dmv_iv_ind_22, fill = type_terr)) +
  geom_boxplot(alpha = 0.7, outlier.size = 0.5, outlier.alpha = 0.3) +
  scale_fill_manual(values = c(col_cyan, col_magenta, col_green, col_yellow), guide = "none") +
  coord_cartesian(ylim = quantile(comm_typo$dmv_iv_ind_22, c(0.01, 0.99), na.rm = TRUE)) +
  labs(x = NULL, y = "Indice de vieillissement",
       caption = make_source("INSEE RP"))
```

### Table 8 types

```{r}
#| label: tbl-profils-typo8
#| tbl-cap: "Profils démographiques détaillés — 8 sous-types TYPO8pers"

# Charger TYPO8pers
comm_typo8 <- comm %>%
  left_join(typo %>% mutate(CODGEO = as.character(CODGEO)) %>% select(CODGEO, TYPO8p),
            by = c("code" = "CODGEO")) %>%
  filter(!is.na(TYPO8p))

# Charger labels TYPO8pers
typo8_labels <- read.csv(
  file.path(PROJECT_ROOT, "data/external/geo-tdc/ptypo-dens-rural/tdc_geo_sat3col_TYPO8pers_2025.csv"),
  sep = ";", stringsAsFactors = FALSE, fileEncoding = "UTF-8-BOM"
)

comm_typo8 <- comm_typo8 %>%
  left_join(typo8_labels %>% select(code, lib), by = c("TYPO8p" = "code")) %>%
  rename(type_terr8 = lib) %>%
  filter(!is.na(type_terr8))

agg_typo8 <- agg_weighted_demog(comm_typo8, "type_terr8")

tbl_typo8 <- agg_typo8 %>%
  mutate(pop_pct = pop / sum(pop) * 100) %>%
  select(type_terr8, n_comm, pop_pct, pop_tcam, sma, sn, iv, tmi_cadre, tmi_65p) %>%
  arrange(desc(pop_pct))

rt_styled(
  tbl_typo8,
  title = "Profils détaillés — 8 sous-types de territoire",
  source = make_source("INSEE RP", "MIGCOM"),
  height = 400,
  columns = list(
    type_terr8 = colDef(name = "Sous-type", minWidth = 180, style = list(fontWeight = "500")),
    n_comm = col_num("Communes"),
    pop_pct = col_num("% pop", digits = 1),
    pop_tcam = col_variation(tbl_typo8, "pop_tcam", label = "TCAM pop", unit = "%/an"),
    sma = col_variation(tbl_typo8, "sma", label = "SMA", unit = "%/an"),
    sn = col_variation(tbl_typo8, "sn", label = "SN", unit = "%/an"),
    iv = col_num("IV", digits = 0),
    tmi_cadre = col_variation(tbl_typo8, "tmi_cadre", label = "TMI Cadres", unit = "pts", digits = 1),
    tmi_65p = col_variation(tbl_typo8, "tmi_65p", label = "TMI 65+", unit = "pts", digits = 1)
  )
)
```

:::

Les boxplots confirment l'interprétation des eta². Pour l'IV, les boîtes sont nettement séparées : la médiane du rural autonome (~160) ne chevauche pas l'intervalle interquartile des pôles (~85). Pour le SN, même séparation nette entre rural autonome (médiane négative) et pôles (médiane positive). En revanche, les distributions du SMA se chevauchent largement entre les 4 types — la dispersion intra-type est plus forte que l'écart entre types. Les pôles affichent la queue gauche la plus longue (communes-centres en forte perte migratoire), tandis que les couronnes ont la médiane la plus élevée.

::: {.callout-important}
## Bilan H3

**H3 confirmée avec nuance** : Le gradient urbain-rural structure fortement les dynamiques naturelles (SN, IV : eta² modéré à fort) mais faiblement les flux migratoires (SMA, TR : eta² < 0,015). Les pôles concentrent le SN+ et le brassage, les couronnes et le rural captent les flux nets. L'attractivité migratoire dépend davantage de facteurs locaux (emploi, cadre de vie, marché immobilier) que du type de territoire seul.
:::


## Synthèse

L'analyse croise trois niveaux géographiques — 287 zones d'emploi, 96 départements, ~34 000 communes — et cinq angles complémentaires : ralentissement macro, trajectoires migratoires, profils par catégorie, origines-destinations (à développer) et gradient urbain-rural. Le résultat convergent est clair : le ralentissement démographique est structurel et quasi-généralisé, mais sa décomposition en solde naturel et solde migratoire révèle des mécanismes différenciés selon l'échelon et le type de territoire.

::: {.callout-tip}
## 6 constats principaux

**1. Ralentissement généralisé** — `r pct_ralenti`% des ZE croissent moins vite entre T1 et T2. Le TCAM national passe de `r fmt_sign(fr_tcam_t1)`% à `r fmt_sign(fr_tcam_t2)`%.

**2. Compensation partielle** — `r n_compensation` ZE compensent un déficit naturel par la migration, mais seulement `r pct_compense`% des ZE vieillissantes (IV > 100) ont un SMA supérieur à leur déficit naturel.

**3. Trajectoires persistantes** — La corrélation SMA T1 × T2 est de `r fmt_fr(cor_sma_t1t2, 2)` : les territoires attractifs le restent, les répulsifs aussi. Mais `r n_renforce` ZE renforcent leur attractivité tandis que `r n_declin_acc` s'enfoncent dans le déclin.

**4. Attractivité cumulée, pas opposée** — Les départements attractifs captent à la fois cadres et retraités (r = `r fmt_fr(cor_cad_65_dep, 2)`). Le clivage n'est pas cadres vs. retraités mais territoires attractifs vs. répulsifs globalement.

**5. Brassage ≠ attractivité** — La corrélation TR × SMA est de `r fmt_fr(cor_tr_sma_dep, 2)` : un fort taux de rotation ne garantit pas un solde migratoire positif.

**6. Le gradient urbain-rural se recompose** — Les pôles ont un SN+ mais perdent en migration nette (SMA-). Entre T1 et T2, le rural autonome accélère le plus en attractivité (+0,15 pts) tandis que le rural périurbain décélère (−0,12 pts) — la déconcentration résidentielle saute un échelon. Mais le type de territoire seul explique peu la variabilité du SMA (eta² < 0,015).
:::

### Bilan hypothèses

| Hypothèse | Résultat | Parties |
|-----------|----------|---------|
| **H1** : Le SMA compense partiellement le déficit naturel dans les territoires vieillissants | **Partiellement confirmée** — SMA+ fréquent mais insuffisant dans `r pct_compense`% des cas. Le rural dispersé reste en déficit structurel | P1 |
| **H2** : Les profils migratoires sont différenciés (cadres→métropoles, retraités→littoral) | **Nuancée** — r(TMI cadres, TMI 65+) = `r fmt_fr(cor_cad_65_dep, 2)` (positif). Attractivité cumulée plutôt que divergente. Les cadres sont plus géographiquement sélectifs (amplitude ×2,4) | P3 |
| **H3** : Les flux suivent un gradient centre→périphérie | **Confirmée partiellement** — KW significatif sur SN et IV (eta² modéré), faible sur SMA. Matrice OD à construire pour tester les flux directionnels | P4 (à développer), P5 |

::: {.grey-section}
**Données manquantes** pour compléter l'analyse : matrice OD département × département (fichier détail MIGCOM), distribution des distances de migration, flux par type de territoire AAV (Sankey). Cf. `ptvol-volet/pdemo/qeval-method-pdemo_260219.md`.
:::


---


## Annexe — Données détaillées {.appendix}

:::: {.column-page}

```{r}
#| label: tbl-synthese-dep

synth <- dep %>%
  summarise(
    across(
      c(dm_pop_vtcam_1622, dm_sma_vtcam_1622, dm_sn_vtcam_1622,
        dmv_iv_ind_22, dmf_tr_pct_22, dmf_tmi_cscadre_22, dmf_tmi_a65p_22),
      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}")
  ) %>%
  pivot_longer(everything(), names_to = c("var", "stat"), names_sep = "__") %>%
  pivot_wider(names_from = stat, values_from = value) %>%
  mutate(
    label = case_when(
      var == "dm_pop_vtcam_1622" ~ "TCAM population (%/an)",
      var == "dm_sma_vtcam_1622" ~ "SMA TCAM (%/an)",
      var == "dm_sn_vtcam_1622" ~ "SN TCAM (%/an)",
      var == "dmv_iv_ind_22" ~ "Indice de vieillissement",
      var == "dmf_tr_pct_22" ~ "Taux de rotation (%)",
      var == "dmf_tmi_cscadre_22" ~ "TMI Cadres (pts)",
      var == "dmf_tmi_a65p_22" ~ "TMI 65+ (pts)"
    ),
    ecart = q90 - q10
  ) %>%
  select(label, med, q10, q90, ecart)

rt_styled(
  synth,
  title = "Tableau récapitulatif — distributions départementales",
  source = make_source("INSEE RP", "MIGCOM"),
  columns = list(
    label = colDef(name = "Indicateur", minWidth = 180, style = list(fontWeight = "500")),
    med = col_num("Médiane", digits = 2),
    q10 = col_num("P10", digits = 2),
    q90 = col_num("P90", digits = 2),
    ecart = colDef(
      header = "Écart P90-P10",
      cell = bar_simple(max(abs(synth$ecart), na.rm = TRUE), fill = col_cyan, bar_width = 60),
      minWidth = 150
    )
  )
)
```

::::


```{r}
#| label: log-save
#| include: false

# &s &LOG_RESULTATS - Logging enrichi inter-sessions

# Métriques manuelles clés — P1
log_result("pct_decline_t2", pct_decline_t2, interpret = glue("{pct_decline_t2}% ZE en déclin 16-22"))
log_result("pct_ralenti", pct_ralenti, interpret = glue("{pct_ralenti}% ZE ralentissent"))
log_result("n_compensation", n_compensation, interpret = glue("{n_compensation} ZE SN- SMA+ (compensation)"))
log_result("n_double_deficit", n_double_deficit, interpret = glue("{n_double_deficit} ZE double déficit"))
log_result("pct_compense_vieilles", pct_compense, interpret = glue("{pct_compense}% vieillissantes compensent"))

# Métriques trajectoires — P2
log_result("cor_sma_t1t2", cor_sma_t1t2, interpret = glue("r(SMA T1, SMA T2) ZE = {fmt_fr(cor_sma_t1t2, 2)} — persistance forte"))
log_result("n_renforce", n_renforce, interpret = glue("{n_renforce} ZE attractif renforcé ({pct_renforce}%)"))
log_result("n_declin_acc", n_declin_acc, interpret = glue("{n_declin_acc} ZE déclin accentué"))
log_result("n_retournements", n_retpos + n_retneg, interpret = glue("{n_retpos} retournements positifs + {n_retneg} négatifs = {n_retpos + n_retneg} total ({pct_retournement}%)"))
log_result("traj_counts", traj_counts %>% mutate(pct = round(100 * n / sum(n), 1)),
           interpret = paste(traj_counts$trajectoire, ":", traj_counts$n, collapse = " | "))

# Métriques bi-période par type_terr — P5
log_result("agg_biperiode", agg_biperiode %>% select(type_terr, sma_t1, sma_t2, sma_delta, pop_tcam_t1, pop_tcam_t2, pop_delta),
           interpret = paste(agg_biperiode$type_terr, ": SMA", fmt_fr(agg_biperiode$sma_t1, 2), "→", fmt_fr(agg_biperiode$sma_t2, 2),
                            "(Δ", fmt_fr(agg_biperiode$sma_delta, 2), ")", collapse = " | "))
log_result("type_max_accel_sma", type_max_accel, interpret = glue("{type_max_accel} accélère le plus en SMA (+{fmt_fr(delta_max_accel, 2)} pts)"))
log_result("type_max_decel_sma", type_max_decel, interpret = glue("{type_max_decel} décélère le plus en SMA ({fmt_fr(delta_max_decel, 2)} pts)"))

# Métriques DEP — P3
log_result("cor_tr_sma_dep", cor_tr_sma_dep, interpret = glue("r(TR,SMA) DEP = {fmt_fr(cor_tr_sma_dep, 2)}"))
log_result("cor_cadre_65p_dep", cor_cad_65_dep, interpret = glue("r(TMI cadres, TMI 65+) DEP = {fmt_fr(cor_cad_65_dep, 2)}"))
log_result("n_dep_attractifs", n_dep_attractifs, interpret = glue("{n_dep_attractifs}/{N_DEP} DEP SMA+"))
log_result("med_sma_dep", med_sma_dep, interpret = glue("Médiane SMA DEP = {fmt_sign(med_sma_dep, 2)}%"))

# Auto-profile DEP
log_auto_profile(dep,
  cols = c("dm_pop_vtcam_1622", "dm_sma_vtcam_1622", "dm_sn_vtcam_1622",
           "dmv_iv_ind_22", "dmf_tr_pct_22", "dmf_tmi_cscadre_22", "dmf_tmi_a65p_22"),
  label_col = "libelle", n_top = 5
)

# Descriptions cartes DEP
log_map_auto("carte_sma_dep", "dm_sma_vtcam_1622", dep, "libelle")
log_map_auto("carte_sn_dep", "dm_sn_vtcam_1622", dep, "libelle")
log_map_auto("carte_iv_dep", "dmv_iv_ind_22", dep, "libelle")
log_map_auto("carte_tmi_cadre_dep", "dmf_tmi_cscadre_22", dep, "libelle")
log_map_auto("carte_tmi_65p_dep", "dmf_tmi_a65p_22", dep, "libelle")

# Descriptions scatter — P1
log_scatter_auto("scatter_tcam_t1t2", ze,
  "dm_pop_vtcam_1116", "dm_pop_vtcam_1622", "libelle")
log_scatter_auto("scatter_sn_sma", ze,
  "dm_sn_vtcam_1622", "dm_sma_vtcam_1622", "libelle")

# Descriptions scatter — P2 trajectoires
log_scatter_auto("scatter_sma_t1t2_traj", ze,
  "dm_sma_vtcam_1116", "dm_sma_vtcam_1622", "libelle")

# Descriptions scatter — P3 profils DEP
log_scatter_auto("scatter_tr_sma_dep", dep,
  "dmf_tr_pct_22", "dm_sma_vtcam_1622", "libelle")
log_scatter_auto("scatter_tmi_cadre_65p_dep", dep,
  "dmf_tmi_cscadre_22", "dmf_tmi_a65p_22", "libelle")

# Auto-profile communes agrégées TYPO4
log_auto_profile(agg_typo4,
  cols = c("sma", "sn", "tr", "iv", "tmi_cadre", "tmi_65p"),
  label_col = "type_terr", n_top = 4
)

# KW results
log_result("kw_tests", kw_df %>% select(indicateur, chi2, eta2, significatif),
           interpret = paste(kw_df$indicateur, "eta²=", kw_df$eta2, collapse = " | "))

# &e

claude_log_save()
```
