Logistique — Fiche sectorielle

Cadrage national & focus Hauts-de-France · NA88 52 (entreposage)

Date de publication

Édition 12 juin 2026

Périmètre : Logistique (entreposage et services auxiliaires des transports) · cœur NA88 52 (Entreposage et services auxiliaires des transports) · comparable NA88 49 (Transports terrestres et transport par conduites) Focus : Hauts-de-France (02, 59, 60, 62, 80) Sources : URSSAF privé (volume), FLORES total (privé + public, 2017-2024), EEC (profil national 2022 + 2024), MOBZELT 2022 (territorial)

Synthèse

Voir le code
# Synthèse auto-suffisante : recalcule 4 chiffres-phares depuis `dat` (indépendant de srpt-01+).
# Préfixe sy_ pour éviter tout clash avec les vars des srpt suivants.
.us <- dat$urssaf |> filter(measure == "EFF_SAL", code_a88 %in% coeur) |>
  group_by(year) |> summarise(emp = sum(value, na.rm = TRUE), .groups = "drop")
sy_vol     <- .us$emp[.us$year == ann_ref]
sy_tcam    <- ((.us$emp[.us$year == p_evol[2]] / .us$emp[.us$year == p_evol[1]])^(1/(p_evol[2]-p_evol[1])) - 1) * 100

.tot <- dat$urssaf |> filter(measure == "EFF_SAL", year %in% c(p_evol[1], p_evol[2])) |>
  group_by(year) |> summarise(emp = sum(value, na.rm = TRUE), .groups = "drop")
sy_part    <- sy_vol / .tot$emp[.tot$year == ann_ref] * 100
sy_tcamtot <- ((.tot$emp[.tot$year == p_evol[2]] / .tot$emp[.tot$year == p_evol[1]])^(1/(p_evol[2]-p_evol[1])) - 1) * 100

# Profil EEC (fem + temps partiel) — dernier millésime ≤ ann_ref
.ec <- dat$eec |> filter(code_naf88 %in% coeur, annee == ann_eec)
sy_fem   <- weighted.mean(.ec$lab_fem_pct,  .ec$lab_eff_vol)
sy_tpart <- weighted.mean(.ec$lab_tpart_pct, .ec$lab_eff_vol)

sy_sect  <- fr_secteur_phrase(params$secteur_label, prep = "de")
.cap0    <- function(s) paste0(toupper(substr(s, 1, 1)), substring(s, 2))
# Phrase secteur réutilisable en prose (article + genre via jcn-prose, minuscule → pas de majuscule
# parasite au milieu d'une phrase). sect_phrase = "le secteur de la logistique" / "du tourisme".
sect_phrase     <- fr_secteur_phrase(params$secteur_label, prep = "de")
sect_phrase_cap <- .cap0(sect_phrase)            # début de phrase
sect_court      <- tolower(fr_secteur_phrase(params$secteur_label, prep = "", prefix = ""))  # "logistique" / "tourisme"
Voir le code
htmltools::tagList(
  kpi_row(wide = TRUE,
    kpi_band(category = "Volume", value = fmt_k(sy_vol),
             ref = sprintf("soit <strong>%s</strong> de l'emploi privé FR", fmt_pct_1dec(sy_part))),
    kpi_band(category = sprintf("Dynamique %d-%d", p_evol[1], p_evol[2]),
             value = sprintf("%s %%/an", fmt_sign_1dec(z0(sy_tcam))),
             ref = sprintf("vs <strong>%s %%/an</strong> emploi privé total", fmt_sign_1dec(z0(sy_tcamtot)))),
    kpi_band(category = "Féminisation", value = fmt_pct_1dec(sy_fem),
             ref = "de femmes parmi les salariés"),
    kpi_band(category = "Temps partiel", value = fmt_pct_1dec(sy_tpart),
             ref = "des postes à temps partiel")
  ),
  kpi_source(sprintf("URSSAF-ACOSS %d · INSEE EEC %d", ann_ref, ann_eec))
)
Volume
412 k soit 2,1 % de l'emploi privé FR
Dynamique 2023-2025
+1,8 %/an vs 0,0 %/an emploi privé total
Féminisation
27,6 % de femmes parmi les salariés
Temps partiel
7,4 % des postes à temps partiel
URSSAF-ACOSS 2025 · INSEE EEC 2024
Voir le code
.dir_dyn <- dplyr::case_when(
  sy_tcam > sy_tcamtot + 0.3 ~ "plus vite que",
  sy_tcam < sy_tcamtot - 0.3 ~ "moins vite que",
  TRUE                       ~ "au rythme de")
cat(glue::glue(
  "- **Un poids significatif** — {sy_sect} emploie **{fmt_k(sy_vol)} salariés privés**, ",
  "soit **{fmt_pct_1dec(sy_part)} de l'emploi privé français**.\n",
  "- **Une croissance soutenue** — les effectifs progressent de **{fmt_sign_1dec(z0(sy_tcam))} % par an** ",
  "sur {p_evol[1]}-{p_evol[2]}, {.dir_dyn} l'emploi privé total (**{fmt_sign_1dec(z0(sy_tcamtot))} % par an**).\n",
  "- **Un profil d'emploi singulier** — main-d'œuvre fortement féminisée (**{fmt_pct_1dec(sy_fem)}**) ",
  "et recours marqué au **temps partiel** (**{fmt_pct_1dec(sy_tpart)}**), deux marqueurs qui distinguent ",
  "le secteur du tissu d'emploi national.\n"
))
  • Un poids significatif — le secteur de la logistique emploie 412 k salariés privés, soit 2,1 % de l’emploi privé français.
  • Une croissance soutenue — les effectifs progressent de +1,8 % par an sur 2023-2025, plus vite que l’emploi privé total (0,0 % par an).
  • Un profil d’emploi singulier — main-d’œuvre fortement féminisée (27,6 %) et recours marqué au temps partiel (7,4 %), deux marqueurs qui distinguent le secteur du tissu d’emploi national.

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

Périmètre, sources et conventions

Cette fiche caractérise le secteur Logistique (entreposage et services auxiliaires des transports) selon quatre axes : son poids et sa dynamique récente, la structure et le profil de ses emplois, sa géographie nationale, puis un focus territorial sur Hauts-de-France.

Périmètre du secteur

Le secteur est défini par les divisions cœur ci-dessous, comparées à un agrégat proche :

Voir le code
# Périmètre dynamique depuis params · libellés officiels via .naf_full_lib (_setup-common.R, TDC NAF)
cat(paste0(
  c(
    vapply(coeur, function(cd) {
      sprintf("- **%s** *(cœur)*", .naf_full_lib(cd))
    }, character(1)),
    vapply(comparab, function(cd) {
      sprintf("- **%s** *(comparable)*", .naf_full_lib(cd))
    }, character(1))
  ),
  collapse = "\n"
))
  • Entreposage et services auxiliaires des transports (cœur)
  • Transports terrestres et transport par conduites (comparable)

Sources mobilisées

  • URSSAF-ACOSS — effectifs salariés privés communaux par NA88 (2006-2025). Source de référence pour le volume et la dynamique.
  • INSEE FLORES — effectifs salariés totaux (privé + public, hors particuliers employeurs), 2017-2024. Sert au poids public et à la spécialisation territoriale.
  • INSEE EEC — profil emploi national par NA88 (millésimes 2022 et 2024) : sexe, âge, diplôme, statut, temps partiel.
  • INSEE MOBZELT 2022 — profil territorial par zone d’emploi × NA88 (lieu de travail).

Conventions de calcul

Les évolutions sont mesurées en TCAM (taux de croissance annuel moyen) sur deux horizons : tendance longue 2019-2025 et dynamique récente 2023-2025 (URSSAF ; 2019-2024 et 2023-2024 pour FLORES). L’indice de spécialisation (IS) rapporte la part du secteur dans l’emploi local à sa part nationale (France = 100) ; il est colorisé en échelle divergente. Les moyennes de profil sont pondérées (EXTRIAN pour l’EEC, IPONDI pour MOBZELT). Validation cross-source URSSAF vs EEC à ±10 %.

Vue nationale

Portrait d’ensemble : poids et dynamique récente

Le secteur de la logistique occupe une place mesurable dans l’emploi privé français et y suit une trajectoire propre. Cette partie en donne le poids, la dynamique récente, la composition interne et le positionnement dans l’économie nationale.

Voir le code
# Volume + dynamique URSSAF privé + profil EEC (compute national, consommé aussi par srpt-03/05/06/07)
us <- dat$urssaf |>
  filter(measure == "EFF_SAL", code_a88 %in% coeur) |>
  group_by(year) |>
  summarise(emp_priv = sum(value, na.rm = TRUE), .groups = "drop")
us_24    <- us$emp_priv[us$year == ann_ref]
tcam_us  <- ((us_24 / us$emp_priv[us$year == p_evol[1]])^(1 / (p_evol[2] - p_evol[1])) - 1) * 100
.us_2019 <- us$emp_priv[us$year == 2019]
tcam_lng <- ((us_24 / .us_2019)^(1 / (ann_ref - 2019)) - 1) * 100

# Référence URSSAF tous secteurs (privé total FR)
us_tot <- dat$urssaf |>
  filter(measure == "EFF_SAL", year %in% c(p_evol[1], ann_ref)) |>
  group_by(year) |>
  summarise(emp_tot = sum(value, na.rm = TRUE), .groups = "drop")
emp_tot_24 <- us_tot$emp_tot[us_tot$year == ann_ref]
tcam_tot   <- ((emp_tot_24 / us_tot$emp_tot[us_tot$year == p_evol[1]])^(1 / (p_evol[2] - p_evol[1])) - 1) * 100
part_secteur <- us_24 / emp_tot_24 * 100

log_result("kpi_volume_ref", us_24, interpret = sprintf("Eff. privés cœur %d : %s (%.2f%% privé FR)", ann_ref, fmt_k(us_24), part_secteur))
log_result("kpi_tcam_sect", tcam_us, interpret = sprintf("TCAM cœur %d-%d : %s (vs %s privé total)", p_evol[1], p_evol[2], fmt_sign(tcam_us), fmt_sign(tcam_tot)))
log_result("kpi_part_sect_pct", part_secteur)

# EEC : profil national secteur vs FR (consommé par srpt-07)
eec     <- dat$eec |> filter(code_naf88 %in% coeur, annee == ann_eec)
eec_agg <- eec |> summarise(emp = sum(lab_eff_vol),
  fem = weighted.mean(lab_fem_pct, lab_eff_vol), tpart = weighted.mean(lab_tpart_pct, lab_eff_vol),
  p50 = weighted.mean(lab_50plus_pct, lab_eff_vol))
eec_tot <- dat$eec |> filter(annee == ann_eec) |> summarise(
  fem_fr = weighted.mean(lab_fem_pct, lab_eff_vol), tpart_fr = weighted.mean(lab_tpart_pct, lab_eff_vol),
  p50_fr = weighted.mean(lab_50plus_pct, lab_eff_vol))
delta_fem <- eec_agg$fem - eec_tot$fem_fr; delta_tpart <- eec_agg$tpart - eec_tot$tpart_fr
delta_p50 <- eec_agg$p50 - eec_tot$p50_fr
eec_full_2024   <- dat$eec |> filter(annee == ann_eec)
eec_agg_bacplus <- eec_full_2024 |> filter(code_naf88 %in% coeur) |> summarise(b = weighted.mean(lab_bacplus_pct, lab_eff_vol)) |> pull(b)
fr_bacplus      <- eec_full_2024 |> summarise(b = weighted.mean(lab_bacplus_pct, lab_eff_vol)) |> pull(b)
gap_bacplus     <- eec_agg_bacplus - fr_bacplus

# Extrêmes par NAF cœur (volume + dynamique) pour ▲▼ des KPI cards
us_par_naf <- dat$urssaf |> filter(measure == "EFF_SAL", code_a88 %in% coeur, year %in% c(p_evol[1], ann_ref)) |>
  group_by(code_a88, year) |> summarise(emp = sum(value, na.rm = TRUE), .groups = "drop") |>
  pivot_wider(names_from = year, values_from = emp, names_prefix = "y") |>
  mutate(tcam = ((.data[[paste0("y", ann_ref)]] / .data[[paste0("y", p_evol[1])]])^(1 / (p_evol[2] - p_evol[1])) - 1) * 100)
.us_top  <- us_par_naf |> slice_max(.data[[paste0("y", ann_ref)]], n = 1)
.us_bot  <- us_par_naf |> slice_min(.data[[paste0("y", ann_ref)]], n = 1)
.tcam_top <- us_par_naf |> slice_max(tcam, n = 1)
.tcam_bot <- us_par_naf |> slice_min(tcam, n = 1)

