---
title: "Dynamiques emploi salarié privé par zone d'emploi"
subtitle: "EDA Économie — Notebook exploratoire"
number-sections: false
format:
html:
theme: [cosmo, ../config/theme-html.scss]
toc: true
toc-location: left
toc-depth: 3
code-fold: true
code-summary: "Code"
code-tools: true
self-contained: true
fig-width: 10
fig-height: 6
execute:
warning: false
message: false
---
```{r}
#| label: setup
#| include: false
source("../_setup-common.R")
# &s &PACKAGES - Libs supplémentaires EDA éco
suppressPackageStartupMessages({
library(ineq) # Gini, Lorenz
library(dunn.test) # Dunn post-hoc
library(corrplot) # Heatmap corrélations
})
# &e
# &s &CLAUDE_LOG - Initialisation logging
claude_log_init("rpt-peco-edanbk-ecoze",
output_dir = file.path(PROJECT_ROOT, "reports/outputs"))
log_step("SETUP", n_ze = N_ZE, n_cols = ncol(ze))
# &e
# &s &VARS_DERIVEES - Variables dérivées notebook
# Série temporelle URSSAF
serie_raw <- read.csv(file.path(PROJECT_ROOT,
"data/processed/urssaf/urssaf_serie_indice100.csv"),
stringsAsFactors = FALSE, fileEncoding = "UTF-8-BOM")
serie_ze <- serie_raw %>% filter(echelon == "ze")
serie_fr <- serie_raw %>% filter(echelon == "france")
# Effectif emploi privé 2024 par ZE
eff_24 <- serie_ze %>%
filter(year == 2024) %>%
select(code, emppriv_vol_24 = eff_total)
ze <- ze %>% left_join(eff_24, by = "code")
# Choc COVID : glissement 2019→2020
eff_covid <- serie_ze %>%
filter(year %in% c(2019, 2020)) %>%
select(code, year, eff_total) %>%
pivot_wider(names_from = year, values_from = eff_total,
names_prefix = "eff_") %>%
mutate(choc_covid_pct = (eff_2020 - eff_2019) / eff_2019 * 100)
ze <- ze %>% left_join(eff_covid, by = "code")
# France référence
fr_eff_24 <- serie_fr %>% filter(year == 2024) %>% pull(eff_total)
fr_eff_19 <- serie_fr %>% filter(year == 2019) %>% pull(eff_total)
fr_eff_20 <- serie_fr %>% filter(year == 2020) %>% pull(eff_total)
fr_choc <- (fr_eff_20 - fr_eff_19) / fr_eff_19 * 100
fr_tcam_emp <- as.numeric(FR$eco_emppriv_vtcam_2224)
# Taille ZE (4 classes)
ze <- ze %>%
mutate(
taille_ze = case_when(
grepl("Paris", libelle) ~ "IDF (Paris)",
P22_POP >= 200000 ~ "Grande (>200k)",
P22_POP >= 50000 ~ "Moyenne (50-200k)",
TRUE ~ "Petite (<50k)"
),
taille_ze = factor(taille_ze,
levels = c("IDF (Paris)", "Grande (>200k)", "Moyenne (50-200k)", "Petite (<50k)"))
)
# Accélération pré/post
ze <- ze %>%
mutate(tcam_diff = eco_emppriv_vtcam_2224 - eco_emppriv_vtcam_1117)
log_step("DATA_ENRICHMENT",
n_ze_with_emp24 = sum(!is.na(ze$emppriv_vol_24)),
n_ze_with_covid = sum(!is.na(ze$choc_covid_pct)),
fr_emp_24 = fr_eff_24,
fr_choc_pct = round(fr_choc, 2))
# &e
```
# Synthèse exécutive
```{r}
#| label: kpi-data
#| include: false
# KPIs calculés ici pour la synthèse en haut + réutilisation
ze_sorted <- ze %>% arrange(desc(emppriv_vol_24))
total_emp <- sum(ze$emppriv_vol_24, na.rm = TRUE)
top10_emp <- sum(ze_sorted$emppriv_vol_24[1:10], na.rm = TRUE)
cr10 <- top10_emp / total_emp * 100
n10 <- ceiling(N_ZE * 0.10)
n40 <- ceiling(N_ZE * 0.40)
palma_employment <- sum(ze_sorted$emppriv_vol_24[1:n10], na.rm = TRUE) /
sum(tail(ze_sorted$emppriv_vol_24, n40), na.rm = TRUE)
gini_emp <- ineq(ze$emppriv_vol_24[!is.na(ze$emppriv_vol_24)], type = "Gini")
n_accel <- sum(ze$tcam_diff > 0, na.rm = TRUE)
pct_accel <- round(100 * n_accel / sum(!is.na(ze$tcam_diff)), 0)
cor_idx_tcam <- cor(ze$idxeco_soc_ind_1622, ze$eco_emppriv_vtcam_2224, use = "complete.obs")
cor_cross <- cor(ze$idxresid_dyn_ind_1623, ze$idxeco_soc_ind_1622, use = "complete.obs")
log_result("cr10", cr10, interpret = glue("CR10 = {fmt_pct(cr10, 1)}"))
log_result("palma", palma_employment, interpret = glue("Palma = {fmt_fr(palma_employment, 1)}"))
log_result("gini_emp", gini_emp, interpret = glue("Gini emploi = {fmt_fr(gini_emp, 3)}"))
log_result("pct_accel", pct_accel, interpret = glue("{pct_accel}% ZE accélèrent post-COVID"))
log_result("cor_cross_resid_eco", cor_cross,
interpret = glue("r resid x eco = {fmt_fr(cor_cross, 2)}"))
```
::: {.callout-note}
## 4 résultats clés — Volet économie
**1. Concentration extrême** — CR10 = `r fmt_pct(cr10, 1)`, Gini = `r fmt_fr(gini_emp, 3)`, Palma = `r fmt_fr(palma_employment, 1)`. L'emploi privé est bien plus concentré que la population.
**2. Trajectoires divergentes** — L'indice 100 base 2014 révèle un écart croissant entre ZE diversifiées (qui accélèrent) et ZE industrielles/agricoles (qui stagnent ou déclinent).
**3. COVID : choc universel, reprise sélective** — Le glissement 2019→2020 a frappé toutes les tailles de ZE (`r fmt_sign(fr_choc, 1)`% France). Mais `r pct_accel`% des ZE accélèrent post-COVID — les tertiaires et couronnes métropolitaines en tête.
**4. Résidentiel ≠ économique** — La corrélation idxresid × idxeco n'atteint que `r fmt_fr(cor_cross, 2)` : des territoires attractifs résidentiellement stagnent économiquement, et inversement.
:::
::: {.grey-section}
**Données** : URSSAF Séquoia (effectifs salariés privés, 2014-2024) · FLORES 2023 (Krugman, Gini) · INSEE RP 2022 (structures sectorielles, taux d'emploi, CSP)
**Périmètre** : `r N_ZE` zones d'emploi métropolitaines (hors DOM) · `r fmt_fr(total_emp / 1e6, 1)` M salariés privés (2024)
**Hypothèses** : **H1** Concentration croissante · **H2** Effet métropole au-delà de la taille · **H3** COVID redistribue vers moyennes/littorales
:::
---
# QE2.1 — Photographie concentration emploi {#qe21}
[**Les 10 premières ZE concentrent `r fmt_pct(cr10, 1)` de l'emploi salarié privé — une distribution plus inégale que celle de la population, typique des rendements d'agglomération.**]{.insight}
La concentration de l'emploi est structurellement élevée et reflète les économies d'échelle urbaines : le ratio de Palma atteint `r fmt_fr(palma_employment, 1)` (les 10% les plus grosses ZE emploient `r fmt_fr(palma_employment, 1)` fois plus que les 40% les plus petites). Le Gini de `r fmt_fr(gini_emp, 3)` confirme une distribution très asymétrique.
:::: {.column-page}
::: {layout-ncol=2}
```{r}
#| label: fig-lorenz
#| fig-height: 6.5
emp_sorted <- sort(ze$emppriv_vol_24[!is.na(ze$emppriv_vol_24)])
n <- length(emp_sorted)
cum_share <- cumsum(emp_sorted) / sum(emp_sorted)
pop_share <- seq_len(n) / n
lorenz_df <- data.frame(pop = c(0, pop_share), emp = c(0, cum_share))
p80_idx <- which.min(abs(pop_share - 0.80))
emp_at_80 <- cum_share[p80_idx] * 100
ggplot(lorenz_df, aes(x = pop, y = emp)) +
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 ZE = {fmt_pct(emp_at_80, 0)} emploi"),
size = 3.5, color = col_orange, fontface = "bold") +
annotate("text", x = 0.35, y = 0.85,
label = glue("Gini = {fmt_fr(gini_emp, 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 = "La concentration de l'emploi privé est très marquée",
subtitle = "Courbe de Lorenz — ZE triées par emploi croissant",
x = "Part cumulée des ZE", y = "Part cumulée de l'emploi privé",
caption = make_source("URSSAF Séquoia 2024"))
```
```{r}
#| label: fig-boxplot-emploi-typo
#| fig-height: 6.5
ggplot(ze %>% filter(!is.na(typo_insee_lib)),
aes(x = emppriv_vol_24,
y = reorder(typo_insee_lib, emppriv_vol_24, FUN = median))) +
geom_boxplot(fill = col_cyan, alpha = 0.5, outlier.size = 1.5) +
scale_x_log10(labels = label_comma()) +
labs(title = "Les ZE métropolitaines dominent en volume",
subtitle = "Emploi salarié privé 2024 — échelle logarithmique",
x = "Emploi salarié privé (échelle log)", y = NULL,
caption = make_source("URSSAF Séquoia 2024"))
```
:::
::::
::: {.note-lecture}
Gauche : courbe de Lorenz — plus la courbe s'éloigne de la diagonale, plus la concentration est forte. Droite : boxplots en échelle log — les ZE à fonctions métropolitaines (17 ZE) concentrent les plus gros volumes.
:::
### Spécialisation sectorielle et taille
:::: {.column-page}
::: {layout-ncol=2}
```{r}
#| label: fig-krugman-taille
#| fig-height: 7
scatter_quadrants(
ze %>% filter(!is.na(eco_krugman_a21_23)),
x = "emppriv_vol_24", y = "eco_krugman_a21_23",
title = "Les petites ZE sont les plus spécialisées",
subtitle = "Krugman A21 vs emploi privé — taille = population",
xlab = "Emploi salarié privé 2024", ylab = "Krugman A21",
hline = median(ze$eco_krugman_a21_23, na.rm = TRUE),
vline = median(ze$emppriv_vol_24, na.rm = TRUE),
quad_labels = c("Grand diversifié", "Petit diversifié",
"Petit spécialisé", "Grand spécialisé")
) +
scale_x_log10(labels = label_comma())
```
```{r}
#| label: fig-bar-emploi
#| fig-height: 7
plot_bar_ranked(
ze, var = "emppriv_vol_24", label_col = "libelle",
n_show = 15,
title = glue("Top/Bottom 15 — Emploi privé 2024"),
subtitle = glue("Total : {fmt_k(total_emp)} salariés privés"),
xlab = "Emploi salarié privé",
caption = make_source("URSSAF Séquoia 2024")
)
```
:::
::::
::: {.note-lecture}
Gauche : le Krugman mesure la dissimilarité sectorielle (0 = identique à la France, 2 = totalement différent). Les grandes ZE diversifiées sont en bas-droite, les petites ZE industrielles/agricoles en haut-gauche. Droite : top/bottom 15 en volume.
:::
### Concentration sectorielle par typologie
```{r}
#| label: tbl-gini-secteurs
gini_typo <- ze %>%
filter(!is.na(typo_insee_lib)) %>%
group_by(typo_insee_lib) %>%
summarise(
n = n(),
krugman_a21 = median(eco_krugman_a21_23, na.rm = TRUE),
gini_a21 = median(eco_gini_a21_23, na.rm = TRUE),
emp_med = median(emppriv_vol_24, na.rm = TRUE),
tcam_2224 = median(eco_emppriv_vtcam_2224, na.rm = TRUE),
pct_cadres = median(dsp_csp_cadres_pct_22, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(desc(emp_med))
rt_styled(
gini_typo,
columns = list(
typo_insee_lib = colDef(name = "Typologie INSEE", minWidth = 220),
n = col_num("Nb ZE"),
krugman_a21 = col_num("Krugman A21", digits = 3),
gini_a21 = col_num("Gini A21", digits = 3),
emp_med = col_pop(gini_typo, "emp_med", label = "Emploi méd."),
tcam_2224 = col_variation(gini_typo, "tcam_2224", label = "TCAM 22-24", unit = "%/an"),
pct_cadres = col_num("% cadres", unit = "%", digits = 1)
)
)
```
```{r}
#| label: log-qe21
#| include: false
log_step("QE2.1_CONCENTRATION", cr10 = round(cr10, 1), gini = round(gini_emp, 3),
palma = round(palma_employment, 1))
log_distribution("emppriv_vol_24", ze$emppriv_vol_24, label = "Emploi privé 2024")
log_ranking("top5_emploi_ze", ze, "libelle", "emppriv_vol_24", n = 5)
log_ranking("bot5_emploi_ze", ze, "libelle", "emppriv_vol_24", n = 5, ascending = TRUE)
```
---
# QE2.2 — Trajectoires emploi 2011-2024 {#qe22}
### Indice 100 base 2014 par typologie
```{r}
#| label: fig-indice100
#| fig-height: 7.5
# Indice base 2014 propre
typo_lookup <- ze %>% select(code, typo_insee_lib, taille_ze)
base2014 <- serie_ze %>% filter(year == 2014) %>% select(code, base_eff = eff_total)
serie_idx <- serie_ze %>%
left_join(base2014, by = "code") %>%
mutate(indice_100 = eff_total / base_eff * 100) %>%
left_join(typo_lookup, by = "code") %>%
filter(!is.na(typo_insee_lib))
serie_summary <- serie_idx %>%
group_by(typo_insee_lib, year) %>%
summarise(
med = median(indice_100, na.rm = TRUE),
q25 = quantile(indice_100, 0.25, na.rm = TRUE),
q75 = quantile(indice_100, 0.75, na.rm = TRUE),
.groups = "drop")
serie_fr_idx <- serie_fr %>%
mutate(indice_100 = eff_total / eff_total[year == 2014] * 100)
ggplot(serie_summary, aes(x = year, y = med, color = typo_insee_lib)) +
geom_ribbon(aes(ymin = q25, ymax = q75, fill = typo_insee_lib),
alpha = 0.08, color = NA) +
geom_line(linewidth = 0.9) +
geom_line(data = serie_fr_idx, aes(x = year, y = indice_100),
inherit.aes = FALSE, linetype = "dashed", color = "#333", linewidth = 0.7) +
geom_vline(xintercept = 2020, linetype = "dotted", color = col_red, alpha = 0.6) +
annotate("text", x = 2020.3, y = 116, label = "COVID", color = col_red,
size = 3, fontface = "italic") +
annotate("text", x = 2023.5,
y = serie_fr_idx$indice_100[serie_fr_idx$year == 2023] + 0.8,
label = "France", size = 3, color = "#333", fontface = "italic") +
scale_x_continuous(breaks = 2014:2024) +
labs(title = "Les trajectoires divergent : les diversifiées accélèrent, les industrielles stagnent",
subtitle = "Indice 100 base 2014 — médiane par typologie INSEE, ruban = IQR",
x = NULL, y = "Indice (base 100 = 2014)",
color = "Typologie", fill = "Typologie",
caption = make_source("URSSAF Séquoia", glue("{N_ZE} ZE"))) +
theme(legend.position = "bottom", legend.text = element_text(size = 8))
```
::: {.note-lecture}
Chaque ligne = médiane des ZE d'une typologie. Le ruban = intervalle interquartile (IQR). Pointillé noir = France. La divergence s'accentue après 2020 : les ZE diversifiées et métropolitaines rattrapent plus vite que les industrielles.
:::
### Choc COVID et trajectoire pré/post
:::: {.column-page}
::: {layout-ncol=2}
```{r}
#| label: fig-choc-covid
#| fig-height: 6
ggplot(ze %>% filter(!is.na(choc_covid_pct)),
aes(x = taille_ze, y = choc_covid_pct, fill = taille_ze)) +
geom_boxplot(alpha = 0.6, outlier.size = 1.5) +
geom_hline(yintercept = fr_choc, linetype = "dashed", color = col_red) +
annotate("text", x = 0.6, y = fr_choc + 0.3,
label = glue("France : {fmt_sign(fr_choc, 1)}%"),
size = 3, color = col_red, hjust = 0) +
scale_fill_manual(values = c(col_cyan, col_green, col_orange, col_yellow)) +
labs(title = "Le choc COVID a frappé toutes les tailles de ZE",
subtitle = "Glissement effectifs privés 2019 → 2020",
x = NULL, y = "Variation emploi (%)",
caption = make_source("URSSAF Séquoia")) +
guides(fill = "none")
```
```{r}
#| label: fig-distrib-tcam
#| fig-height: 6
plot_distribution(ze, "eco_emppriv_vtcam_2224", bins = 35,
title = "TCAM emploi privé 2022-2024",
xlab = "TCAM emploi privé (%/an)") +
geom_vline(xintercept = fr_tcam_emp, linetype = "dashed",
color = col_orange, linewidth = 0.7) +
annotate("text", x = fr_tcam_emp + 0.12, y = Inf,
label = glue("France : {fmt_sign(fr_tcam_emp, 2)}%"),
vjust = 2, hjust = 0, size = 3, color = col_orange)
```
:::
::::
### Scatter pré × post COVID
```{r}
#| label: fig-scatter-pre-post
#| fig-cap: "TCAM pré-COVID vs post-reprise — Qui accélère, qui décroche ?"
#| fig-height: 8
labs_pre <- label_top_bottom(ze, "eco_emppriv_vtcam_2224", n = 12)
ggplot(ze, aes(x = eco_emppriv_vtcam_1117, y = eco_emppriv_vtcam_2224)) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = col_gray) +
geom_hline(yintercept = 0, color = "#ddd") +
geom_vline(xintercept = 0, color = "#ddd") +
geom_point(aes(size = emppriv_vol_24, color = choc_covid_pct), alpha = 0.6) +
geom_text_repel(
data = ze %>% filter(libelle %in% labs_pre),
aes(label = libelle), size = 2.8, max.overlaps = 20,
color = col_spacegray, segment.color = col_gray) +
scale_size_continuous(range = c(1, 10), guide = "none") +
scale_color_gradient2(low = col_red, mid = "white", high = col_green,
midpoint = 0, name = "Choc COVID\n(% 19→20)") +
labs(title = glue("Sous la bissectrice = décélération ({100 - pct_accel}% des ZE)"),
subtitle = "Taille = emploi 2024, couleur = ampleur du choc COVID",
x = "TCAM emploi privé 2011-2017 (%/an)",
y = "TCAM emploi privé 2022-2024 (%/an)",
caption = make_source("URSSAF Séquoia", glue("{N_ZE} ZE")))
```
::: {.note-lecture}
Points au-dessus de la bissectrice = accélération post-COVID. Points rouges = ZE très touchées en 2020. Le scatter montre que le choc COVID n'a pas systématiquement favorisé les petites ZE : les accélérations sont sélectives.
:::
**`r pct_accel`% des ZE accélèrent** (TCAM post > TCAM pré), mais l'accélération n'est pas aléatoire — elle favorise les ZE tertiaires, les couronnes métropolitaines et certaines littorales. Les ZE industrielles restent majoritairement sous la bissectrice.
### Top/Bottom 15 — TCAM post-reprise
:::: {.column-page}
```{r}
#| label: tbl-topbot-tcam
tb_emp <- table_top_bottom(ze, "eco_emppriv_vtcam_2224", n = 15,
extra_cols = c("eco_emppriv_vtcam_1117", "tcam_diff", "typo_insee_lib",
"emppriv_vol_24", "eco_krugman_a21_23"))
tb_emp_data <- bind_rows(
tb_emp$top %>% mutate(groupe = "Top 15 — Croissance rapide"),
tb_emp$bottom %>% mutate(groupe = "Bottom 15 — Déclin ou stagnation")
)
rt_styled(
tb_emp_data,
groupBy = "groupe", defaultExpanded = TRUE, height = 550,
columns = list(
rang = colDef(name = "#", width = 45),
libelle = colDef(name = "Zone d'emploi", minWidth = 160),
eco_emppriv_vtcam_2224 = col_variation(tb_emp_data, "eco_emppriv_vtcam_2224",
label = "TCAM 22-24", unit = "%/an"),
eco_emppriv_vtcam_1117 = col_variation(tb_emp_data, "eco_emppriv_vtcam_1117",
label = "TCAM 11-17", unit = "%/an"),
tcam_diff = col_variation(tb_emp_data, "tcam_diff",
label = "Accélération", unit = "pts"),
typo_insee_lib = colDef(name = "Typologie", minWidth = 140),
emppriv_vol_24 = col_pop(tb_emp_data, "emppriv_vol_24", label = "Emploi 24"),
eco_krugman_a21_23 = col_num("Krugman", digits = 3),
groupe = colDef(show = FALSE)
)
)
```
::::
```{r}
#| label: log-qe22
#| include: false
log_step("QE2.2_TRAJECTOIRES",
pct_accel = pct_accel,
fr_choc = round(fr_choc, 2),
fr_tcam_emp = fr_tcam_emp)
log_distribution("tcam_emppriv_2224", ze$eco_emppriv_vtcam_2224, label = "TCAM emp privé 22-24")
log_distribution("choc_covid", ze$choc_covid_pct, label = "Choc COVID 19→20")
log_ranking("top5_tcam_post", ze, "libelle", "eco_emppriv_vtcam_2224", n = 5)
log_ranking("bot5_tcam_post", ze, "libelle", "eco_emppriv_vtcam_2224", n = 5, ascending = TRUE)
```
---
# QE2.3 — Tests statistiques {#qe23}
### Kruskal-Wallis : TCAM par typologie
```{r}
#| label: kw-tcam-typo
ze_kw <- ze %>% filter(!is.na(typo_insee_lib), !is.na(eco_emppriv_vtcam_2224))
kw_result <- kruskal.test(eco_emppriv_vtcam_2224 ~ typo_insee_lib, data = ze_kw)
kw_chi2 <- round(kw_result$statistic, 2)
kw_p <- kw_result$p.value
kw_sig <- ifelse(kw_p < 0.001, "p < 0.001",
paste0("p = ", formatC(kw_p, format = "f", digits = 3)))
```
::: {.callout-tip}
## Kruskal-Wallis — TCAM emploi par typologie
**H₀** : Les TCAM emploi privé 2022-2024 sont identiques entre typologies INSEE.
**Résultat** : χ² = `r kw_chi2`, **`r kw_sig`** → les TCAM diffèrent significativement selon la typologie.
:::
:::: {.column-page}
::: {layout-ncol=2}
```{r}
#| label: fig-kw-boxplot
#| fig-height: 6
ggplot(ze_kw,
aes(x = eco_emppriv_vtcam_2224,
y = reorder(typo_insee_lib, eco_emppriv_vtcam_2224, FUN = median))) +
geom_boxplot(fill = col_cyan, alpha = 0.5, outlier.size = 1.5) +
geom_vline(xintercept = 0, linetype = "dashed", color = "#333") +
geom_vline(xintercept = fr_tcam_emp, linetype = "dotted", color = col_orange) +
labs(title = glue("KW : {kw_sig}"),
subtitle = "TCAM emploi privé 2022-2024 par typologie",
x = "TCAM (%/an)", y = NULL,
caption = "Pointillé orange = France")
```
```{r}
#| label: fig-kw-boxplot-taille
#| fig-height: 6
ggplot(ze %>% filter(!is.na(taille_ze), !is.na(eco_emppriv_vtcam_2224)),
aes(x = eco_emppriv_vtcam_2224, y = taille_ze, fill = taille_ze)) +
geom_boxplot(alpha = 0.5, outlier.size = 1.5) +
geom_vline(xintercept = 0, linetype = "dashed", color = "#333") +
geom_vline(xintercept = fr_tcam_emp, linetype = "dotted", color = col_orange) +
scale_fill_manual(values = c(col_cyan, col_green, col_orange, col_yellow)) +
labs(title = "Par taille de ZE",
subtitle = "TCAM emploi privé 2022-2024",
x = "TCAM (%/an)", y = NULL,
caption = "Pointillé orange = France") +
guides(fill = "none")
```
:::
::::
### Dunn post-hoc
```{r}
#| label: dunn-test
#| results: asis
if (kw_p < 0.05) {
dunn_res <- dunn.test(ze_kw$eco_emppriv_vtcam_2224,
ze_kw$typo_insee_lib,
method = "bonferroni", list = FALSE, table = FALSE)
dunn_df <- data.frame(
comparaison = dunn_res$comparisons,
z = round(dunn_res$Z, 2),
p_ajuste = round(dunn_res$P.adjusted, 4),
significatif = ifelse(dunn_res$P.adjusted < 0.05, "Oui", "Non")
) %>%
filter(significatif == "Oui") %>%
arrange(p_ajuste)
n_sig <- nrow(dunn_df)
n_total <- length(dunn_res$P.adjusted)
}
```
```{r}
#| label: tbl-dunn
if (exists("dunn_df") && nrow(dunn_df) > 0) {
rt_styled(
dunn_df,
columns = list(
comparaison = colDef(name = "Comparaison", minWidth = 300),
z = col_num("Z", digits = 2),
p_ajuste = col_num("p ajusté", digits = 4),
significatif = colDef(name = "Sign.", width = 60)
)
)
}
```
`r if (exists("n_sig")) glue("**{n_sig} paire(s) significative(s)** sur {n_total} comparaisons (Bonferroni α = 0.05).") else "*KW non significatif — Dunn non exécuté.*"`
### KW sur l'accélération pré/post
```{r}
#| label: kw-accel
ze_kw2 <- ze %>% filter(!is.na(typo_insee_lib), !is.na(tcam_diff))
kw2 <- kruskal.test(tcam_diff ~ typo_insee_lib, data = ze_kw2)
kw2_p <- ifelse(kw2$p.value < 0.001, "p < 0.001",
paste0("p = ", formatC(kw2$p.value, format = "f", digits = 3)))
```
::: {.callout-tip}
## KW — Accélération emploi (TCAM post − TCAM pré)
χ² = `r round(kw2$statistic, 2)`, **`r kw2_p`** — l'accélération/décélération `r ifelse(kw2$p.value < 0.05, "diffère", "ne diffère pas")` significativement selon la typologie.
:::
### Corrélations Spearman
```{r}
#| label: fig-corr-heatmap
#| fig-cap: "Matrice de corrélations Spearman — 10 variables structurelles et dynamiques"
#| fig-width: 11
#| fig-height: 11
corr_vars <- c(
"eco_krugman_a21_23", "eco_gini_a21_23",
"eco_emppriv_vtcam_1117", "eco_emppriv_vtcam_2224", "tcam_diff",
"eco_sectindus_pct_22", "eco_sectservi_pct_22",
"dsp_csp_cadres_pct_22", "eco_txemp_1564_22",
"idxeco_soc_ind_1622"
)
corr_labels <- c(
"Krugman A21", "Gini A21",
"TCAM emp 11-17", "TCAM emp 22-24", "Accélération",
"% industrie", "% services",
"% cadres", "Tx emploi 15-64",
"Idx éco composite"
)
corr_data <- ze[, corr_vars] %>% na.omit()
corr_mat <- cor(corr_data, method = "spearman")
colnames(corr_mat) <- corr_labels
rownames(corr_mat) <- corr_labels
corrplot(corr_mat, method = "color", type = "lower",
tl.col = "#333", tl.srt = 45, tl.cex = 0.85,
addCoef.col = "#333", number.cex = 0.75,
col = colorRampPalette(c(col_red, "white", col_cyan))(200),
diag = FALSE,
title = "Corrélations Spearman — variables structurelles et dynamiques emploi",
mar = c(0, 0, 2, 0))
```
::: {.note-lecture}
Corrélations de Spearman (robustes aux outliers). Le TCAM post-COVID corrèle positivement avec le % services et le % cadres, négativement avec le Krugman et le % industrie. L'indice éco composite corrèle fortement avec les TCAM — validation interne.
:::
### Scatters exploratoires
::: {.panel-tabset}
#### % industrie × TCAM post
```{r}
#| label: fig-scatter-indus
#| fig-height: 6.5
cor_indus <- cor(ze$eco_sectindus_pct_22, ze$eco_emppriv_vtcam_2224,
use = "complete.obs", method = "spearman")
scatter_quadrants(
ze, x = "eco_sectindus_pct_22", y = "eco_emppriv_vtcam_2224",
title = glue("L'industrie freine la reprise (ρ = {fmt_fr(cor_indus, 2)})"),
subtitle = "Part industrie 2022 vs TCAM emploi privé 2022-2024",
xlab = "Part industrie (%)", ylab = "TCAM emploi privé (%/an)",
hline = 0, vline = median(ze$eco_sectindus_pct_22, na.rm = TRUE),
quad_labels = c("Industrie + Croissance", "Tertiaire + Croissance",
"Tertiaire + Déclin", "Industrie + Déclin")
)
```
#### Krugman × TCAM post
```{r}
#| label: fig-scatter-krugman-tcam
#| fig-height: 6.5
cor_krug <- cor(ze$eco_krugman_a21_23, ze$eco_emppriv_vtcam_2224,
use = "complete.obs", method = "spearman")
scatter_quadrants(
ze, x = "eco_krugman_a21_23", y = "eco_emppriv_vtcam_2224",
title = glue("La spécialisation freine la reprise (ρ = {fmt_fr(cor_krug, 2)})"),
subtitle = "Krugman A21 (2023) vs TCAM emploi privé 2022-2024",
xlab = "Krugman A21", ylab = "TCAM emploi privé (%/an)",
hline = 0, vline = median(ze$eco_krugman_a21_23, na.rm = TRUE),
quad_labels = c("Spécialisé + Croissance", "Diversifié + Croissance",
"Diversifié + Déclin", "Spécialisé + Déclin")
)
```
#### % cadres × TCAM post
```{r}
#| label: fig-scatter-cadres-tcam
#| fig-height: 6.5
cor_cadres <- cor(ze$dsp_csp_cadres_pct_22, ze$eco_emppriv_vtcam_2224,
use = "complete.obs", method = "spearman")
scatter_quadrants(
ze, x = "dsp_csp_cadres_pct_22", y = "eco_emppriv_vtcam_2224",
title = glue("Cadres et croissance emploi (ρ = {fmt_fr(cor_cadres, 2)})"),
subtitle = "Part cadres 2022 vs TCAM emploi privé 2022-2024",
xlab = "Part cadres (%)", ylab = "TCAM emploi privé (%/an)",
hline = 0, vline = median(ze$dsp_csp_cadres_pct_22, na.rm = TRUE),
quad_labels = c("Cadres + Croissance", "Ouvriers + Croissance",
"Ouvriers + Déclin", "Cadres + Déclin")
)
```
#### Taux emploi × TCAM post
```{r}
#| label: fig-scatter-txemp
#| fig-height: 6.5
cor_txemp <- cor(ze$eco_txemp_1564_22, ze$eco_emppriv_vtcam_2224,
use = "complete.obs", method = "spearman")
scatter_quadrants(
ze, x = "eco_txemp_1564_22", y = "eco_emppriv_vtcam_2224",
title = glue("Taux d'emploi et dynamique (ρ = {fmt_fr(cor_txemp, 2)})"),
subtitle = "Taux emploi 15-64 ans 2022 vs TCAM emploi privé 2022-2024",
xlab = "Taux emploi 15-64 (%)", ylab = "TCAM emploi privé (%/an)",
hline = 0, vline = median(ze$eco_txemp_1564_22, na.rm = TRUE),
quad_labels = c("Plein emploi + Croissance", "Chômage + Croissance",
"Chômage + Déclin", "Plein emploi + Déclin")
)
```
:::
```{r}
#| label: log-qe23
#| include: false
log_step("QE2.3_TESTS_STATS",
kw_chi2 = kw_chi2, kw_p = kw_p,
kw2_chi2 = round(kw2$statistic, 2), kw2_p = kw2$p.value)
log_result("cor_indus_tcam", cor_indus,
interpret = glue("ρ industrie×TCAM = {fmt_fr(cor_indus, 2)}"))
log_result("cor_cadres_tcam", cor_cadres,
interpret = glue("ρ cadres×TCAM = {fmt_fr(cor_cadres, 2)}"))
log_result("cor_krugman_tcam", cor_krug,
interpret = glue("ρ Krugman×TCAM = {fmt_fr(cor_krug, 2)}"))
```
---
# QE2.4 — Indice économique & croisement résidentiel {#qe24}
```{r}
#| label: idx-eco-data
cor_idx_krugman <- cor(ze$idxeco_soc_ind_1622, ze$eco_krugman_a21_23, use = "complete.obs")
```
[**Le croisement résidentiel × économique (r = `r fmt_fr(cor_cross, 2)`) révèle quatre profils : les « double moteur » (métropoles, littoral attractif), les « attractifs sans emploi » (tourisme, retraites), les « éco sans résidentiel » (industrie en reconversion) et les « double fragilité » (diagonale du vide).**]{.insight}
```{r}
#| label: fig-cross-resid-eco
#| fig-cap: "Croisement indices résidentiel × économique — 4 profils territoriaux"
#| fig-height: 8.5
labs_cross <- label_top_bottom(ze, "idxeco_soc_ind_1622", n = 10)
ggplot(ze, aes(x = idxresid_dyn_ind_1623, y = idxeco_soc_ind_1622)) +
geom_hline(yintercept = 50, linetype = "dashed", color = col_gray) +
geom_vline(xintercept = 50, linetype = "dashed", color = col_gray) +
geom_point(aes(size = P22_POP, color = typo_insee_lib), alpha = 0.6) +
geom_text_repel(
data = ze %>% filter(libelle %in% labs_cross),
aes(label = libelle), size = 2.8, max.overlaps = 20,
color = col_spacegray, segment.color = col_gray) +
scale_size_continuous(range = c(1, 8), guide = "none") +
scale_color_manual(values = pal_urbn_cat) +
annotate("text", x = 85, y = 88, label = "Double moteur",
color = col_green, fontface = "bold", size = 3.5) +
annotate("text", x = 15, y = 88, label = "Éco dynamique\nsans attractivité",
color = col_orange, fontface = "bold", size = 3.5) +
annotate("text", x = 15, y = 12, label = "Double fragilité",
color = col_red, fontface = "bold", size = 3.5) +
annotate("text", x = 85, y = 12, label = "Attractif résidentiel\nsans emploi",
color = col_cyan, fontface = "bold", size = 3.5) +
labs(title = glue("Résidentiel × Économique (r = {fmt_fr(cor_cross, 2)}) — deux dimensions partiellement indépendantes"),
subtitle = "Percentiles 0-100, ligne 50 = médiane France",
x = "Indice résidentiel (attractivité migratoire)",
y = "Indice économique (dynamisme emploi)",
color = "Typologie",
caption = make_source("URSSAF", "MIGCOM", glue("{N_ZE} ZE"))) +
theme(legend.position = "bottom", legend.text = element_text(size = 8))
```
::: {.note-lecture}
Le croisement résidentiel × économique montre que les deux dimensions ne sont pas redondantes (r = `r fmt_fr(cor_cross, 2)`). Les ZE touristiques/littorales sont souvent en haut-droite (attractives résidentiellement) mais pas toujours en haut sur l'axe économique. Les ZE industrielles en reconversion peuvent avoir un idxeco élevé mais un idxresid faible.
:::
### Stats descriptives par quadrant
```{r}
#| label: tbl-quadrants-eco-resid
ze <- ze %>%
mutate(quad_eco_resid = case_when(
idxresid_dyn_ind_1623 >= 50 & idxeco_soc_ind_1622 >= 50 ~ "Double moteur",
idxresid_dyn_ind_1623 < 50 & idxeco_soc_ind_1622 >= 50 ~ "Éco sans résidentiel",
idxresid_dyn_ind_1623 >= 50 & idxeco_soc_ind_1622 < 50 ~ "Résidentiel sans éco",
idxresid_dyn_ind_1623 < 50 & idxeco_soc_ind_1622 < 50 ~ "Double fragilité",
TRUE ~ NA_character_
))
quad_stats <- ze %>%
filter(!is.na(quad_eco_resid)) %>%
group_by(quad_eco_resid) %>%
summarise(
n = n(),
pop_med = median(P22_POP, na.rm = TRUE),
emp_med = median(emppriv_vol_24, na.rm = TRUE),
tcam_med = median(eco_emppriv_vtcam_2224, na.rm = TRUE),
pct_cadres = median(dsp_csp_cadres_pct_22, na.rm = TRUE),
krugman = median(eco_krugman_a21_23, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(desc(n))
rt_styled(
quad_stats,
columns = list(
quad_eco_resid = colDef(name = "Quadrant", minWidth = 180,
style = list(fontWeight = "500")),
n = col_num("Nb ZE"),
pop_med = col_pop(quad_stats, "pop_med", label = "Pop. méd."),
emp_med = col_pop(quad_stats, "emp_med", label = "Emploi méd."),
tcam_med = col_variation(quad_stats, "tcam_med", label = "TCAM 22-24", unit = "%/an"),
pct_cadres = col_num("% cadres", unit = "%", digits = 1),
krugman = col_num("Krugman", digits = 3)
)
)
```
```{r}
#| label: log-qe24
#| include: false
log_step("QE2.4_CROISEMENT",
cor_cross = round(cor_cross, 3),
cor_idx_tcam = round(cor_idx_tcam, 3),
cor_idx_krugman = round(cor_idx_krugman, 3))
log_result("quadrants_eco_resid",
as.list(setNames(quad_stats$n, quad_stats$quad_eco_resid)),
interpret = paste(quad_stats$quad_eco_resid, "=", quad_stats$n, collapse = " | "))
```
---
# Synthèse provisoire {#synthese}
::: {.callout-tip}
## 5 constats principaux
1. **Concentration extrême** : CR10 = `r fmt_pct(cr10, 1)`, Gini = `r fmt_fr(gini_emp, 3)`, Palma = `r fmt_fr(palma_employment, 1)`. L'emploi privé est bien plus concentré que la population — rendements d'agglomération.
2. **Trajectoires divergentes post-2014** : L'indice 100 révèle un écart croissant entre ZE diversifiées/métropolitaines et ZE industrielles/agricoles. La divergence s'accentue après 2020.
3. **Choc COVID universel, reprise sélective** : `r fmt_sign(fr_choc, 1)`% en 2020 pour toutes les tailles. Mais `r pct_accel`% des ZE accélèrent post-COVID — les tertiaires, couronnes métropolitaines et certaines littorales.
4. **Structure sectorielle = facteur explicatif clé** : % industrie et Krugman corrèlent négativement avec le TCAM post (ρ = `r fmt_fr(cor_indus, 2)` et `r fmt_fr(cor_krug, 2)`). Les cadres corrèlent positivement (ρ = `r fmt_fr(cor_cadres, 2)`).
5. **Résidentiel ≠ Économique** : r = `r fmt_fr(cor_cross, 2)` — les deux dimensions sont partiellement indépendantes, justifiant leur traitement séparé dans l'observatoire.
### Bilan hypothèses
| Hypothèse | Résultat |
|-----------|----------|
| **H1** Concentration croissante | **À nuancer** — CR10 élevé mais les grandes ZE n'accélèrent pas toutes plus vite |
| **H2** Effet métropole | **Partiellement confirmée** — KW significatif (`r kw_sig`), médié par cadres et tertiarisation |
| **H3** COVID redistribue | **Partiellement confirmée** — accélérations en ZE moyennes, mais pas de pattern systématique |
:::
::: {.callout-warning}
## Variables manquantes — phase 2
- **Salaire moyen** : masse salariale absente → ratio salaire/emploi indisponible
- **Part littorale** : dummy géographique à construire (codes ZE → proximité côte)
- **Theil décomposé** : inter/intra-typologies pour quantifier les inégalités territoriales
- **OLS multivarié** : TCAM ~ taille + Krugman + % cadres + dummy_metro + dummy_littoral
- **Croisement peco × pdemo** : scatter TCAM emploi × z-trajectoire résidentielle
:::
```{r}
#| label: log-save
#| include: false
log_result("synthese_h1", "a_nuancer",
interpret = "H1 concentration : CR10 élevé mais pas d'accélération systématique grandes ZE")
log_result("synthese_h2", "partiellement_confirmee",
interpret = glue("H2 effet métropole : KW {kw_sig}, médié par cadres+tertiarisation"))
log_result("synthese_h3", "partiellement_confirmee",
interpret = glue("H3 COVID redistribue : {pct_accel}% accélèrent, sélectif"))
claude_log_save()
```