.kpi_ext <- function(lbl, val) sprintf("%s %s", lbl, val)
Voir le code
htmltools::tagList(
  kpi_row(wide = TRUE,
    kpi_band(category = "Volume", value = fmt_k(us_24),
             ref = sprintf("soit <strong>%s</strong> de l'emploi privé FR", fmt_pct_1dec(part_secteur)),
             top = .kpi_ext(.naf_lbl(.us_top$code_a88), fmt_k(.us_top[[paste0("y", ann_ref)]])),
             bottom = .kpi_ext(.naf_lbl(.us_bot$code_a88), fmt_k(.us_bot[[paste0("y", ann_ref)]]))),
    kpi_band(category = sprintf("Dynamique récente %d-%d", p_evol[1], p_evol[2]),
             value = sprintf("%s %%/an", fmt_sign_1dec(z0(tcam_us))),
             ref = sprintf("vs <strong>%s %%/an</strong> emploi privé total", fmt_sign_1dec(z0(tcam_tot))),
             top = .kpi_ext(.naf_lbl(.tcam_top$code_a88), sprintf("%s %%", fmt_sign_1dec(.tcam_top$tcam))),
             bottom = .kpi_ext(.naf_lbl(.tcam_bot$code_a88), sprintf("%s %%", fmt_sign_1dec(.tcam_bot$tcam)))),
    kpi_band(category = sprintf("Tendance longue %d-%d", 2019L, ann_ref),
             value = sprintf("%s %%/an", fmt_sign_1dec(tcam_lng)),
             ref = "effectifs salariés privés")
  ),
  kpi_source(sprintf("URSSAF-ACOSS · %d · ▲ NAF le plus haut / ▼ le plus bas (intra-cœur)", ann_ref))
)
Volume
412 k soit 2,1 % de l'emploi privé FR
Entreposage et services auxiliaires des transports 412 k Entreposage et services auxiliaires des transports 412 k
Dynamique récente 2023-2025
+1,8 %/an vs 0,0 %/an emploi privé total
Entreposage et services auxiliaires des transports +1,8 % Entreposage et services auxiliaires des transports +1,8 %
Tendance longue 2019-2025
+2,1 %/an effectifs salariés privés
URSSAF-ACOSS · 2025 · ▲ NAF le plus haut / ▼ le plus bas (intra-cœur)
Voir le code
.sect_cap <- .cap0(fr_secteur_phrase(params$secteur_label, prep = "de"))
# Comparaison de deux taux : seuil absolu (l'écart relatif explose quand le réf ≈ 0)
.dirq <- dplyr::case_when(
  tcam_us > tcam_tot + 0.3 ~ "nettement plus rapide que",
  tcam_us < tcam_tot - 0.3 ~ "plus lent que",
  TRUE                     ~ "comparable à")
cat(glue::glue(
  "[**{.sect_cap}** emploie **{fmt_k(us_24)} salariés privés** en {ann_ref}, ",
  "soit **{fmt_pct_1dec(part_secteur)} de l'emploi privé français**.]{{.insight}}\n\n",
  "Sur la période récente {p_evol[1]}-{p_evol[2]}, le secteur progresse de **{fmt_sign_1dec(z0(tcam_us))} % par an**, ",
  "un rythme {.dirq} celui de l'emploi privé total (**{fmt_sign_1dec(z0(tcam_tot))} % par an**). ",
  "Sa tendance de fond depuis 2019 (**{fmt_sign_1dec(tcam_lng)} % par an**) confirme une croissance structurelle ",
  "qui fait du secteur un contributeur net à l'emploi privé."
))

Le secteur de la logistique emploie 412 k salariés privés en 2025, soit 2,1 % de l’emploi privé français.

Sur la période récente 2023-2025, le secteur progresse de +1,8 % par an, un rythme nettement plus rapide que celui de l’emploi privé total (0,0 % par an). Sa tendance de fond depuis 2019 (+2,1 % par an) confirme une croissance structurelle qui fait du secteur un contributeur net à l’emploi privé.

Avant d’examiner sa trajectoire, on situe le secteur dans l’économie nationale : sa section d’appartenance parmi les 21 grands secteurs, son poids relatif et son orientation récente.

Voir le code
# Treemap A — le secteur dans l'économie nationale (A21, FLORES privé+public).
# Mapping A88→A21 + agrégation France entière ; la section du cœur ressort (bordure épaisse).
.a21_lbl <- c(
  A = "Agriculture", B = "Industries extractives", C = "Industrie manufacturière",
  D = "Énergie", E = "Eau, déchets", F = "Construction", G = "Commerce",
  H = "Transports-entreposage", I = "Hébergement-restauration",
  J = "Information-communication", K = "Finance-assurance", L = "Immobilier",
  M = "Activités scientifiques-techniques", N = "Services administratifs",
  O = "Administration publique", P = "Enseignement", Q = "Santé-action sociale",
  R = "Arts-spectacles", S = "Autres services", T = "Ménages employeurs",
  U = "Extra-territorial")

map_a88_a21 <- readr::read_delim(
  "C:/Users/vince/DBD-datab/TDC-ref/fr/eco-tdc/NAF/tdc_naf_mapping_a88_a38_a17_a21_260527.csv",
  delim = ";", show_col_types = FALSE) |>
  dplyr::transmute(code_a88 = a88, a21 = a21) |> dplyr::distinct()

.fl_max <- max(dat$flores_com$year, na.rm = TRUE)
eco_a21 <- dat$flores_com |>
  filter(year %in% c(2019L, .fl_max)) |>
  left_join(map_a88_a21, by = "code_a88") |>
  filter(!is.na(a21)) |>
  group_by(a21, year) |>
  summarise(emp = sum(value, na.rm = TRUE), .groups = "drop") |>
  tidyr::pivot_wider(names_from = year, values_from = emp, names_prefix = "y") |>
  mutate(libelle = unname(.a21_lbl[a21]),
         emp_ref = .data[[paste0("y", .fl_max)]],
         evol    = ((.data[[paste0("y", .fl_max)]] / .data[[paste0("y", 2019L)]]) - 1) * 100) |>
  filter(!is.na(emp_ref), emp_ref > 0)

# Section A21 du secteur cœur (highlight)
sect_a21     <- unique(map_a88_a21$a21[map_a88_a21$code_a88 %in% coeur])
sect_a21_lbl <- unname(.a21_lbl[sect_a21])

plot_tree_bygroup(
  eco_a21, area_col = "emp_ref", label_col = "libelle",
  fill_mode = "gradient", fill_var = "evol", gradient_center = 0,
  highlight_labels = sect_a21_lbl,
  label_extra_col = "evol", label_extra_unit = " %",
  legend_title = sprintf("Évol.\n%d-%d", 2019L, .fl_max),
  label_mode = "pct_vol",
  title    = sprintf("La section %s dans l'économie française — 21 grands secteurs",
                     paste(sect_a21_lbl, collapse = " / ")),
  subtitle = sprintf("Aire = effectifs salariés totaux %d (privé+public) · couleur = évolution %d-%d · cadre = section du secteur",
                     .fl_max, 2019L, .fl_max),
  caption  = make_source("INSEE FLORES")
)

Voir le code
.eco_rk   <- eco_a21 |> arrange(desc(emp_ref)) |> mutate(rk = dplyr::row_number())
.sect_rw  <- .eco_rk |> filter(libelle %in% sect_a21_lbl)
.sect_sh  <- .sect_rw$emp_ref / sum(eco_a21$emp_ref) * 100
.eco_evol <- (sum(eco_a21[[paste0("y", .fl_max)]]) / sum(eco_a21[[paste0("y", 2019L)]]) - 1) * 100
.sect_dir <- dplyr::case_when(
  .sect_rw$evol > .eco_evol + 1 ~ "plus vite que l'ensemble",
  .sect_rw$evol < .eco_evol - 1 ~ "moins vite que l'ensemble",
  TRUE                          ~ "au rythme de l'ensemble")
cat(glue::glue(
  "La section **{sect_a21_lbl}** est le **{.sect_rw$rk}{ifelse(.sect_rw$rk==1,'er','e')} employeur** ",
  "de France parmi les 21 grands secteurs, avec **{fmt_pct_1dec(.sect_sh)} de l'emploi salarié total** ",
  "*(figure ci-contre)*. Sur {2019L}-{.fl_max}, ses effectifs progressent de **{fmt_sign_1dec(.sect_rw$evol)} %**, ",
  "soit {.sect_dir} (**{fmt_sign_1dec(.eco_evol)} %**)."
))

La section Transports-entreposage est le 9e employeur de France parmi les 21 grands secteurs, avec 5,6 % de l’emploi salarié total (figure ci-contre). Sur 2019-2024, ses effectifs progressent de +3,8 %, soit moins vite que l’ensemble (+5,4 %).

L’emploi sectoriel se lit ensuite à deux échelles complémentaires : la trajectoire agrégée depuis dix ans (URSSAF privé et FLORES tous statuts en parallèle) et la dispersion interne entre sous-segments d’activité, où certains accélèrent quand d’autres décrochent.

Voir le code
# Trajectoire agrégée — indice base 100 = 2019, 3 séries parallèles
# URSSAF privé (→ann_ref) / FLORES total privé+public (→2024) / réf. emploi privé France.
.an_idx <- 2019L
us_raw <- dat$urssaf |> filter(measure == "EFF_SAL", year >= .an_idx)
fl_raw <- dat$flores_com |> filter(measure == "EFF_SAL", year >= .an_idx)

# Labels de séries courts (le titre porte déjà le secteur) → encadrés de fin compacts
us_coeur <- us_raw |> filter(code_a88 %in% coeur) |>
  group_by(year) |> summarise(emp = sum(value, na.rm = TRUE), .groups = "drop") |>
  mutate(grp = "Cœur privé (URSSAF)", src = "urssaf")
fl_coeur <- fl_raw |> filter(code_a88 %in% coeur) |>
  group_by(year) |> summarise(emp = sum(value, na.rm = TRUE), .groups = "drop") |>
  mutate(grp = "Cœur total (FLORES)", src = "flores")
us_priv_tot <- us_raw |>
  group_by(year) |> summarise(emp = sum(value, na.rm = TRUE), .groups = "drop") |>
  mutate(grp = "Privé total France", src = "urssaf")

dyn_long <- bind_rows(us_coeur, fl_coeur, us_priv_tot) |>
  group_by(grp) |> arrange(year) |>
  mutate(indice = emp / emp[year == .an_idx] * 100) |>
  ungroup()

# Palette : cœur privé (focus) / cœur total (teinte atténuée) / réf France (gris)
pal_dyn <- c(
  setNames("#1696d2", "Cœur privé (URSSAF)"),
  setNames("#9ecae1", "Cœur total (FLORES)"),
  setNames("#a2aaad", "Privé total France"))
dyn_long$grp <- factor(dyn_long$grp, levels = names(pal_dyn))

# Creux du secteur cœur URSSAF (covid) — utilisé par la narration
sect_serie <- dyn_long |> filter(grepl("URSSAF", grp))
creux_row  <- sect_serie |> filter(year > .an_idx) |> slice_min(indice, n = 1)
log_result("dyn_indices_ref", setNames(
  round(dyn_long$indice[dyn_long$year == max(dyn_long$year[dyn_long$src=="urssaf"])], 1),
  as.character(dyn_long$grp[dyn_long$year == max(dyn_long$year[dyn_long$src=="urssaf"])])))
Voir le code
# Courbe indice 100 pleine page via le helper canonique (focus + end-labels in-plot, sans points)
plot_line_indice100(
  dyn_long, x = "year", idx = "indice", serie = "grp",
  ref_value = 100, points = FALSE, name_in_plot = TRUE, value_labels = FALSE,
  colors = unname(pal_dyn),
  x_breaks = seq(.an_idx, ann_ref, by = 2),
  title    = sprintf("Emploi %s · indice base 100 = %d", sect_lbl, .an_idx),
  subtitle = "Cœur privé (URSSAF, → 2025) · total privé+public (FLORES, → 2024) · référence emploi privé France",
  y_lab    = sprintf("Indice (%d = 100)", .an_idx),
  caption  = make_source("URSSAF-ACOSS", "INSEE FLORES"))

Voir le code
# Narration trajectoire — sector-agnostic, dérivée de dyn_long (chunk précédent)
# grepl("URSSAF") = ligne secteur privé UNIQUEMENT (pas "Emploi privé total France")
.su       <- dyn_long |> filter(grepl("URSSAF", grp))
.an_us    <- max(.su$year); .ind_us_end <- .su$indice[.su$year == .an_us]
.sf       <- dyn_long |> filter(grepl("FLORES", grp))
.an_fl    <- max(.sf$year); .ind_fl_end <- .sf$indice[.sf$year == .an_fl]
.gap_pub  <- .ind_fl_end - .su$indice[.su$year == .an_fl]   # écart total−privé à l'année FLORES max
.gap_lbl  <- dplyr::case_when(
  abs(.gap_pub) < 1.5 ~ "restent quasi confondues",
  .gap_pub >= 1.5     ~ "s'écartent, l'emploi total progressant plus vite que le seul privé",
  TRUE                ~ "s'écartent, le privé progressant plus vite que l'emploi total")

# Creux : distinguer un VRAI repli (covid, indice < 99) d'une croissance continue
.a_repli <- creux_row$indice < 99
.recov   <- .su |> filter(year > creux_row$year, indice >= 100) |> slice_min(year, n = 1)
if (.a_repli) {
  .insight_txt <- glue::glue("L'emploi du secteur dépasse son niveau d'avant-crise de **{fmt_sign_1dec(.ind_us_end - 100)} %** en {.an_us}, après le repli de **{fmt_sign_1dec(creux_row$indice - 100)} %** subi en {creux_row$year}.")
  .recov_txt   <- if (nrow(.recov)) glue::glue(", mais la reprise a été rapide : dès {.recov$year} le secteur repassait au-dessus de son niveau d'avant-crise") else ""
  .traj_txt    <- glue::glue("Le choc de {creux_row$year} a fait plonger l'emploi salarié privé à l'indice **{round(creux_row$indice)}** (base 100 en {2019L}){.recov_txt}. ")
} else {
  .insight_txt <- glue::glue("L'emploi du secteur progresse sans rupture sur la période, gagnant **{fmt_sign_1dec(.ind_us_end - 100)} %** depuis {2019L} (base 100).")
  .traj_txt    <- glue::glue("La trajectoire est régulière : aucun repli marqué, le point bas ({creux_row$year}, indice {round(creux_row$indice)}) restant au-dessus du niveau initial. ")
}

.ind_su_atfl <- .su$indice[.su$year == .an_fl]   # indice privé à l'année FLORES la plus récente
cat(glue::glue(
  "[{.insight_txt}]{{.insight}}\n\n",
  "{.traj_txt}",
  "Les deux mesures — emploi privé (URSSAF) et tous statuts (FLORES) — **{.gap_lbl}** : ",
  "depuis {2019L}, l'emploi total a progressé de **{fmt_sign_1dec(.ind_fl_end - 100)} %** et le seul privé de ",
  "**{fmt_sign_1dec(.ind_su_atfl - 100)} %** à fin {.an_fl}, ce qui situe le poids de l'emploi public dans le secteur."
))

L’emploi du secteur progresse sans rupture sur la période, gagnant +13,2 % depuis 2019 (base 100).

La trajectoire est régulière : aucun repli marqué, le point bas (2020, indice 100) restant au-dessus du niveau initial. Les deux mesures — emploi privé (URSSAF) et tous statuts (FLORES) — restent quasi confondues : depuis 2019, l’emploi total a progressé de +10,9 % et le seul privé de +11,2 % à fin 2024, ce qui situe le poids de l’emploi public dans le secteur.

Structure et profil d’emplois

Voir le code
# Sources : EEC (national) — agrégat tous statuts millésime ref
eec_24_data <- dat$eec |> filter(annee == ann_eec)

# Helper agrégation pondérée (sum en dernier pour éviter masquage colonne)
# TODO pipeline EEC : ajouter lab_30moins_pct, lab_anc20p_pct, lab_infbac_pct,
#   lab_bac2p_pct, lab_bac5p_pct (cf gdtodo-enrich-eec-pipeline_260602.md)
.weighted_agg <- function(df, label) {
  df |> summarise(
    lab_fem_pct      = weighted.mean(lab_fem_pct, lab_eff_vol),
    lab_agemoy       = weighted.mean(lab_agemoy, lab_eff_vol),
    lab_25moins_pct  = weighted.mean(lab_25moins_pct, lab_eff_vol),
    lab_50plus_pct   = weighted.mean(lab_50plus_pct, lab_eff_vol),
    lab_65plus_pct   = weighted.mean(lab_65plus_pct, lab_eff_vol),
    lab_anc5p_pct    = weighted.mean(lab_anc5p_pct, lab_eff_vol),
    lab_infrabac_pct = weighted.mean(lab_infrabac_pct, lab_eff_vol),
    lab_bac2plus_pct = weighted.mean(lab_bac2plus_pct, lab_eff_vol),
    lab_bac5plus_pct = weighted.mean(lab_bac5plus_pct, lab_eff_vol),
    lab_bacplus_pct  = weighted.mean(lab_bacplus_pct, lab_eff_vol),
    lab_cdd_pct      = weighted.mean(lab_cdd_pct, lab_eff_vol),
    lab_tpart_pct    = weighted.mean(lab_tpart_pct, lab_eff_vol),
    lab_souempl_pct  = weighted.mean(lab_souempl_pct, lab_eff_vol),
    lab_pub_pct      = weighted.mean(lab_pub_pct, lab_eff_vol),
    lab_pe_pct       = weighted.mean(lab_pe_pct, lab_eff_vol),
    lab_eff_vol      = sum(lab_eff_vol)
  ) |> mutate(label = label)
}

# Labels propres pour affichage (pas de codes NA88 dans le rendu)
# unname() obligatoire : vapply retourne un vecteur nommé qui casse tidyselect (select renomme)
sect_lbl  <- params$secteur_short %||% params$secteur_label
naf_lbls  <- unname(vapply(coeur, .naf_lbl, character(1)))

# Total emploi national (référence) et secteur cœur agrégé
eec_fr   <- .weighted_agg(eec_24_data, "FR total")
eec_sect <- .weighted_agg(eec_24_data |> filter(code_naf88 %in% coeur), sect_lbl)

# NAF individuels (1 ligne par code coeur, dynamique avec label propre)
.profile_cols <- c("label", "lab_eff_vol",
                   "lab_fem_pct", "lab_agemoy", "lab_25moins_pct", "lab_50plus_pct",
                   "lab_65plus_pct", "lab_anc5p_pct",
                   "lab_infrabac_pct", "lab_bac2plus_pct", "lab_bac5plus_pct", "lab_bacplus_pct",
                   "lab_cdd_pct", "lab_tpart_pct", "lab_souempl_pct",
                   "lab_pub_pct", "lab_pe_pct")

eec_individuels <- purrr::map_dfr(coeur, function(cd) {
  eec_24_data |> filter(code_naf88 == cd) |> mutate(label = .naf_lbl(cd))
}) |> select(all_of(.profile_cols))

# Tous regroupés
all_cols <- bind_rows(eec_sect, eec_individuels, eec_fr) |>
  select(all_of(.profile_cols))

# Part dans emp tot
emp_tot_global <- eec_fr$lab_eff_vol
all_cols$part_emp_tot_pct <- all_cols$lab_eff_vol / emp_tot_global * 100

# Logging des chiffres clés
log_result("eec_sect_profile", list(
  sect_eff = eec_sect$lab_eff_vol, sect_fem = eec_sect$lab_fem_pct,
  sect_tp = eec_sect$lab_tpart_pct, sect_cdd = eec_sect$lab_cdd_pct,
  sect_pub = eec_sect$lab_pub_pct, sect_anc5p = eec_sect$lab_anc5p_pct,
  sect_souempl = eec_sect$lab_souempl_pct
))

# Calculs des écarts secteur vs FR (réutilisés narration analytique)
delta_qualif <- list(
  fem    = eec_sect$lab_fem_pct    - eec_fr$lab_fem_pct,
  tpart  = eec_sect$lab_tpart_pct  - eec_fr$lab_tpart_pct,
  cdd    = eec_sect$lab_cdd_pct    - eec_fr$lab_cdd_pct,
  pub    = eec_sect$lab_pub_pct    - eec_fr$lab_pub_pct,
  anc5p  = eec_sect$lab_anc5p_pct  - eec_fr$lab_anc5p_pct,
  bacplus = eec_sect$lab_bacplus_pct - eec_fr$lab_bacplus_pct,
  souempl = eec_sect$lab_souempl_pct - eec_fr$lab_souempl_pct,
  agemoy = eec_sect$lab_agemoy     - eec_fr$lab_agemoy
)
log_result("_detail_delta_qualif", delta_qualif,
           interpret = sprintf("Écarts secteur vs FR (pts) : fem %+.1f · TP %+.1f · pub %+.1f · CDD %+.1f · bac+ %+.1f",
                              delta_qualif$fem, delta_qualif$tpart, delta_qualif$pub,
                              delta_qualif$cdd, delta_qualif$bacplus))
Voir le code
# Analyse insight-first — qualifs écart relatif alignés sur breaks_ratio gt_jumelles_pool
# (cohérence prose / coloration tableau). Pour régen : /rgen-write-anal profil_natnl
qualif_fem    <- fr_ecart_qualif(eec_sect$lab_fem_pct,    eec_fr$lab_fem_pct,    mode = "rel")
qualif_tp     <- fr_ecart_qualif(eec_sect$lab_tpart_pct,  eec_fr$lab_tpart_pct,  mode = "rel")
qualif_p50    <- fr_ecart_qualif(eec_sect$lab_50plus_pct, eec_fr$lab_50plus_pct, mode = "rel")
qualif_bac    <- fr_ecart_qualif(eec_sect$lab_bacplus_pct, eec_fr$lab_bacplus_pct, mode = "rel")
qualif_pub    <- fr_ecart_qualif(eec_sect$lab_pub_pct,   eec_fr$lab_pub_pct,    mode = "rel")

# Contrastes internes calculés dynamiquement (au lieu de hardcode NAF 87/88)
fem_par_naf <- eec_individuels |> dplyr::arrange(dplyr::desc(lab_fem_pct))
tp_par_naf  <- eec_individuels |> dplyr::arrange(dplyr::desc(lab_tpart_pct))
naf_fem_max <- fem_par_naf$label[1];   v_fem_max <- fem_par_naf$lab_fem_pct[1]
naf_fem_min <- fem_par_naf$label[nrow(fem_par_naf)]; v_fem_min <- fem_par_naf$lab_fem_pct[nrow(fem_par_naf)]
naf_tp_max  <- tp_par_naf$label[1];    v_tp_max  <- tp_par_naf$lab_tpart_pct[1]
naf_tp_min  <- tp_par_naf$label[nrow(tp_par_naf)];   v_tp_min  <- tp_par_naf$lab_tpart_pct[nrow(tp_par_naf)]

# Contraste interne UNIQUEMENT si ≥ 2 divisions cœur (sinon = comparer une division à elle-même)
.multi_div <- nrow(eec_individuels) >= 2
.contraste_fem <- if (.multi_div) glue::glue(
  "Le contraste interne est net : **{naf_fem_max}** tire la moyenne à la hausse ({fmt_pct_1dec(v_fem_max)}), ",
  "tandis que **{naf_fem_min}** affiche un niveau plus contenu ({fmt_pct_1dec(v_fem_min)}). ") else ""
.contraste_tp <- if (.multi_div) glue::glue(
  "Là encore les sous-secteurs divergent : **{naf_tp_max}** culmine à {fmt_pct_1dec(v_tp_max)} de temps partiel, ",
  "quand **{naf_tp_min}** redescend à {fmt_pct_1dec(v_tp_min)}. ",
  "Cette dispersion signale des modèles d'organisation du travail contrastés au sein d'un même périmètre statistique. ") else ""

cat(glue::glue(
  # INSIGHT — chiffres-choc + mise en perspective
  "[Le secteur compte **{fmt_pct_1dec(eec_sect$lab_fem_pct)} de femmes** et **{fmt_pct_1dec(eec_sect$lab_pub_pct)} d'emploi public**, ",
  "deux marqueurs qui le situent à l'écart du tissu d'emploi national.]{{.insight}}\n\n",

  # Para 1 — féminisation, qualif aligné couleur tableau + contraste sous-secteurs (si multi-division)
  "Le profil démographique du secteur est singulier. ",
  "La part des femmes y atteint **{fmt_pct_1dec(eec_sect$lab_fem_pct)}**, un niveau {qualif_fem} salariat français ",
  "({fmt_pct_1dec(eec_fr$lab_fem_pct)}, écart de **{fmt_sign_1dec(delta_qualif$fem)} pts**). ",
  "{.contraste_fem}\n\n",

  # Para 2 — temps partiel, dispersion intra-secteur (si multi-division)
  "Le **temps partiel** est un second marqueur structurel. ",
  "Il concerne **{fmt_pct_1dec(eec_sect$lab_tpart_pct)}** des salariés, un niveau {qualif_tp} tissu national ",
  "({fmt_pct_1dec(eec_fr$lab_tpart_pct)}, écart de **{fmt_sign_1dec(delta_qualif$tpart)} pts**). ",
  "{.contraste_tp}\n\n",

  # Para 3 — statut public + vieillissement + qualification
  "L'**emploi public** représente **{fmt_pct_1dec(eec_sect$lab_pub_pct)}** des effectifs, un niveau {qualif_pub} salariat national ",
  "({fmt_pct_1dec(eec_fr$lab_pub_pct)}, écart de **{fmt_sign_1dec(delta_qualif$pub)} pts**). ",
  "Le **vieillissement** y atteint **{fmt_pct_1dec(eec_sect$lab_50plus_pct)}** de 50 ans et plus, un niveau {qualif_p50} salariat national ",
  "({fmt_pct_1dec(eec_fr$lab_50plus_pct)}). ",
  "Le **niveau de qualification** se situe à **{fmt_pct_1dec(eec_sect$lab_bacplus_pct)}** de Bac et plus — un niveau {qualif_bac} privé français ",
  "({fmt_pct_1dec(eec_fr$lab_bacplus_pct)}, écart de **{fmt_sign_1dec(delta_qualif$bacplus)} pts**). ",
  "Cet écart de diplôme pèse directement sur les besoins de formation et les marges de progression salariale du secteur."
))

Le secteur compte 27,6 % de femmes et 12,4 % d’emploi public, deux marqueurs qui le situent à l’écart du tissu d’emploi national.

Le profil démographique du secteur est singulier. La part des femmes y atteint 27,6 %, un niveau nettement en deçà du salariat français (48,8 %, écart de -21,2 pts).

Le temps partiel est un second marqueur structurel. Il concerne 7,4 % des salariés, un niveau très en deçà du tissu national (17,6 %, écart de -10,1 pts).

L’emploi public représente 12,4 % des effectifs, un niveau nettement en deçà du salariat national (23,5 %, écart de -11,1 pts). Le vieillissement y atteint 29,1 % de 50 ans et plus, un niveau légèrement en deçà du salariat national (32,9 %). Le niveau de qualification se situe à 60,3 % de Bac et plus — un niveau légèrement en deçà du privé français (68,8 %, écart de -8,5 pts). Cet écart de diplôme pèse directement sur les besoins de formation et les marges de progression salariale du secteur.

Voir le code
# Tableau gt_jumelles_pool — pattern écart/cible (showcase gt-tr-grp-2h-colecart)
# Cible = secteur cœur, ref = France, sous-cols = NAF individuels du cœur (Hébergement, Restauration, etc.)
source(file.path("C:/Users/vince/hh/pq/PDS/mutils/jrr", "jcn-jumelles-pool.R"))

# Définition indicateurs — 4 catégories métier (VOLUME ET POIDS / DÉMOGRAPHIE /
# QUALIFICATION / STRUCTURE DE L'EMPLOI). EEC enrichi 26-06-05 : âge jeunes/50+/65+,
# diplôme infra-bac / Bac+2 et + / Bac+5 et + (AGE6 ne permet pas <30 ni 60+).
indic_def <- tibble::tribble(
  ~var,                ~categorie,                ~bloc,         ~indicateur,                       ~unite,  ~type,
  # VOLUME ET POIDS — volume brut + poids dans l'emploi + part publique
  "lab_eff_vol",       "VOLUME ET POIDS",         "effectifs",   "Effectifs salariés",               "n",     "vol",
  "part_emp_tot_pct",  "VOLUME ET POIDS",         "structure",   "Part dans emploi total",           "%",     "vol",
  "lab_pub_pct",       "VOLUME ET POIDS",         "statut",      "Part emploi public",               "%",     "pct",
  # DÉMOGRAPHIE — genre, âge (jeunes / seniors), ancienneté
  "lab_fem_pct",       "DÉMOGRAPHIE",             "genre",       "Part de femmes",                   "%",     "pct",
  "lab_agemoy",        "DÉMOGRAPHIE",             "âge",         "Âge moyen",                        "ans",   "ratio",
  "lab_25moins_pct",   "DÉMOGRAPHIE",             "âge",         "Part de jeunes (< 25 ans)",        "%",     "pct",
  "lab_50plus_pct",    "DÉMOGRAPHIE",             "âge",         "Part 50 ans et plus",              "%",     "pct",
  "lab_65plus_pct",    "DÉMOGRAPHIE",             "âge",         "Part 65 ans et plus",              "%",     "pct",
  "lab_anc5p_pct",     "DÉMOGRAPHIE",             "ancienneté",  "Part ancienneté 5 ans et plus",    "%",     "pct",
  # QUALIFICATION — gradient diplôme (infra-bac → Bac+2 → Bac+5)
  "lab_infrabac_pct",  "QUALIFICATION",           "diplôme",     "Part infra-bac",                   "%",     "pct",
  "lab_bac2plus_pct",  "QUALIFICATION",           "diplôme",     "Part Bac+2 et plus",               "%",     "pct",
  "lab_bac5plus_pct",  "QUALIFICATION",           "diplôme",     "Part Bac+5 et plus",               "%",     "pct",
  # STRUCTURE DE L'EMPLOI — contrat, durée, qualité d'emploi
  "lab_cdd_pct",       "STRUCTURE DE L'EMPLOI",   "contrat",     "Part CDD",                         "%",     "pct",
  "lab_tpart_pct",     "STRUCTURE DE L'EMPLOI",   "durée",       "Part temps partiel",               "%",     "pct",
  "lab_souempl_pct",   "STRUCTURE DE L'EMPLOI",   "qualité",     "Part en sous-emploi",              "%",     "pct"
)

# Pivot wide : 1 ligne = indicateur, cols territoires
tab_long <- all_cols |>
  pivot_longer(-label, names_to = "var", values_to = "value") |>
  inner_join(indic_def |> select(var, indicateur), by = "var")

tab_wide <- tab_long |>
  select(indicateur, label, value) |>
  pivot_wider(names_from = label, values_from = value) |>
  rename(France = `FR total`)

# Panel final avec meta cols + cols territoires (focus + NAF + France)
panel <- indic_def |>
  inner_join(tab_wide, by = "indicateur") |>
  mutate(indicateur = factor(indicateur, levels = indic_def$indicateur)) |>
  arrange(indicateur) |>
  mutate(indicateur = as.character(indicateur)) |>
  select(categorie, bloc, indicateur, unite, type,
         all_of(sect_lbl), all_of(naf_lbls), France)

# Titre insight dynamique : 2 marqueurs les + distinctifs (|écart relatif| vs France)
.markers <- tibble::tribble(
  ~lbl,                    ~v,                       ~ref,
  "féminisation",          eec_sect$lab_fem_pct,     eec_fr$lab_fem_pct,
  "temps partiel",         eec_sect$lab_tpart_pct,   eec_fr$lab_tpart_pct,
  "poids de l'emploi public", eec_sect$lab_pub_pct,  eec_fr$lab_pub_pct,
  "vieillissement",        eec_sect$lab_50plus_pct,  eec_fr$lab_50plus_pct,
  "faible qualification",  eec_sect$lab_infrabac_pct, eec_fr$lab_infrabac_pct
) |> mutate(gap_rel = abs(v / ref - 1)) |> arrange(desc(gap_rel))
.tbl_title <- gt::md(sprintf("**Profil emploi du secteur %s : %s et %s le distinguent du tissu national**",
                             sect_lbl, .markers$lbl[1], .markers$lbl[2]))

# Render via gt_jumelles_pool (auto-calcul écart vs France + coloration + spanners)
gt_jumelles_pool(
  panel,
  focus_col          = sect_lbl,
  ref_col            = "France",
  ref_label          = "France",
  title              = .tbl_title,
  source_note        = sprintf("Source : INSEE EEC %d · pondération EXTRIAN · actifs occupés. Âge ventilé en tranches AGE6 (< 25 / 50+ / 65+) ; diplôme en gradient infra-bac → Bac+2 → Bac+5.", ann_eec),
  spanner_other_label = "DÉTAIL PAR SOUS-SECTEUR"
)
Profil emploi du secteur Logistique : temps partiel et poids de l’emploi public le distinguent du tissu national
Theme Indicateur
 
U.
 
COMPARAISON PRINCIPALE
DÉTAIL PAR SOUS-SECTEUR
Logistique
cible
Logistique
cible
Écart
vs France
Écart
vs France
France
med.
France
med.
Entreposage et services auxiliaires des transports
VOLUME ET POIDS
effectifs Effectifs salariés n 410 976 29 021 954 410 976
structure Part dans emploi total % 1,4 100,0 1,4
statut Part emploi public % 12,4 −11,1 23,5 12,4
DÉMOGRAPHIE
genre Part de femmes % 27,6 −21,2 48,8 27,6
âge Âge moyen ans 41,0 −3 % 42,2 41,0
Part de jeunes (< 25 ans) % 10,6 +1,1 9,4 10,6
Part 50 ans et plus % 29,1 −3,8 32,9 29,1
Part 65 ans et plus % 0,2 −1,8 2,1 0,2
ancienneté Part ancienneté 5 ans et plus % 55,6 −1,4 57,0 55,6
QUALIFICATION
diplôme Part infra-bac % 39,3 +8,5 30,7 39,3
Part Bac+2 et plus % 33,5 −13,7 47,2 33,5
Part Bac+5 et plus % 7,3 −10,8 18,2 7,3
STRUCTURE DE L'EMPLOI
contrat Part CDD % 3,6 −4,0 7,6 3,6
durée Part temps partiel % 7,4 −10,1 17,6 7,4
qualité Part en sous-emploi % 2,9 −1,4 4,3 2,9
Coloration écart relatif · rose = cible au-dessus de la référence · bleu = cible en dessous de la référence
Intensité (5 niveaux) : ±5-15 % ±15-30 % ±30-50 % ±50-80 % ≥ 80 %  ·  volumes bruts non comparés (n.c.)
Source : INSEE EEC 2024 · pondération EXTRIAN · actifs occupés. Âge ventilé en tranches AGE6 (< 25 / 50+ / 65+) ; diplôme en gradient infra-bac → Bac+2 → Bac+5.

DéfinitionsPart en sous-emploi (INSEE) : salariés à temps partiel souhaitant travailler plus, ou salariés en chômage technique/partiel (sous-utilisation involontaire de la force de travail). Part emploi public : salariés sous statut public (fonction publique d’État, territoriale, hospitalière) parmi l’ensemble des actifs salariés du périmètre.

Lecture : ligne « Part de femmes » colonne secteur cœur = part des femmes dans l’ensemble des salariés du périmètre NAF en 2024. Cellules colorées = écart vs France (rouge = supérieur, bleu = inférieur). Source : INSEE EEC 2024 pondérée EXTRIAN, filtre actifs occupés.

Voir le code
# Liste codes ZE du territoire focus via TDC (réutilisé par srpt-07) + shapes ZE/région
ze_terr_focus <- tdc |>
  filter(DEP %in% params$territoire_codes) |>
  pull(ZE2020) |> unique() |> sort()

shp_ze <- sf::st_read(SHP_ZE_FILE, quiet = TRUE)
code_col <- intersect(c("code_insee", "code", "ZE2020", "ze2020"), names(shp_ze))[1]
shp_ze$code_ze <- sprintf("%04s", as.character(shp_ze[[code_col]]))
lib_col <- intersect(c("libze2020", "nom_officiel", "lib", "libelle"), names(shp_ze))[1]
shp_ze$lib_ze <- if (!is.na(lib_col)) shp_ze[[lib_col]] else shp_ze$code_ze
ze_terr_focus <- intersect(ze_terr_focus, shp_ze$code_ze)

shp_reg_path <- file.path(GEO, "nodom_region_2025.geojson")
shp_reg <- if (file.exists(shp_reg_path)) sf::st_read(shp_reg_path, quiet = TRUE) else NULL

Géographie nationale : où le secteur se concentre-t-il en France ?

Voir le code
# Agrégation sectorielle par maille via sector_geo_agg (helper canonique).
#   IS statique (lq_ind, centré 100)             ← FLORES total (privé + public, ref 2024)
#   Dynamique compétitive (lq_dyn_long, c.100)   ← URSSAF privé (shift-share, ref ann_ref)
#   Croissance (tcam_long / tcam_court)          ← URSSAF privé
.yrs    <- c(base_long = 2019, base_court = 2023, ref = ann_ref)
.fl_ref <- max(dat$flores_com$year, na.rm = TRUE)        # FLORES s'arrête à 2024
.yrs_fl <- c(base_long = 2019, base_court = 2023, ref = .fl_ref)

# URSSAF privé : dynamique compétitive + croissance (dep + ze)
ze_tab  <- sector_geo_agg(dat$urssaf, coeur, geo_col = "code_ze", years = .yrs)
dep_tab <- sector_geo_agg(dat$urssaf, coeur, geo_col = "DEP", years = .yrs,
                          tdc = tdc, tdc_key = "CODGEO")
# FLORES total : indice de spécialisation (dep + ze)
ze_tab_fl  <- sector_geo_agg(dat$flores_ze,  coeur, geo_col = "code_ze", years = .yrs_fl)
dep_tab_fl <- sector_geo_agg(dat$flores_com, coeur, geo_col = "DEP", years = .yrs_fl,
                             tdc = tdc, tdc_key = "CODGEO")

dep_data    <- shp_dep |> left_join(dep_tab,    by = c("code_dep" = "geo"))
ze_data     <- shp_ze  |> left_join(ze_tab,     by = c("code_ze"  = "geo"))
dep_data_fl <- shp_dep |> left_join(dep_tab_fl, by = c("code_dep" = "geo"))
ze_data_fl  <- shp_ze  |> left_join(ze_tab_fl,  by = c("code_ze"  = "geo"))

# Parts nationales de référence (indice 100) : URSSAF privé + FLORES total
part_nat    <- with(dplyr::filter(dat$urssaf, year == ann_ref),
                    sum(value[code_a88 %in% coeur], na.rm = TRUE) / sum(value, na.rm = TRUE) * 100)
part_nat_fl <- with(dplyr::filter(dat$flores_com, year == .fl_ref),
                    sum(value[code_a88 %in% coeur], na.rm = TRUE) / sum(value, na.rm = TRUE) * 100)

# Bins partagés dep ↔ ze par indicateur → légende identique gauche/droite (comparabilité)
.bins_is   <- make_bins_divergent(c(dep_data_fl$lq_ind,      ze_data_fl$lq_ind),      center = 100, palette = pal_div_bb_7)
.bins_dyn  <- make_bins_divergent(c(dep_data$lq_dyn_long,    ze_data$lq_dyn_long),    center = 100, palette = pal_div_bb_7)
.bins_tcl  <- auto_bins(c(dep_data$tcam_long,  ze_data$tcam_long)[is.finite(c(dep_data$tcam_long,  ze_data$tcam_long))],  var_name = "tcam_long")
.bins_tcc  <- auto_bins(c(dep_data$tcam_court, ze_data$tcam_court)[is.finite(c(dep_data$tcam_court, ze_data$tcam_court))], var_name = "tcam_court")

.zd <- ze_data_fl |> sf::st_drop_geometry() |> filter(!is.na(lq_ind))
n_ze_total <- nrow(.zd)
n_ze_sur   <- sum(.zd$lq_ind > 110); n_ze_sous <- sum(.zd$lq_ind < 90)
log_result("cartes_quantiles", list(part_nat = part_nat, part_nat_fl = part_nat_fl,
                                     n_sur = n_ze_sur, n_sous = n_ze_sous))

Le secteur de la logistique pèse 2,1 % de l’emploi privé (URSSAF) et 1,5 % de l’emploi total (FLORES, privé + public) en France : c’est la double référence (indice 100) face à laquelle se lit la spécialisation de chaque territoire. Quatre lectures, chacune en double maille — départements à gauche, zones d’emploi à droite : où le secteur pèse (spécialisation, emploi total), où il gagne des parts (dynamique compétitive), puis sa croissance sur deux horizons.

Spécialisation : quels territoires concentrent le secteur ?

Voir le code
.dd  <- dep_data_fl |> sf::st_drop_geometry() |> filter(!is.na(lq_ind))
.top <- .dd |> slice_max(lq_ind, n = 3); .bot <- .dd |> slice_min(lq_ind, n = 3)
n_dep_sur <- sum(.dd$lq_ind > 110); n_dep_sous <- sum(.dd$lq_ind < 90)
log_result("cartes_is", list(n_dep_sur = n_dep_sur, n_dep_sous = n_dep_sous,
                             dep_top = .top$lib_dep[1], dep_top_ind = round(.top$lq_ind[1])))
cat(glue::glue(
  "[Sur **{nrow(.dd)} départements**, **{n_dep_sur}** sont sur-spécialisés (indice&nbsp;>&nbsp;110) ",
  "et **{n_dep_sous}** sous-spécialisés (indice&nbsp;<&nbsp;90).]{{.insight}}\n\n",
  "La spécialisation se mesure ici sur l'**emploi total** (FLORES, privé + public), pour intégrer le poids du secteur public. ",
  "Les départements les plus spécialisés sont **{.top$lib_dep[1]}** (indice&nbsp;{round(.top$lq_ind[1])}), ",
  "**{.top$lib_dep[2]}** ({round(.top$lq_ind[2])}) et **{.top$lib_dep[3]}** ({round(.top$lq_ind[3])}) ; ",
  "à l'opposé, **{.bot$lib_dep[1]}** ({round(.bot$lq_ind[1])}) et **{.bot$lib_dep[2]}** ({round(.bot$lq_ind[2])}) ",
  "sont les moins dotés. L'indice est inscrit sur chaque département *(carte de gauche)* ; la maille zone d'emploi *(droite)* affine la lecture.\n"
))

Sur 96 départements, 23 sont sur-spécialisés (indice > 110) et 57 sous-spécialisés (indice < 90).

La spécialisation se mesure ici sur l’emploi total (FLORES, privé + public), pour intégrer le poids du secteur public. Les départements les plus spécialisés sont Seine-Maritime (indice 252), Seine-Saint-Denis (240) et Val-d’Oise (234) ; à l’opposé, Creuse (16) et Gers (17) sont les moins dotés. L’indice est inscrit sur chaque département (carte de gauche) ; la maille zone d’emploi (droite) affine la lecture.

Voir le code
build_map(dep_data_fl, "lq_ind", bins = .bins_is, interactive = FALSE,
  titre = "Spécialisation (IS) — départements", source_txt = NULL, label_col = "lib_dep",
  n_top = 4, n_bottom = 4, show_values = FALSE, label_position = "repel", label_style = "text",
  contour_geo = shp_reg, border_color = "#cccccc", border_lw = 0.2,
  legend_title = "indice") + th_map_sm

Voir le code
build_map(ze_data_fl, "lq_ind", bins = .bins_is, interactive = FALSE,
  titre = "Spécialisation (IS) — zones d'emploi", source_txt = NULL, label_col = "lib_ze",
  n_top = 4, n_bottom = 4, show_values = FALSE, label_position = "repel", label_style = "text",
  contour_geo = shp_reg, border_color = "#cccccc", border_lw = 0.15,
  legend_title = "indice") + th_map_sm

Source : INSEE FLORES · emploi total · Logistique · indice de spécialisation 2024 (France = 100)

Dynamique compétitive : où le secteur gagne-t-il des parts ?

Voir le code
.zt   <- ze_data |> sf::st_drop_geometry() |> filter(!is.na(lq_dyn_long))
.gain <- .zt |> slice_max(lq_dyn_long, n = 2); .perd <- .zt |> slice_min(lq_dyn_long, n = 2)
.ddc  <- dep_data |> sf::st_drop_geometry() |> filter(!is.na(lq_dyn_long))
n_dep_gain <- sum(.ddc$lq_dyn_long > 100, na.rm = TRUE)
log_result("cartes_dyncomp", list(n_ze_gain = sum(.zt$lq_dyn_long > 100), n_dep_gain = n_dep_gain,
                                   ze_gain = .gain$lib_ze[1], ze_perd = .perd$lib_ze[1]))
cat(glue::glue(
  "[Peser n'est pas accélérer : **{.gain$lib_ze[1]}** (indice&nbsp;{round(.gain$lq_dyn_long[1])}) gagne le plus de parts, ",
  "**{.perd$lib_ze[1]}** ({round(.perd$lq_dyn_long[1])}) en perd le plus.]{{.insight}}\n\n",
  "La dynamique compétitive rapporte la croissance locale du secteur à sa tendance nationale {2019L}-{ann_ref} ",
  "*(indice&nbsp;100&nbsp;=&nbsp;même rythme qu'au national ; au-dessus = gain de parts)*. ",
  "Sur les **{nrow(.ddc)} départements**, **{n_dep_gain}** progressent plus vite que la moyenne nationale du secteur *(carte de gauche)*.\n"
))

Peser n’est pas accélérer : Oloron-Sainte-Marie (indice 510) gagne le plus de parts, Corte (2) en perd le plus.

La dynamique compétitive rapporte la croissance locale du secteur à sa tendance nationale 2019-2025 (indice 100 = même rythme qu’au national ; au-dessus = gain de parts). Sur les 96 départements, 33 progressent plus vite que la moyenne nationale du secteur (carte de gauche).

Voir le code
build_map(dep_data, "lq_dyn_long", bins = .bins_dyn, interactive = FALSE,
  titre = "Dynamique compétitive — départements", source_txt = NULL, label_col = "lib_dep",
  n_top = 4, n_bottom = 4, show_values = FALSE, label_position = "repel", label_style = "text",
  contour_geo = shp_reg, border_color = "#cccccc", border_lw = 0.2,
  legend_title = "indice") + th_map_sm

Voir le code
build_map(ze_data, "lq_dyn_long", bins = .bins_dyn, interactive = FALSE,
  titre = "Dynamique compétitive — zones d'emploi", source_txt = NULL, label_col = "lib_ze",
  n_top = 4, n_bottom = 4, show_values = FALSE, label_position = "repel", label_style = "text",
  contour_geo = shp_reg, border_color = "#cccccc", border_lw = 0.15,
  legend_title = "indice") + th_map_sm

Source : URSSAF-ACOSS · privé salarié · Logistique · dynamique compétitive (shift-share) 2019-2025

Croissance de l’emploi : tendance longue et dynamique récente

Voir le code
.zt <- ze_data |> sf::st_drop_geometry() |> filter(!is.na(tcam_court))
.d_court <- geo_distrib(.zt, "tcam_court", "lib_ze", n = 5, z = 1.5, pivot = 0)
fr_tcam_court <- mean(.zt$tcam_court, na.rm = TRUE)
fr_tcam_long  <- mean(.zt$tcam_long,  na.rm = TRUE)
log_result("cartes_tcam", list(fr_long = fr_tcam_long, fr_court = fr_tcam_court))
cat(glue::glue(
  "La **croissance annualisée** brute (TCAM, URSSAF privé) se lit sur deux horizons : la **tendance longue 2019-{ann_ref}** ",
  "puis la **dynamique récente 2023-{ann_ref}**. À l'échelle nationale, le secteur progresse de ",
  "**{fmt_sign_1dec(fr_tcam_long)} % par an** sur la période longue, puis de **{fmt_sign_1dec(fr_tcam_court)} % par an** récemment.\n\n"
))

La croissance annualisée brute (TCAM, URSSAF privé) se lit sur deux horizons : la tendance longue 2019-2025 puis la dynamique récente 2023-2025. À l’échelle nationale, le secteur progresse de +1,4 % par an sur la période longue, puis de +2,8 % par an récemment.

Voir le code
cat(geo_distrib_phrase(.d_court, terr_lbl = "zones d'emploi",
                       fmt = function(x) paste0(fmt_sign_1dec(x), " %/an")))

Sur les 286 zones d’emploi, 167 progressent quand 114 reculent. En tête : L’Aigle (+400,0 %/an), Oloron-Sainte-Marie (+116,5 %/an), Rambouillet (+64,7 %/an), Château-Gontier-sur-Mayenne (+58,1 %/an), Manosque (+46,5 %/an) ; à l’opposé : Corte (-83,3 %/an), Guingamp (-77,0 %/an), Remiremont (-40,7 %/an), Béthune (-39,5 %/an), Saint-Dié-des-Vosges (-19,8 %/an). Statistiquement atypiques (écart > 1,5 σ) : L’Aigle (+400,0 %/an), Oloron-Sainte-Marie (+116,5 %/an), Rambouillet (+64,7 %/an) se détachent nettement au-dessus ; Corte (-83,3 %/an), Guingamp (-77,0 %/an), Remiremont (-40,7 %/an) décrochent franchement.

Voir le code
build_map(dep_data, "tcam_long", bins = .bins_tcl, interactive = FALSE,
  titre = sprintf("Croissance %d-%d — départements", 2019L, ann_ref), source_txt = NULL, label_col = "lib_dep",
  n_top = 4, n_bottom = 4, show_values = FALSE, label_position = "repel", label_style = "text",
  contour_geo = shp_reg, border_color = "#cccccc", border_lw = 0.2,
  legend_title = "%/an") + th_map_sm

Voir le code
build_map(ze_data, "tcam_long", bins = .bins_tcl, interactive = FALSE,
  titre = sprintf("Croissance %d-%d — zones d'emploi", 2019L, ann_ref), source_txt = NULL, label_col = "lib_ze",
  n_top = 4, n_bottom = 4, show_values = FALSE, label_position = "repel", label_style = "text",
  contour_geo = shp_reg, border_color = "#cccccc", border_lw = 0.15,
  legend_title = "%/an") + th_map_sm

Source : URSSAF-ACOSS · privé salarié · TCAM 2019-2025 · Logistique

Voir le code
build_map(dep_data, "tcam_court", bins = .bins_tcc, interactive = FALSE,
  titre = sprintf("Croissance %d-%d — départements", 2023L, ann_ref), source_txt = NULL, label_col = "lib_dep",
  n_top = 4, n_bottom = 4, show_values = FALSE, label_position = "repel", label_style = "text",
  contour_geo = shp_reg, border_color = "#cccccc", border_lw = 0.2,
  legend_title = "%/an") + th_map_sm

Voir le code
build_map(ze_data, "tcam_court", bins = .bins_tcc, interactive = FALSE,
  titre = sprintf("Croissance %d-%d — zones d'emploi", 2023L, ann_ref), source_txt = NULL, label_col = "lib_ze",
  n_top = 4, n_bottom = 4, show_values = FALSE, label_position = "repel", label_style = "text",
  contour_geo = shp_reg, border_color = "#cccccc", border_lw = 0.15,
  legend_title = "%/an") + th_map_sm

Source : URSSAF-ACOSS · privé salarié · TCAM 2023-2025 · Logistique

Vue territoriale : Hauts-de-France

Vue d’ensemble : le secteur dans Hauts-de-France

Voir le code
# Agrégat régional URSSAF privé (focus = territoire_codes) — volume, part, dynamique
.us_reg <- dat$urssaf |>
  filter(measure == "EFF_SAL", year %in% c(2019L, p_evol[1], ann_ref)) |>
  left_join(tdc |> select(CODGEO, DEP), by = c("code_geo" = "CODGEO")) |>
  filter(DEP %in% params$territoire_codes)
.rsec <- .us_reg |> filter(code_a88 %in% coeur) |> group_by(year) |> summarise(e = sum(value, na.rm = TRUE), .groups = "drop")
.rtot <- .us_reg |> group_by(year) |> summarise(e = sum(value, na.rm = TRUE), .groups = "drop")
reg_vol_ref  <- .rsec$e[.rsec$year == ann_ref]
reg_part     <- reg_vol_ref / .rtot$e[.rtot$year == ann_ref] * 100
reg_tcam_rec <- ((reg_vol_ref / .rsec$e[.rsec$year == p_evol[1]])^(1/(ann_ref - p_evol[1])) - 1) * 100
reg_tcam_lng <- ((reg_vol_ref / .rsec$e[.rsec$year == 2019L])^(1/(ann_ref - 2019L)) - 1) * 100
log_result("terr_overview", list(reg_vol = reg_vol_ref, reg_part = reg_part,
                                  reg_tcam_rec = reg_tcam_rec, reg_tcam_lng = reg_tcam_lng))
Voir le code
htmltools::tagList(
  kpi_row(wide = TRUE,
    kpi_band(category = "Volume régional", value = fmt_k(reg_vol_ref),
             ref = sprintf("soit <strong>%s</strong> de l'emploi privé régional", fmt_pct_1dec(reg_part))),
    kpi_band(category = sprintf("Dynamique récente %d-%d", p_evol[1], ann_ref),
             value = sprintf("%s %%/an", fmt_sign_1dec(reg_tcam_rec)),
             ref = sprintf("vs <strong>%s %%/an</strong> national", fmt_sign_1dec(tcam_us))),
    kpi_band(category = sprintf("Tendance longue %d-%d", 2019L, ann_ref),
             value = sprintf("%s %%/an", fmt_sign_1dec(reg_tcam_lng)),
             ref = "effectifs salariés privés")
  ),
  kpi_source(sprintf("URSSAF-ACOSS · %s · %d", params$territoire_focus, ann_ref))
)
Volume régional
44 k soit 2,9 % de l'emploi privé régional
Dynamique récente 2023-2025
+2,5 %/an vs +1,8 %/an national
Tendance longue 2019-2025
+3,2 %/an effectifs salariés privés
URSSAF-ACOSS · Hauts-de-France · 2025
Voir le code
.spec_reg <- fr_ecart_qualif(reg_part, part_secteur, mode = "rel")
.dyn_reg  <- fr_ecart_qualif(reg_tcam_rec, tcam_us, mode = "rel")
cat(glue::glue(
  "En **{params$territoire_focus}**, {sect_phrase} emploie **{fmt_k(reg_vol_ref)} salariés privés**, ",
  "soit **{fmt_pct_1dec(reg_part)} de l'emploi privé régional** — un poids {.spec_reg} la moyenne nationale ",
  "(**{fmt_pct_1dec(part_secteur)}**). Sa dynamique récente ({fmt_sign_1dec(reg_tcam_rec)} % par an) est {.dyn_reg} ",
  "le rythme national ({fmt_sign_1dec(tcam_us)} % par an). La suite décompose ce poids par département, zone d'emploi et commune."
))

En Hauts-de-France, le secteur de la logistique emploie 44 k salariés privés, soit 2,9 % de l’emploi privé régional — un poids nettement au-dessus du la moyenne nationale (2,1 %). Sa dynamique récente (+2,5 % par an) est nettement au-dessus du le rythme national (+1,8 % par an). La suite décompose ce poids par département, zone d’emploi et commune.

Départements : quelles dynamiques infra-régionales ?

Voir le code
# Calculs par DEP pour le territoire focus
dep_focus <- params$territoire_codes
# Libellés DEP dynamiques depuis TDC (champ DEPlib) — réplicable
dep_libs <- tdc |>
  filter(DEP %in% dep_focus) |>
  distinct(DEP, DEPlib) |>
  tibble::deframe()

# URSSAF : emp SAP + emp total par DEP × année
us_dep <- dat$urssaf |>
  filter(measure == "EFF_SAL", year %in% c(ann_base, p_evol[1], ann_ref)) |>
  left_join(tdc |> select(CODGEO, DEP), by = c("code_geo" = "CODGEO")) |>
  filter(DEP %in% dep_focus)

# Volume SAP par DEP × année
emp_sap_dep <- us_dep |>
  filter(code_a88 %in% coeur) |>
  group_by(DEP, year) |>
  summarise(emp = sum(value, na.rm = TRUE), .groups = "drop")

# Volume tot privé par DEP × année (référence pour part SAP locale)
emp_tot_dep <- us_dep |>
  group_by(DEP, year) |>
  summarise(emp_tot = sum(value, na.rm = TRUE), .groups = "drop")

# Wide pivot
sap_wide <- emp_sap_dep |>
  pivot_wider(names_from = year, values_from = emp, names_prefix = "y") |>
  rename_with(~ paste0("emp_", sub("y", "", .x)), starts_with("y")) |>
  left_join(
    emp_tot_dep |> filter(year == ann_ref) |> select(DEP, emp_tot_24 = emp_tot),
    by = "DEP"
  ) |>
  mutate(
    libelle = dep_libs[DEP],
    part_sap_pct = .data[[paste0("emp_", ann_ref)]] / emp_tot_24 * 100,
    tcam_1924 = ((.data[[paste0("emp_", ann_ref)]] /
                  .data[[paste0("emp_", p_evol[1])]])^(1 / (p_evol[2] - p_evol[1])) - 1) * 100,
    tcam_1424 = ((.data[[paste0("emp_", ann_ref)]] /
                  .data[[paste0("emp_", ann_base)]])^(1 / (ann_ref - ann_base)) - 1) * 100
  )

# IS = (part_sap_dep / part_sap_fr)
sap_wide$is_sap <- sap_wide$part_sap_pct / part_secteur * 100

# Profil EEC pondéré par effectif DEP (proxy : on prend le profil FR car EEC pas dispo DEP)
# Pour V1 on utilise MOBZELT au niveau ZE puis remonté en DEP via TDC (approximation)
mobz_dep <- dat$mobz_ze |>
  left_join(tdc |> select(ZE2020, DEP) |> distinct(),
            by = c("code_ze" = "ZE2020")) |>
  filter(DEP %in% dep_focus, code_naf88 %in% coeur) |>
  group_by(DEP) |>
  summarise(
    lab_fem_pct = weighted.mean(lab_fem_pct, lab_eff_vol, na.rm = TRUE),
    lab_50plus_pct = weighted.mean(lab_50plus_pct, lab_eff_vol, na.rm = TRUE),
    .groups = "drop"
  )

sap_dep_full <- sap_wide |>
  left_join(mobz_dep, by = "DEP") |>
  arrange(desc(.data[[paste0("emp_", ann_ref)]]))

log_result("decompo_dep_ref", sap_dep_full |>
             select(DEP, libelle, all_of(paste0("emp_", ann_ref)),
                    part_sap_pct, tcam_1924, is_sap, lab_fem_pct, lab_50plus_pct))
Voir le code
top_dep <- sap_dep_full[1, ]
bot_dep <- sap_dep_full[nrow(sap_dep_full), ]
ratio_top_bot <- top_dep[[paste0("emp_", ann_ref)]] / bot_dep[[paste0("emp_", ann_ref)]]

# Articles définis FR (fr_article jcn-prose) : "l'Ille-et-Vilaine", "les Côtes-d'Armor", "le Morbihan"
.cap1   <- function(s) paste0(toupper(substr(s, 1, 1)), substring(s, 2))
top_art <- .cap1(paste0(fr_article(top_dep$libelle), top_dep$libelle))   # début d'insight → majuscule
bot_art <- paste0(fr_article(bot_dep$libelle), bot_dep$libelle)          # milieu de phrase → minuscule

cat(glue::glue(
  "[**{top_art}** concentre **{fmt_k(top_dep[[paste0('emp_', ann_ref)]])} emplois** du secteur cœur, ",
  "soit **{round(ratio_top_bot, 1)}×** plus que **{bot_art}** ",
  "({fmt_k(bot_dep[[paste0('emp_', ann_ref)]])}).]{{.insight}}\n\n",
  "L'indice de spécialisation sectoriel (IS = part DEP / part FR × 100) varie de ",
  "**{round(min(sap_dep_full$is_sap), 0)}** à **{round(max(sap_dep_full$is_sap), 0)}** entre les ",
  "{nrow(sap_dep_full)} départements de {params$territoire_focus}, ",
  "signe d'une **{ifelse(max(sap_dep_full$is_sap) - min(sap_dep_full$is_sap) > 20, 'hétérogénéité marquée', 'cohésion forte')}** ",
  "du tissu sectoriel. La dynamique récente {p_evol[1]}-{p_evol[2]} oscille entre ",
  "**{fmt_sign(min(sap_dep_full$tcam_1924))} % par an** et **{fmt_sign(max(sap_dep_full$tcam_1924))} % par an** selon les départements."
))

Le Nord concentre 20 k emplois du secteur cœur, soit 9.9× plus que l’Aisne (2 k).

L’indice de spécialisation sectoriel (IS = part DEP / part FR × 100) varie de 99 à 228 entre les 5 départements de Hauts-de-France, signe d’une hétérogénéité marquée du tissu sectoriel. La dynamique récente 2023-2025 oscille entre -4,61 % par an et +10,32 % par an selon les départements.

Voir le code
# Tableau dept avec rt_table + helpers jcn-reactable (pattern RT-FILL bars élastiques)
# Colonnes génériques (réplicable autre secteur : SAP, tourisme, etc.)
tab_dep <- sap_dep_full |>
  transmute(
    libelle = libelle,
    emp_sect = .data[[paste0("emp_", ann_ref)]],
    is_sect = is_sap,
    tcam_pevol_pct = tcam_1924,
    tcam_full_pct = tcam_1424,
    fem_pct = lab_fem_pct,
    p50_pct = lab_50plus_pct
  )

# Logging AVANT widget (règle log_*() avant la dernière expression)
log_table_auto("decompo_dep_table", tab_dep, label_col = "libelle")

rt_table(
  data = tab_dep,
  cols = list(
    libelle        = rt_col_lib(tab_dep, "libelle", "Département",
                                indent = FALSE, mw = 160),
    emp_sect       = rt_col_level(tab_dep, "emp_sect",
                                   sprintf("Emp. %d", ann_ref), unit = "n",
                                   bw = "fill", bh = 16, mw = 150),
    is_sect        = rt_col_idx(tab_dep, "is_sect", "IS", unit = "FR=100",
                                 bw = 80, bh = 16, mw = 130),
    tcam_pevol_pct = rt_col_var(tab_dep, "tcam_pevol_pct",
                                 sprintf("TCAM %d-%d", p_evol[1], p_evol[2]),
                                 unit = "%/an",
                                 bw = "fill", bh = 16, mw = 150),
    tcam_full_pct  = rt_col_var(tab_dep, "tcam_full_pct",
                                 sprintf("TCAM %d-%d", ann_base, ann_ref),
                                 unit = "%/an",
                                 bw = "fill", bh = 16, mw = 150),
    fem_pct        = rt_col_level(tab_dep, "fem_pct", "% Femmes", unit = "%",
                                   bw = "fill", bh = 16, mw = 130),
    p50_pct        = rt_col_level(tab_dep, "p50_pct", "% 50+", unit = "%",
                                   bw = "fill", bh = 16, mw = 130)
  ),
  title = sprintf("Décomposition %s par département — volume, spécialisation, dynamique",
                  params$territoire_focus),
  subtitle = sprintf("Effectifs %s en %d · IS = part DEP / part FR × 100 · profil démographique MOBZELT 2022",
                     sect_lbl, ann_ref),
  searchable = FALSE,
  full_width = TRUE,
  footnote = "Sources : URSSAF-ACOSS (volume, spécialisation, TCAM) · INSEE MOBZELT 2022 remonté ZE→DEP (profil démographique). EEC indisponible au niveau DEP."
)
Décomposition Hauts-de-France par département — volume, spécialisation, dynamique
Effectifs Logistique en 2025 · IS = part DEP / part FR × 100 · profil démographique MOBZELT 2022
⇅ triable · survol
Sources : URSSAF-ACOSS (volume, spécialisation, TCAM) · INSEE MOBZELT 2022 remonté ZE→DEP (profil démographique). EEC indisponible au niveau DEP.

Lecture : barre bleue = volume relatif au plus grand DEP. IS > 110 = département sur-spécialisé, IS < 90 = sous-spécialisé (référence France = 100). TCAM = taux de croissance annuel moyen. Sources : URSSAF privé (volume, IS, TCAM) · INSEE MOBZELT 2022 remonté ZE→DEP (profil démographique).

Voir le code
# Small multiples modernisés via plot_line_facet (jcn-graph-line) : indice base 100 par DEP
# + référence secteur France tracée dans chaque panneau (pointillé).
us_all <- dat$urssaf |> filter(year >= ann_base)

# Série par DEP focus — emp brut (le helper calcule l'indice base 100 = première année)
us_dep_long <- us_all |>
  filter(code_a88 %in% coeur) |>
  left_join(tdc |> select(CODGEO, DEP), by = c("code_geo" = "CODGEO")) |>
  filter(DEP %in% dep_focus) |>
  group_by(DEP, year) |>
  summarise(emp = sum(value, na.rm = TRUE), .groups = "drop") |>
  mutate(lib = paste0(DEP, " · ", dep_libs[DEP]))

# Référence secteur France (indice base 100 = ann_base) → col .fref attendue par plot_line_facet
fref_sect <- us_all |>
  filter(code_a88 %in% coeur) |>
  group_by(year) |> summarise(emp = sum(value, na.rm = TRUE), .groups = "drop") |>
  arrange(year) |> mutate(.fref = emp / emp[year == ann_base] * 100) |>
  select(year, .fref)
# Référence emploi privé tous secteurs (loggée pour narration)
fref_tot <- us_all |>
  group_by(year) |> summarise(emp = sum(value, na.rm = TRUE), .groups = "drop") |>
  arrange(year) |> mutate(ind = emp / emp[year == ann_base] * 100)
log_result("smallmultiples_fr_refs", list(
  sect_fr_indref = fref_sect$.fref[fref_sect$year == ann_ref],
  tot_fr_indref  = fref_tot$ind[fref_tot$year == ann_ref]))

plot_line_facet(
  us_dep_long, x = "year", y = "emp", facet_col = "lib",
  mode = "indice100", scales = "fixed", points = TRUE, label_points = "ends",
  x_breaks = seq(ann_base, ann_ref, 2),
  france_ref = fref_sect, france_lab = "Secteur France",
  title    = sprintf("Emploi sectoriel par département · indice base 100 = %d", ann_base),
  subtitle = sprintf("Départements de %s (plein) · référence secteur France (pointillé)",
                     params$territoire_focus),
  y_lab = "Indice", caption = make_source(sprintf("URSSAF-ACOSS · %s", sect_lbl)))

Récap territorial : quel poids du secteur dans chaque ZE de Hauts-de-France ?

Voir le code
# Données URSSAF par commune × NA88 × année pour le focus territoire
# Filtre : communes appartenant aux DEP du territoire focus (via TDC)

# Agrégation par ZE × année (cœur + total) — sur communes région focus
us_reg <- dat$urssaf |>
  filter(measure == "EFF_SAL",
         year %in% c(p_evol[1], p_evol[2], ann_base)) |>
  left_join(tdc |> select(CODGEO, DEP, ZE2020) |> distinct(),
            by = c("code_geo" = "CODGEO")) |>
  filter(DEP %in% params$territoire_codes,
         ZE2020 %in% ze_terr_focus)

# Volume cœur secteur par ZE × année
emp_sect_ze <- us_reg |>
  filter(code_a88 %in% coeur) |>
  group_by(ZE2020, year) |>
  summarise(emp = sum(value, na.rm = TRUE), .groups = "drop")

# Volume total privé par ZE × année (pour part secteur locale)
emp_tot_ze <- us_reg |>
  group_by(ZE2020, year) |>
  summarise(emp_tot = sum(value, na.rm = TRUE), .groups = "drop")

# Pivot wide + indicateurs
sect_ze_wide <- emp_sect_ze |>
  pivot_wider(names_from = year, values_from = emp, names_prefix = "y") |>
  left_join(
    emp_tot_ze |> filter(year == p_evol[2]) |> select(ZE2020, emp_tot_ref = emp_tot),
    by = "ZE2020"
  ) |>
  mutate(
    code_ze       = sprintf("%04s", as.character(ZE2020)),
    part_sect_pct = .data[[paste0("y", p_evol[2])]] / emp_tot_ref * 100,
    evol_2periode = ((.data[[paste0("y", p_evol[2])]] /
                      .data[[paste0("y", p_evol[1])]]) - 1) * 100,
    evol_long     = ((.data[[paste0("y", p_evol[2])]] /
                      .data[[paste0("y", ann_base)]]) - 1) * 100
  ) |>
  left_join(
    shp_ze |> sf::st_drop_geometry() |> select(code_ze, lib_ze),
    by = "code_ze"
  ) |>
  arrange(desc(.data[[paste0("y", p_evol[2])]]))

# IS = part_locale / part_FR × 100 (réutilise part_secteur calculé en 1.1)
sect_ze_wide$is_sect <- sect_ze_wide$part_sect_pct / part_secteur * 100

log_table_auto("recap_region_ze", sect_ze_wide, label_col = "lib_ze")
Voir le code
# Carte zoom région focus : ZE colorées par part secteur, contour régional
shp_ze_focus <- shp_ze |>
  filter(code_ze %in% ze_terr_focus) |>
  left_join(sect_ze_wide |> select(code_ze, part_sect_pct, is_sect,
                                    evol_2periode, evol_long),
            by = "code_ze")

build_map(shp_ze_focus, "part_sect_pct",
  bin_type = "level", interactive = FALSE,
  titre = sprintf("Part %s dans l'emploi local · zoom %s",
                  params$secteur_short %||% "secteur", params$territoire_focus),
  source_txt = NULL,
  label_col = "lib_ze", n_top = 3, n_bottom = 3,
  show_values = TRUE, label_position = "repel",
  label_fill_sign = FALSE, label_style = "text",
  border_color = "#cccccc", border_lw = 0.15,
  legend_title = "%",
  bin_args = list(suffix = " %", palette = pal_seq7_byrv, n_mid = 5)) +
  th_map_sm

Sur les 22 zones d’emploi de Hauts-de-France, le secteur de la logistique affiche un poids hétérogène : la part dans l’emploi local varie de 0.2 % à 8.1 % (figure ci-contre).

L’indice de spécialisation s’étend de 12 à 393 (référence France = 100), signe d’une hétérogénéité marquée du tissu sectoriel dans la région. Les zones les plus spécialisées concentrent à la fois volume et densité d’emploi sectoriel.

Dynamique 2 périodes : où le secteur progresse-t-il dans la région ?

Voir le code
build_map(shp_ze_focus, "evol_2periode", bin_type = "level", interactive = FALSE,
  titre = sprintf("Évolution %d-%d", p_evol[1], p_evol[2]), source_txt = NULL,
  label_col = "lib_ze", n_top = 3, n_bottom = 3, label_size = 3.2,
  show_values = TRUE, label_position = "repel", label_fill_sign = FALSE, label_style = "text",
  border_color = "#cccccc", border_lw = 0.15, legend_title = "%",
  bin_args = list(suffix = " %", palette = pal_seq7_byrv, n_mid = 5)) + th_map_sm

Voir le code
build_map(shp_ze_focus, "evol_long", bin_type = "level", interactive = FALSE,
  titre = sprintf("Évolution %d-%d", ann_base, p_evol[2]), source_txt = NULL,
  label_col = "lib_ze", n_top = 3, n_bottom = 3, label_size = 3.2,
  show_values = TRUE, label_position = "repel", label_fill_sign = FALSE, label_style = "text",
  border_color = "#cccccc", border_lw = 0.15, legend_title = "%",
  bin_args = list(suffix = " %", palette = pal_seq7_byrv, n_mid = 5)) + th_map_sm

Source : URSSAF-ACOSS · privé salarié · Logistique

Voir le code
# Tableau récap par ZE — rt_table avec barres élastiques (body width)
tab_ze <- sect_ze_wide |>
  transmute(
    libelle       = lib_ze,
    emp_sect      = .data[[paste0("y", p_evol[2])]],
    part_sect_pct = part_sect_pct,
    is_sect       = is_sect,
    evol_pevol    = evol_2periode,
    evol_long     = evol_long
  )

log_table_auto("recap_region_table", tab_ze, label_col = "libelle")

rt_table(
  data = tab_ze,
  cols = list(
    libelle        = rt_col_lib(tab_ze, "libelle", "Zone d'emploi",
                                 indent = FALSE, mw = 180),
    emp_sect       = rt_col_level(tab_ze, "emp_sect",
                                   sprintf("Emp. %d", p_evol[2]), unit = "n",
                                   bw = "fill", bh = 16, mw = 180),
    part_sect_pct  = rt_col_level(tab_ze, "part_sect_pct",
                                   "Part secteur", unit = "%",
                                   bw = "fill", bh = 16, mw = 180),
    is_sect        = rt_col_idx(tab_ze, "is_sect", "IS", unit = "FR=100",
                                 bw = 80, bh = 16, mw = 130),
    evol_pevol     = rt_col_var(tab_ze, "evol_pevol",
                                 sprintf("Évol %d-%d", p_evol[1], p_evol[2]),
                                 unit = "%",
                                 bw = "fill", bh = 16, mw = 180),
    evol_long      = rt_col_var(tab_ze, "evol_long",
                                 sprintf("Évol %d-%d", ann_base, p_evol[2]),
                                 unit = "%",
                                 bw = "fill", bh = 16, mw = 180)
  ),
  title = sprintf("Récap territorial — %d zones d'emploi de %s",
                  nrow(tab_ze), params$territoire_focus),
  subtitle = sprintf("Effectifs secteur cœur (NA88 %s) · IS = part ZE / part FR × 100 · évolutions URSSAF privé",
                     paste(coeur, collapse = "+")),
  searchable = TRUE,
  full_width = TRUE,
  footnote = "Sources : URSSAF-ACOSS (volume, part, IS, évolutions) · TDC INSEE (mapping commune → ZE2020) · périmètre filtré sur les communes des départements du territoire focus."
)
Récap territorial — 22 zones d'emploi de Hauts-de-France
Effectifs secteur cœur (NA88 52) · IS = part ZE / part FR × 100 · évolutions URSSAF privé
⇅ triable · survol
Sources : URSSAF-ACOSS (volume, part, IS, évolutions) · TDC INSEE (mapping commune → ZE2020) · périmètre filtré sur les communes des départements du territoire focus.

Lecture : barre bleue = volume relatif à la plus grande ZE de la région. IS > 110 = ZE sur-spécialisée par rapport à la France ; IS < 90 = sous-spécialisée. Les évolutions sont des variations totales sur la période (pas des TCAM). Sources : URSSAF privé via TDC commune vers ZE.

Vue communale : quelles communes portent le secteur en Hauts-de-France ?

Voir le code
# Données URSSAF par commune pour le focus territoire (communes >5k habitants)
SEUIL_POP <- 5000

# TDC enrichi : commune > seuil pop, DEP focus + coords lat/lon
tdc_comm_focus <- tdc |>
  filter(DEP %in% params$territoire_codes,
         TYPECOM == "COM" | is.na(TYPECOM),
         P22_POP >= SEUIL_POP) |>
  select(CODGEO, LIBGEO, DEP, DEPlib, ZE2020, P22_POP, lat, lon) |>
  mutate(lat = as.numeric(lat), lon = as.numeric(lon)) |>
  filter(!is.na(lat), !is.na(lon))

# Données URSSAF par commune × année (sur communes >5k de la région focus)
us_comm <- dat$urssaf |>
  filter(measure == "EFF_SAL",
         year %in% c(p_evol[1], p_evol[2], ann_base),
         code_geo %in% tdc_comm_focus$CODGEO)

# Volume cœur par commune × année
emp_sect_comm <- us_comm |>
  filter(code_a88 %in% coeur) |>
  group_by(code_geo, year) |>
  summarise(emp = sum(value, na.rm = TRUE), .groups = "drop")

# Volume tot privé par commune × année
emp_tot_comm <- us_comm |>
  group_by(code_geo, year) |>
  summarise(emp_tot = sum(value, na.rm = TRUE), .groups = "drop")

# Pivot wide + indicateurs
sect_comm_wide <- emp_sect_comm |>
  pivot_wider(names_from = year, values_from = emp, names_prefix = "y") |>
  left_join(
    emp_tot_comm |> filter(year == p_evol[2]) |> select(code_geo, emp_tot_ref = emp_tot),
    by = "code_geo"
  ) |>
  left_join(tdc_comm_focus, by = c("code_geo" = "CODGEO")) |>
  mutate(
    part_sect_pct = .data[[paste0("y", p_evol[2])]] / emp_tot_ref * 100,
    evol_pevol    = ((.data[[paste0("y", p_evol[2])]] /
                      .data[[paste0("y", p_evol[1])]]) - 1) * 100,
    evol_long     = ((.data[[paste0("y", p_evol[2])]] /
                      .data[[paste0("y", ann_base)]]) - 1) * 100,
    is_sect       = part_sect_pct / part_secteur * 100
  ) |>
  filter(!is.na(part_sect_pct), !is.infinite(part_sect_pct)) |>
  arrange(desc(.data[[paste0("y", p_evol[2])]]))

# Convertir en sf POINT pour build_map_points
sect_comm_sf <- sect_comm_wide |>
  filter(!is.na(lat), !is.na(lon)) |>
  sf::st_as_sf(coords = c("lon", "lat"), crs = 4326)

log_table_auto("commune_recap_table", sect_comm_wide, label_col = "LIBGEO")

La vue communale complète la lecture par zone d’emploi en isolant les communes ≥ 5 000 habitants des 5 départements du focus. Sur les 156 communes ainsi retenues, la part la logistique dans l’emploi local varie de 0.0 % à 40.8 %.

Voir le code
build_map_points(sect_comm_sf, "part_sect_pct",
  size_col = "P22_POP", label_col = "LIBGEO",
  label_style = "text",
  n_top = 3, n_bottom = 3,
  bg_geo = shp_ze_focus,
  contour_geo = shp_ze_focus, contour_color = "#bbbbbb", contour_lw = 0.3,
  bin_type = "level", interactive = FALSE,
  titre = sprintf("Part %s par commune · zoom %s",
                  params$secteur_short %||% "secteur", params$territoire_focus),
  subtitle = sprintf("Communes ≥ %s habitants (RP %d) · part secteur cœur sur emploi privé URSSAF %d",
                     format(SEUIL_POP, big.mark = " "), 2022, p_evol[2]),
  source_txt = NULL,
  legend_title = "%",
  size_range = c(2, 14),
  bin_args = list(suffix = " %", palette = pal_seq7_byrv, n_mid = 5))

Les communes en sur-spécialisation (IS supérieur à 110) cumulent volume d’emploi et densité sectorielle locale (figure ci-contre). La taille des bulles reflète la population 2022, la couleur la part du secteur dans l’emploi communal.

À l’opposé, les communes les moins spécialisées (IS < 90, en bleu) correspondent typiquement à des villes industrielles ou résidentielles où le secteur étudié reste marginal.

Dynamique 2 périodes : quelles communes accélèrent ou décrochent ?

Voir le code
build_map_points(sect_comm_sf, "evol_pevol", size_col = "P22_POP", label_col = "LIBGEO",
  label_style = "text", n_top = 4, n_bottom = 4,
  bg_geo = shp_ze_focus, contour_geo = shp_ze_focus, contour_color = "#bbbbbb", contour_lw = 0.3,
  bin_type = "divergent", interactive = FALSE,
  titre = sprintf("Évolution %d-%d", p_evol[1], p_evol[2]), source_txt = NULL,
  legend_title = "%", size_range = c(2, 11),
  bin_args = list(center = 0, suffix = " %", palette = pal_div_bb_7))

Voir le code
build_map_points(sect_comm_sf, "evol_long", size_col = "P22_POP", label_col = "LIBGEO",
  label_style = "text", n_top = 4, n_bottom = 4,
  bg_geo = shp_ze_focus, contour_geo = shp_ze_focus, contour_color = "#bbbbbb", contour_lw = 0.3,
  bin_type = "divergent", interactive = FALSE,
  titre = sprintf("Évolution %d-%d", ann_base, p_evol[2]), source_txt = NULL,
  legend_title = "%", size_range = c(2, 11),
  bin_args = list(center = 0, suffix = " %", palette = pal_div_bb_7))

Source : URSSAF-ACOSS · privé salarié · Logistique · communes ≥ 5 000 hab.

Voir le code
# Tableau récap par commune — top par volume (rt_table avec barres)
tab_comm <- sect_comm_wide |>
  transmute(
    libelle       = LIBGEO,
    departement   = DEPlib,
    pop           = P22_POP,
    emp_sect      = .data[[paste0("y", p_evol[2])]],
    part_sect_pct = part_sect_pct,
    is_sect       = is_sect,
    evol_pevol    = evol_pevol,
    evol_long     = evol_long
  ) |>
  slice_head(n = 30)  # Top 30 communes par effectif secteur

rt_table(
  data = tab_comm,
  cols = list(
    libelle        = rt_col_lib(tab_comm, "libelle", "Commune",
                                 indent = FALSE, mw = 160),
    departement    = colDef(name = "DEP", maxWidth = 130,
                            style = list(fontSize = "11px", color = "#666")),
    pop            = rt_col_level(tab_comm, "pop", "Pop. 2022", unit = "hab",
                                   bw = "fill", bh = 16, mw = 150),
    emp_sect       = rt_col_level(tab_comm, "emp_sect",
                                   sprintf("Emp. %d", p_evol[2]), unit = "n",
                                   bw = "fill", bh = 16, mw = 150),
    part_sect_pct  = rt_col_level(tab_comm, "part_sect_pct",
                                   "Part secteur", unit = "%",
                                   bw = "fill", bh = 16, mw = 150),
    is_sect        = rt_col_idx(tab_comm, "is_sect", "IS", unit = "FR=100",
                                 bw = 80, bh = 16, mw = 130),
    evol_pevol     = rt_col_var(tab_comm, "evol_pevol",
                                 sprintf("Évol %d-%d", p_evol[1], p_evol[2]),
                                 unit = "%",
                                 bw = "fill", bh = 16, mw = 150),
    evol_long      = rt_col_var(tab_comm, "evol_long",
                                 sprintf("Évol %d-%d", ann_base, p_evol[2]),
                                 unit = "%",
                                 bw = "fill", bh = 16, mw = 150)
  ),
  title = sprintf("Communes >5k habitants de %s — top %d par volume sectoriel",
                  params$territoire_focus, nrow(tab_comm)),
  subtitle = sprintf("Effectifs secteur cœur (NA88 %s) en %d · IS = part commune / part FR × 100 · évolutions URSSAF privé",
                     paste(coeur, collapse = "+"), p_evol[2]),
  searchable = TRUE,
  full_width = TRUE,
  footnote = sprintf("Sources : URSSAF-ACOSS (volume, part, IS, évolutions) · INSEE RP %d (population) · TDC commune. Périmètre filtré aux communes ≥ %s habitants.",
                    2022, format(SEUIL_POP, big.mark = " "))
)
Communes >5k habitants de Hauts-de-France — top 30 par volume sectoriel
Effectifs secteur cœur (NA88 52) en 2025 · IS = part commune / part FR × 100 · évolutions URSSAF privé
⇅ triable · survol
Sources : URSSAF-ACOSS (volume, part, IS, évolutions) · INSEE RP 2022 (population) · TDC commune. Périmètre filtré aux communes ≥ 5 000 habitants.

Lecture : barre = volume relatif à la plus grande commune affichée. IS > 110 = commune sur-spécialisée par rapport à la France ; IS < 90 = sous-spécialisée. Filtre population : seules les communes ≥ 5 000 habitants en 2022 sont affichées, ce qui réduit le bruit statistique sur les très petites communes. Tri par volume décroissant, top 30.

Voir le code
# Nombre de ZE du focus (le poids/dynamique régionaux viennent des AGRÉGATS reg_part /
# reg_tcam_rec calculés en 06 — PAS d'une moyenne de ratios par ZE qui surpondère les petites ZE).
terr_n_ze <- ze_data |> sf::st_drop_geometry() |>
  filter(code_ze %in% ze_terr_focus, !is.na(part)) |> nrow()
Voir le code
# Briques GÉNÉRIQUES de la synthèse (aucune valeur sector-specific en dur) :
#   (a) contrastes internes entre divisions NAF cœur · (b) poids du public · (c) qualité d'emploi.
.naf_prof  <- eec_individuels |> filter(label %in% naf_lbls)
.multi_naf <- nrow(.naf_prof) >= 2
if (.multi_naf) {
  .pub_max <- .naf_prof |> slice_max(lab_pub_pct, n = 1); .pub_min <- .naf_prof |> slice_min(lab_pub_pct, n = 1)
  .fem_max <- .naf_prof |> slice_max(lab_fem_pct, n = 1); .fem_min <- .naf_prof |> slice_min(lab_fem_pct, n = 1)
}

# Poids du secteur public = part de l'emploi total (FLORES) hors champ privé (URSSAF), millésime FLORES commun
.us_flref    <- sum(dat$urssaf$value[dat$urssaf$code_a88 %in% coeur & dat$urssaf$year == .fl_ref], na.rm = TRUE)
.fl_flref    <- sum(dat$flores_com$value[dat$flores_com$code_a88 %in% coeur & dat$flores_com$year == .fl_ref], na.rm = TRUE)
public_share <- if (is.finite(.fl_flref) && .fl_flref > 0) max(0, (1 - .us_flref / .fl_flref) * 100) else NA_real_

# Qualité d'emploi : compte de marqueurs nettement au-dessus de la moyenne nationale
.n_fragile <- sum(c(delta_qualif$tpart > 2, delta_qualif$cdd > 1, delta_qualif$souempl > 0.5))
.q_verdict <- if (.n_fragile >= 2) {
  "Ces marqueurs, plus marqués que dans le reste de l'économie, signalent des conditions d'emploi plus fragiles."
} else {
  "Le secteur reste globalement dans la moyenne nationale sur ces dimensions."
}

log_result("synth_generic", list(public_share = round(public_share, 1), n_fragile = .n_fragile,
  contrast = if (.multi_naf) sprintf("pub %s vs %s · fem %s vs %s", .pub_max$label, .pub_min$label, .fem_max$label, .fem_min$label) else "mono-division"))

Hauts-de-France — 22 zones d’emploi : - Part du secteur dans l’emploi privé local : 2,9 % (vs 2,1 % moyenne nationale) - Dynamique 2023-2025 : +2,5 %/an (vs +1,8 %/an national)

En synthèse — l’essentiel en chiffres

Voir le code
# Comparaison de DEUX TAUX : seuil absolu (fr_ecart_qualif relatif explose si le réf ≈ 0)
.dir1 <- dplyr::case_when(
  tcam_us > tcam_tot + 0.3 ~ "nettement plus rapide que",
  tcam_us < tcam_tot - 0.3 ~ "plus lent que",
  TRUE                     ~ "comparable à")
.dir3 <- dplyr::case_when(
  reg_tcam_rec > tcam_us + 0.3 ~ "plus dynamique que",
  reg_tcam_rec < tcam_us - 0.3 ~ "moins dynamique que",
  TRUE                         ~ "proche de")

.m <- c(
  glue::glue("**Une croissance soutenue** — {sect_phrase_cap} progresse de **{fmt_sign_1dec(z0(tcam_us))} % par an** sur {p_evol[1]}-{p_evol[2]} (URSSAF privé), un rythme {.dir1} celui de l'emploi privé total (**{fmt_sign_1dec(z0(tcam_tot))} % par an**)."),
  glue::glue("**Un profil d'emploi singulier** — **{fmt_pct_1dec(eec_sect$lab_fem_pct)} de femmes** (contre {fmt_pct_1dec(eec_fr$lab_fem_pct)} tous secteurs) et **{fmt_pct_1dec(eec_sect$lab_tpart_pct)} de temps partiel** (contre {fmt_pct_1dec(eec_fr$lab_tpart_pct)} au national)."),
  glue::glue("**Le focus {params$territoire_focus}** — Le secteur y pèse **{fmt_pct_1dec(reg_part)}** de l'emploi privé local (contre {fmt_pct_1dec(part_secteur)} en moyenne nationale) et y progresse de **{fmt_sign_1dec(z0(reg_tcam_rec))} % par an**, un rythme {.dir3} la moyenne nationale.")
)
if (.multi_naf) .m <- c(.m, glue::glue(
  "**Des sous-segments contrastés** — **{.pub_max$label}** est la composante la plus publique ({fmt_pct_1dec(.pub_max$lab_pub_pct)} d'emploi public) quand **{.pub_min$label}** relève surtout du privé ({fmt_pct_1dec(.pub_min$lab_pub_pct)}) ; **{.fem_max$label}** est la plus féminisée ({fmt_pct_1dec(.fem_max$lab_fem_pct)}) face à **{.fem_min$label}** ({fmt_pct_1dec(.fem_min$lab_fem_pct)})."))
.m <- c(.m,
  glue::glue("**Qualité et stabilité de l'emploi** — La part de CDD atteint **{fmt_pct_1dec(eec_sect$lab_cdd_pct)}** (contre {fmt_pct_1dec(eec_fr$lab_cdd_pct)} au national) et le sous-emploi **{fmt_pct_1dec(eec_sect$lab_souempl_pct)}** (contre {fmt_pct_1dec(eec_fr$lab_souempl_pct)}). {.q_verdict}"),
  glue::glue("**Deux périmètres de mesure** — Environ **{fmt_pct_1dec(public_share)}** de l'emploi du secteur échappe à l'URSSAF (emploi public, hors champ privé) : la dynamique s'appuie sur l'**emploi privé** (URSSAF, {ann_ref}), la spécialisation sur l'**emploi total** (FLORES, privé + public, {.fl_ref}). Les particuliers employeurs (emploi à domicile direct) restent hors champ des deux sources.")
)
cat(paste(sprintf("%d. %s", seq_along(.m), .m), collapse = "\n\n"))
  1. Une croissance soutenue — Le secteur de la logistique progresse de +1,8 % par an sur 2023-2025 (URSSAF privé), un rythme nettement plus rapide que celui de l’emploi privé total (0,0 % par an).

  2. Un profil d’emploi singulier27,6 % de femmes (contre 48,8 % tous secteurs) et 7,4 % de temps partiel (contre 17,6 % au national).

  3. Le focus Hauts-de-France — Le secteur y pèse 2,9 % de l’emploi privé local (contre 2,1 % en moyenne nationale) et y progresse de +2,5 % par an, un rythme plus dynamique que la moyenne nationale.

  4. Qualité et stabilité de l’emploi — La part de CDD atteint 3,6 % (contre 7,6 % au national) et le sous-emploi 2,9 % (contre 4,3 %). Le secteur reste globalement dans la moyenne nationale sur ces dimensions.

  5. Deux périmètres de mesure — Environ 1,1 % de l’emploi du secteur échappe à l’URSSAF (emploi public, hors champ privé) : la dynamique s’appuie sur l’emploi privé (URSSAF, 2025), la spécialisation sur l’emploi total (FLORES, privé + public, 2024). Les particuliers employeurs (emploi à domicile direct) restent hors champ des deux sources.

Sources & méthodologie

  • URSSAF-ACOSS (URSSAF-COM-tidy-A88.parquet) — Effectifs salariés privés communaux par NA88, 2006-2024
  • INSEE FLORES (FLORES-COM-tidy-A88.parquet) — Effectifs salariés total (privé + public, hors PE), 2017-2024
  • INSEE EEC (EEC-AGG-naf88-annee.parquet) — Profil emploi national par NA88, millésimes 2022 + 2024
  • INSEE MOBZELT 2022 (MOBZELT-AGG-naf88-ze.parquet) — Profil territorial ZE2020 × NA88

Convention indicateurs : eco_emppriv_* (URSSAF privé), eco_empstot_* (FLORES total), lab_* (profil EEC/MOBZELT). Validation cross-source ±10% sur écarts FLORES-EEC pour le secteur.

Limites V1 : EEC limité aux millésimes 2022+2024 (sans recodage PCS 2003→2020 pour antériorité). FLORES non encore intégré au pipeline PTB transverse (consommation directe du tidy DBD).

Modèle de référence : tplj-modele-plan-fiche-sectoriel.md (structure cadre psect-fiche).