2  Analyse centrographique

L’objectif de ce TD est de mettre en lumière l’intérêt de l’analyse centrographique pour évaluer des tendances spatio-temporelles. Nous utilisons l’exemple de la dynamique spatiale de la répartition de la population des États-Unis depuis 1790.

TipObjectif du TD

Méthode : Être en mesure d’identifier et de discuter la centralité d’un phénomène spatial au cours du temps.

Sur R :

  • Charger un jeu de données .csv.
  • Manipulation de données :
    • Filtrer des données
    • Créer des indicateurs
    • Joindre des tableaux de données
  • Visualisation :
    • Cartographie simple
    • Graph de ligne
    • Créer une carte animée
NoteMéthode

Voilà les étapes de l’analyse. Il s’agit d’une démarche classique de résolution d’une question de recherche à visé descriptive.

  1. Formulation de la question et de la méthode
  2. Identification et collecte des données nécessaires
  3. Prépraitement des données
  4. Application de la méthode
  5. Analyse des résultats
  6. Interprétation des résultats

Le TD suit cette démarche.

2.1 Formulation du problème et cadre de l’analyse

Questionnement : quelle est la dynamique spatiale de la population des États-Unis depuis la fin du 18e siècle ? (le questionnement s’intéresse donc à un processus spatio-temporel)

Type de démarche : descriptive. Ici, on ne s’intéresse qu’à la description du phénomène, il n’est pas question de prouver le pourquoi : il ne s’agit pas de modélisation à visé explicative.

Méthode :

  • La méthode d’analyse spatiale qui permet l’identification de la localisation moyenne d’entités spatiales est l’analyse centrographique. (dimension spatial)
  • Pour évoluer la tendance temporelle de cette localisation moyenne, il faut évaluer cette localisation au cours du temps. (dimension temporelle)

Ici, je vous donne la méthode. Il va de soit qu’en dehors du TD, vous devrez trouvez vous-même les méthodes appropriées à la résolution de votre questionnement (en lisant la littérature scientifique).

2.2 Données

Pour répondre au questionnement et au vu de la méthode retenue, il faut trouver une donnée qui enregistre la population des États-Unis à une échelle spatiale cohérente et sur un pas de temps régulier (pour évaluer la tendance temporelle). En temps normal, vous devrez trouvez vous-même la donnée (si elle existe), ici je vous la donne directement.

Le recensement de la population des États-Unis est réalisé pour la première fois en 1790, conformément à la Constitution américaine, qui exige qu’un dénombrement de la population soit effectué tous les dix ans. Son objectif initial était politique : déterminer la représentation de chaque État au Congrès et répartir l’impôt fédéral selon la population. Très vite, le recensement est aussi devenu un outil administratif et économique, permettant de connaître la distribution de la population et l’essor des territoires. Organisé par le Census Bureau, il constitue aujourd’hui l’une des plus longues séries statistiques au monde.

La donnée est disponible sur le site du Census Bureau. Elle est mise à disposition dans un format inexploitable en l’état. Sur Kaggle, on trouve le jeu de données correctement sous un format tidy, c’est la donnée que nous utiliserons. Le jeu de données dispose de trois variables : state, year et population.

Code
readr::read_csv("data/src/us_population_by_state.csv", show_col_types = FALSE) |>
reactable::reactable(
    searchable = TRUE,
    filterable = TRUE,
    striped = TRUE,
    highlight = TRUE,
    bordered = TRUE,
    defaultPageSize = 10,
    theme = reactablefmtr::nytimes()
  )
NoteInstructions

Chargez le jeu de données avec la fonction readr::read_csv() (c.f. Section 8.1.1). Téléchargez la donnée en cliquant ici.

Code
# Read pop data
dt <- read_csv("data/src/us_population_by_state.csv")
## Rows: 6579 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (1): state
## dbl (2): year, population
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

À ce stade vous disposez de presque toutes les données nécessaires : l’année, l’état et la population. Cependant, l’information spatiale n’est pas explicitement inclue dans la table (vous n’avez que le nom des états), vous ne pouvez donc pas effectuer d’analyse centrographique. Il faut donc trouver une donnée qui enregistre la géométrie des états.

Les jeux de données proposés par natural earth permettent de récupérer la géométrie des états. On peut récupérer la donnée directement sous R via le package rnaturalearth. Une fois les données récupérées, on les projette en NAD83 (le référentiel métrique des USA, équivalent de notre lambert93).

# Récupérer les états des USA (1:50m scale)
states <- ne_states(country = "United States of America", returnclass = "sf")

# Pour le détail de la projection
st_crs(states)
Coordinate Reference System:
  User input: WGS 84 
  wkt:
GEOGCRS["WGS 84",
    DATUM["World Geodetic System 1984",
        ELLIPSOID["WGS 84",6378137,298.257223563,
            LENGTHUNIT["metre",1]]],
    PRIMEM["Greenwich",0,
        ANGLEUNIT["degree",0.0174532925199433]],
    CS[ellipsoidal,2],
        AXIS["latitude",north,
            ORDER[1],
            ANGLEUNIT["degree",0.0174532925199433]],
        AXIS["longitude",east,
            ORDER[2],
            ANGLEUNIT["degree",0.0174532925199433]],
    ID["EPSG",4326]]
NoteInstructions
  1. Récupérer la donnée avec rnaturalearth.
    • (bis) Dans le cas où rnaturalearth ne fonctionnerait pas, vous pouvez télécharger la donnée ici. Pour lire une donnée spatiale on utilise le package sf avec la fonction read_sf() qui s’utilise d’une façon similaire à readr::read_csv().
  2. Convertissez la donnée en NAD83 (EPSG:5070).

Pour convertir un objet sf dans un autre système de projection, on utilise la fonction sf::st_transform().

states <- st_transform(states, crs = 5070)
Code
# Dans le cas où rnaturalearth ne fonctionnerait pas :
states <- read_sf("data/src/states.gpkg")
states <- st_transform(states, crs = 5070)
WarningRappel

À chaque fois que vous voyez un nouveau package vous devez l’installer et le charger.

Pensez à inscrire les fonctions library() en tête de votre script et pas en plein milieu.

TipQuestion

Pourquoi doit-on convertir la donnée spatiale dans un référentiel métrique ?

2.3 Pré-traitement des données

2.3.1 Filtrage des états

NoteInstructions

Supprimer l’Alaska et Hawaii dans les deux jeux de données.

Code
to_remove <- c("Hawaii", "Alaska")
dt <- filter(dt, !state %in% to_remove)
states <- filter(states, !name %in% to_remove)

# Équivalent à (moins élégant) :
dt <- filter(dt, state != "Hawaii", state != "Alaska")
states <- filter(states, name != "Hawaii", name != "Alaska")
TipQuestion

Pourquoi les supprime-t-on ?

2.3.2 Calcul des centroïdes des états

Le code ci-dessous nécessite les packages dplyr et sf (permet de manipuler des données spatiales). Les fonctions commençant par st_ sont issues du package sf.

state_centroids <-
    # Générer les centroïdes
    st_centroid(states) |>
    # Extraire les coordonnées
    mutate(
        x = st_coordinates(geometry)[,1],
        y = st_coordinates(geometry)[,2]
    ) |>
    # Convertir en data.frame standard
    st_drop_geometry() |>
    # Sélectionner uniquement les variables d'intérêt
    select(name, x, y)
Warning: st_centroid assumes attributes are constant over geometries
TipQuestion

Pourquoi calcule-t-on le centroïde des états ?

2.3.3 Jointure des données

NoteInstructions

Joindre les deux jeux de données. Référez-vous à la Section 8.1.5 pour les jointures.

Code
data_joined <- left_join(dt, state_centroids, by = c("state" = "name"))
TipQuestion

Pourquoi fait-on cela ?

On souhaite récupérer les informations spatiales

2.4 Application de la méthode

Le centre de gravité (ou barycentre ou point moyen) d’un nuage de point (Equation 2.1). Il s’agit d’une simple moyenne.

\[ (\bar{x}, \bar{y}) = \left(\frac{1}{N}\sum_{i = 1}^{N}x_i, \quad \frac{1}{N}\sum_{i = 1}^{N}y_i\right) \tag{2.1}\]

Sous R, cela revient à calculer la moyenne des :

  • Longitudes (mean(x))
  • Latitudes (mean(y)).

Le centre de gravité pondéré par la variable \(z\), \(\bar{x}_z\) Equation 2.2.

\[ (\bar{x}_z, \bar{y}_z) = \left(\frac{\sum_{i = 1}^{N}z_ix_i}{\sum^{N}_{i=1}z_i}, \frac{\sum_{i = 1}^{N}z_iy_i}{\sum^{N}_{i=1}z_i}\right) \tag{2.2}\]

Sous R, cela revient à calculer la moyenne pondérée des :

  • Longitudes : (sum(population * x) / sum(population))
  • Latitudes : (sum(population * y) / sum(population))

La distance standard non pondérée (Equation 2.3).

\[ \text{SD} = \sqrt{\text{var}(x) + \text{var}(y)} = \sqrt{\frac{1}{N}\sum_{i=1}^{N}(x_i - \bar{x})^2 + \frac{1}{N}\sum_{i=1}^{N}(y_i - \bar{y})^2} \tag{2.3}\]

Sous R, on écrira : sqrt(var(x) + var(y))

La distance standard pondérée des \(x\) et \(y\) par la variable \(z\) (Equation 2.4).

\[ \text{SD}_z = \sqrt{\frac{\sum_{i=1}^{N}z[(x_i - \bar{x}_z)^2 + (y_i - \bar{y}_z)^2]}{\sum^{N}_{i=1}z_i}} \tag{2.4}\]

Sous R, on écrira : sqrt(sum(z * ((x - mean(x_pond))^2 + (y - mean(y_pond))^2)) / sum(z))

NoteInstructions
  1. Calculer le centre de gravité non pondéré des états avec sa distance standard. Utilisez le jeu de données des centroïdes des états (state_centroids). Le résultat doit être un data.frame d’une seule ligne avec trois variables : \(\bar{x}\), \(\bar{y}\) et \(\text{SD}\). Vous utiliserez la fonction dplyr::summarise().
Code
center_unpondered <- state_centroids |>
  summarise(
    c_x = mean(x),
    c_y = mean(y),
    c_sd = sqrt(var(x) + var(y)),
  )
print(center_unpondered)
##      c_x     c_y    c_sd
## 1 430395 1935957 1344019
  1. Calculer le centre de gravité des états pondéré par la population avec sa distance standard pour toutes les années depuis 1790. Utilisez le jeu de données issu de la jointure entre les centroïdes des états (state_centroids) et celui issu du filtrage des données. Le résultat doit être un data.frame avec quatre variables : l’année, \(\bar{x}_z\), \(\bar{y}_z\) et \(\text{SD}_z\). Vous utiliserez les fonctions group_by() et summarise() du package dplyr.
Code
center_pondered <- data_joined |>
  group_by(year) |>
  summarise(
    cw_x = sum(population * x) / sum(population),
    cw_y = sum(population * y) / sum(population),
    cw_sd = sqrt(sum(population * ((x - mean(cw_x))^2 + (y - mean(cw_y))^2)) / sum(population))
  )

Au final, vous aurez donc deux jeux de données :

  • Un d’une seule ligne contenant le centre de gravité non pondéré et sa distance standard.
  • Un contenant, pour chaque année, le centre de gravité pondéré par la population et sa distance standard pondérée.

Je vous demande d’écrire les formules vous même pour vous entraîner. Sachez qu’il existe également des packages qui intègrent des fonctions pour ce genre d’analyse :

  • aspace
    • Fonctions : nombreuses fonctions, on peut réaliser l’ensemble de ce qu’il est possible de faire en analyse centrographique.
    • Points forts : bien pour contrôler dans le détail l’analyse.
    • Points faible : moins user-friendly que sfcentral, pas intégré à sf.
  • sfcentral :
    • Fonctions : indicateurs de centralité, distance standard (boîte (2D, 3D), cercle), ellipse de distribution directionnelle.
    • Points forts : récent (donc moderne), adapté au tidyverse (e.g. dplyr)
    • Point faible : récent (donc peu mature)

2.5 Cartographie des résultats

2.5.1 Analyse non-pondérée

NoteInstructions

Cartographiez le centre non pondéré des États-Unis avec sa distance standard avec le package ggplot2 (c.f. Section 10.2 pour les graphs avec ggplot2 et Section 11.1 pour la carto, Attention nouveau package).

Vous aurez les couches suivantes :

  • Fond de carte : les USA (geom_sf())
  • Le centroïdes des états (geom_point())
  • Le centre non pondéré (geom_point())
  • La distance standard (ggforce::geom_circle()). Attention nouveau package

Vous vous appuierez sur le code ci-dessous. Vous aurez sans-doute à changer le nom des jeux de données et des variables :

Code
map_unpondered <- ggplot() +
  # [ Couche des états ]
  geom_sf(data = states) + # Le jeu de données spatiales brute
  # [ La distance standard ]
  ggforce::geom_circle(
    data = center_unpondered, # votre jeu de données non-pondéré
    aes(
      x0 = c_x, # La moyenne des x
      y0 = c_y, # La moyenne des y
      r = c_sd  # la distance standard
    ),
    # Symbologie
    fill = "red", alpha = 0.1, inherit.aes = FALSE
  ) +
  # [ Le centroïde des états ]
  geom_point(
    data = state_centroids, # Le jeu de données contenant les centroïde
    aes(
      x = x, # Les x
      y = y  # Les y
    ),
    # Symbologie
    color = "grey"
  ) +
  # [ Le centre moyen pondéré ]
  geom_point(
    data = center_unpondered, # Le jeu de données non-pondéré
    aes(
      x = c_x, # La moyenne des x
      y = c_y  # La moyenne des y
    ),
    # Symbologie
    color = "black", shape = 21, fill = "red", size = 3
  ) +
  # Titre, labels des axes, sources et réalisation
  labs(
    title = "Titre",
    subtitle = "Sous-titre",
    x = "",
    y = "",
    caption = "Données : .... Réalisation: ..."
  ) +
  # Theme
  theme_minimal()
map_unpondered

2.5.2 Analyse pondérée

NoteInstructions

Cartographiez le centre pondéré de la population des États-Unis depuis 1970 et sa distance standard avec le package ggplot2.

Créez une nouvelle carte sur la base de l’ancienne. Il vous faudra “juste” changer certains jeux de données et nom de variables.

Vous conserverez le centre non-pondéré pour comparer et vous interpréterez la carte.

Code
map_pondered <- ggplot() +
    # [ Couche des états ]
    geom_sf(data = states) + # Le jeu de données spatiales brute
    # [ La distance standard ]
    ggforce::geom_circle(
        data = center_pondered, # votre jeu de données non-pondéré
        aes(
            x0 = cw_x, # La moyenne des x
            y0 = cw_y, # La moyenne des y
            r = cw_sd,  # la distance standard
            color = year,
        ),
        # Symbologie
        fill = NA, alpha = 0.1, inherit.aes = FALSE
    ) +
    # [ Le centroïde des états ]
    geom_point(
        data = state_centroids, # Le jeu de données contenant les centroïde
        aes(
            x = x, # Les x
            y = y  # Les y
        ),
        # Symbologie
        color = "grey"
    ) +
    # [ Les centres moyens pondérés ]
    geom_point(
        data = center_pondered, # Le jeu de données non-pondéré
        aes(
            x = cw_x, # La moyenne des x
            y = cw_y,  # La moyenne des y
            fill = year
        ),
        # Symbologie
        color = "white", shape = 21, size = 3
    ) +
    # [ Le centre moyen pondéré ]
    geom_point(
        data = center_unpondered, # Le jeu de données non-pondéré
        aes(
            x = c_x, # La moyenne des x
            y = c_y  # La moyenne des y
        ),
        # Symbologie
        color = "black", shape = 21, fill = "red", size = 3
    ) +
    # Titre, labels des axes, sources et réalisation
    labs(
        title = "Titre",
        subtitle = "Sous-titre",
        x = "",
        y = "",
        caption = "Données : .... Réalisation: ..."
    ) +
    # Theme
    theme_minimal()
map_pondered

NoteInstructions

La carte est peu lisible à cause :

  • De la variation du pas de temps
  • Des distances standards
  1. Utilisez un pas de temps uniforme pour l’ensemble de la période
Code
# Conversion en km des distances standards
center_pondered <- mutate(center_pondered, cw_sd_km = cw_sd/1000)
center_unpondered <- mutate(center_unpondered, c_sd_km = c_sd/1000)

center_pondered <-
  mutate(center_pondered, year = plyr::round_any(year, 10)) |>
  group_by(year) |>
  dplyr::summarise(across(everything(), ~mean(.x)))
  1. Créez un graphique en ligne pour représentez les distances standards. Utilisez la fonction geom_line()
Code
pond_sd_line <- ggplot(center_pondered, aes(x = year, y = cw_sd_km, color = year)) +
  geom_line() +
  geom_point(size = 2) +
  geom_hline(
    yintercept = center_unpondered$c_sd_km, color = "red"
  ) +
  annotate(
    "text",
    x = min(as.numeric(center_pondered$year)) + 50,
    y = center_unpondered$c_sd_km + 50,
    label = "Distance standard non-pondérée",
    color = "red"
  ) +
  labs(x = "", y = "Distance Standard pondérée (km)") +
  theme_minimal() +
  theme(legend.position = "none")
pond_sd_line

  1. Mettez à jour votre carte des centres moyens pondérés.
Code
pond_map_disc <- ggplot() +
    geom_sf(data = states) +
    geom_point(
        data = center_pondered,
        aes(x = cw_x, y = cw_y, fill = year),
        color = "white", shape = 21, size = 4
    ) +
    geom_point(
        data = center_unpondered,
        aes( x = c_x, y = c_y),
        color = "white", shape = 21, fill = "red", size = 4
    ) +
    guides(fill = guide_colorbar(barwidth = 15)) +
    labs( x = "", y = "", fill = "Années") +
    theme_minimal() +
    theme(legend.position = "bottom")
pond_map_disc

Ensuite, combinez la carte et le graph nouvellement créé au moyen de package patchwork. Utilisez la fonction wrap_plots() (c.f. Section 10.2.4).

Code
wrap_plots(
  list(pond_map_disc, pond_sd_line),
  ncol = 1
) +
  plot_annotation(
    title = "Dynamique moyenne de la population des USA entre 1790 et 2024",
    subtitle = "**A)** Centres moyens pondérés par la population. Le <span style='color:red;'>**point rouge**</span> indique le centre moyen non-pondéré.<br>**B)** Distances standards pondérées par la population.",
    caption = "**Données :** United States Census Bureau, 2025. **Réalisation :** Antoine Le Doeuff, 2026",
    tag_levels = 'A',
    theme = theme(
        plot.title = element_text(size = 16, face = "bold"),
        plot.subtitle = ggtext::element_markdown(size = 12),
        plot.caption = ggtext::element_markdown(size = 10),
    )
  ) +
  plot_layout(
    heights = c(4, 2)
  )

2.5.3 Cartographie animée

Sous R, on peut facilement animer un graph ggplot2 en utilisant le package gganimate. Le rendu est particulièrement parlant pour l’analyse centrographique temporelle.

NoteInstructions

Créez une carte avec ggplot2 les couches suivantes :

  • Fond de carte des USA
  • Centres pondérés de la population
  • Distance standard pondérée de la population

Appuyez-vous sur les cartes précédentes. Ajoutez les lignes suivantes à votre code ggplot2 :

# Convertissez l'année en facteur dans votre tableau des centres pondérés
center_pondered <- mutate(center_pondered, year = as.factor(year))

anim <- ggplot() +
    # ... votre code
    labs(
        title = "Centre pondéré de la population : {closest_state}",
        x = "", y = ""
    ) +
    transition_states(year) +
    shadow_mark(
        past = TRUE,
        alpha = 0.5,
        colour = "red",
        # l'argument exclude_layer vous permet de supprimer des couches de votre
        # objet ggplot pour les années passées. Le chiffre indiquent l'ordre
        # d'apparition des couches.
        # exclude_layer = c(3, 4)
    )

Puis rendez l’animation :

animate(
    anim, # votre object animation
    duration = 10,
    fps = 20,
    width = 800,
    height = 500,
    renderer = gifski_renderer(
        "figures/weighted_center.gif" # le nom de votre anim
    )
)

La Figure 2.1 donne un exemple de rendu. Vous pouvez modifier votre code pour aboutir à un rendu similaire.

Figure 2.1: Centre moyen pondéré de la population des États-Unis depuis 1790 (United States Census Bureau, 2025)
Code
p <- ggplot() +
  geom_sf(data = states) +
  geom_point(
    data = pond_disc,
    aes(x = cw_x, y = cw_y),
    color = "red", size = 4
  ) +
  geom_text(
    data = pond_disc,
    aes(x = cw_x, y = cw_y, label = year),
    color = "red", vjust = -1, size = 4
  ) +
  ggforce::geom_circle(
    data = pond_disc,
    aes(x0 = cw_x, y0 = cw_y, r = cw_sd),
    fill = "transparent", color = "black", inherit.aes = FALSE,
    linewidth = 1
  ) +
  labs(
    title = "Centre moyen pondéré de la population des États-Unis",
    subtitle = "Le cercle représente la distance standard pondérée. Année : {closest_state}",
    x = "", y = "",
    caption = "Données : United States Census Bureau, 2025. Réalisation : Antoine Le Doeuff, 2026"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(size = 18, face = "bold"),
    plot.subtitle = element_text(size = 14, face = "italic"),
  ) +
  transition_states(year) +
  shadow_mark(
    past = TRUE,
    alpha = 0.3,
    colour = "red",
    exclude_layer = c(3, 4)
  )

# Rendre l'animation
anim <- animate(
  p,
  duration = 10,
  fps = 20,
  width = 800,
  height = 500,
  renderer = gifski_renderer("figures/weighted_center.gif")
)

2.5.4 Script complet

Voilà à quoi peut ressembler votre script complet.

################################################################################
# Script Name: centro_usa.r
# Description: Analyse centrographique de la population des USA entre 1790 et 2024
# Author: Antoine Le Doeuff
# Date created: 2025-10-28
################################################################################

library(readr)
library(sf)
library(dplyr)
library(rnaturalearth)
library(gganimate)
library(ggplot2)
library(patchwork)

# //////////////////////////////////////////////////////////////////////////////
# Chargement des données -------------------------------------------------------
# Données de pop
# https://www.kaggle.com/datasets/rolfhendriks/us-population-by-state-comprehensive-data
dt <- read_csv("01_points/data/src/us_population_by_state.csv")

# Polygones des USA
# Convertion en NAD83 / Conus Albers Equal Area Projection
states <- ne_states(country = "United States of America", returnclass = "sf") |>
  st_transform(crs = 5070)

# //////////////////////////////////////////////////////////////////////////////
# Prétraitements ----------------------------------------------------------------
# Suppression de certains états ................................................
to_remove <- c("Hawaii", "Alaska")
dt <- filter(dt, !state %in% to_remove)
states <- filter(states, !name %in% to_remove)

# Calcul des centroïdes ........................................................
state_centroids <-
  st_centroid(states) |>
  mutate(
    x = st_coordinates(geometry)[,1],
    y = st_coordinates(geometry)[,2]
  ) |>
  st_drop_geometry() |>
  select(name, x, y)

# Jointure .....................................................................
data_joined <- left_join(dt, state_centroids, by = c("state" = "name"))

# //////////////////////////////////////////////////////////////////////////////
# Analyse centrographique ------------------------------------------------------
# Centre non-pondéré ...........................................................
center_unpondered <- state_centroids |>
  summarise(
    c_x = mean(x),
    c_y = mean(y),
    c_sd = sqrt(var(x) + var(y)),
  )

# Centre pondéré pour chaque année .............................................
center_pondered <- data_joined |>
  group_by(year) |>
  summarise(
    cw_x = sum(population * x) / sum(population),
    cw_y = sum(population * y) / sum(population),
    cw_sd = sqrt(sum(population * ((x - mean(cw_x))^2 + (y - mean(cw_y))^2)) / sum(population))
  )

# //////////////////////////////////////////////////////////////////////////////
# Analyse ----------------------------------------------------------------------
# Centre pondéré ...............................................................
map_unpondered <- ggplot() +
  geom_sf(data = states) +
  ggforce::geom_circle(
    data = center_unpondered,
    aes(x0 = c_x, y0 = c_y, r = c_sd),
    fill = "red", alpha = 0.1, inherit.aes = FALSE
  ) +
  geom_point(
    data = state_centroids,
    aes( x = x, y = y),
    color = "grey"
  ) +
  geom_point(
    data = center_unpondered,
    aes(x = c_x, y = c_y),
    color = "black", shape = 21, fill = "red", size = 3
  ) +
  labs(
    title = "Titre",
    subtitle = "Sous-titre",
    x = "", y = "",
    caption = "Données : .... Réalisation: ..."
  ) +
  theme_minimal()
map_unpondered

# Centre non-pondéré ...........................................................
# Conversion en km des distances standards
center_pondered <- mutate(center_pondered, cw_sd_km = cw_sd/1000)
center_unpondered <- mutate(center_unpondered, c_sd_km = c_sd/1000)

# Mise à jour du pas de temps
center_pondered <-
  mutate(center_pondered, year = plyr::round_any(year, 10)) |>
  group_by(year) |>
  dplyr::summarise(across(everything(), ~mean(.x)))

# Graphique des distances standards
pond_sd_line <- ggplot(center_pondered, aes(x = year, y = cw_sd_km, color = year)) +
  geom_line() +
  geom_point(size = 2) +
  geom_hline(
    yintercept = center_unpondered$c_sd_km, color = "red"
  ) +
  annotate(
    "text",
    x = min(as.numeric(center_pondered$year)) + 50,
    y = center_unpondered$c_sd_km + 50,
    label = "Distance standard non-pondérée",
    color = "red"
  ) +
  labs(x = "", y = "Distance Standard pondérée (km)") +
  theme_minimal() +
  theme(legend.position = "none")

# Cartographie des centres pondérés
pond_map_disc <- ggplot() +
  geom_sf(data = states) +
  geom_point(
    data = center_pondered,
    aes(x = cw_x, y = cw_y, fill = year),
    color = "white", shape = 21, size = 4
  ) +
  geom_point(
    data = center_unpondered,
    aes( x = c_x, y = c_y),
    color = "white", shape = 21, fill = "red", size = 4
  ) +
  guides(fill = guide_colorbar(barwidth = 15)) +
  labs(x = "", y = "", fill = "Années") +
  theme_minimal() +
  theme(legend.position = "bottom")

# Jointure du graphique et de la carte
wrap_plots(
  list(pond_map_disc, pond_sd_line),
  ncol = 1
) +
  plot_annotation(
    title = "Dynamique moyenne de la population des USA entre 1790 et 2024",
    subtitle = "**A)** Centres moyens pondérés par la population. Le <span style='color:red;'>**point rouge**</span> indique le centre moyen non-pondéré.<br>**B)** Distances standards pondérées par la population.",
    caption = "**Données :** United States Census Bureau, 2025. **Réalisation :** Antoine Le Doeuff, 2026",
    tag_levels = 'A',
    theme = theme(
      plot.title = element_text(size = 16, face = "bold"),
      plot.subtitle = ggtext::element_markdown(size = 12),
      plot.caption = ggtext::element_markdown(size = 10),
    )
  ) +
  plot_layout(
    heights = c(4, 2)
  )

# //////////////////////////////////////////////////////////////////////////////
# Animation --------------------------------------------------------------------
p <- ggplot() +
  geom_sf(data = states) +
  geom_point(
    data = center_pondered,
    aes(x = cw_x, y = cw_y),
    color = "red", size = 4
  ) +
  geom_text(
    data = center_pondered,
    aes(x = cw_x, y = cw_y, label = year),
    color = "red", vjust = -1, size = 4
  ) +
  ggforce::geom_circle(
    data = center_pondered,
    aes(x0 = cw_x, y0 = cw_y, r = cw_sd),
    fill = "transparent", color = "black", inherit.aes = FALSE,
    linewidth = 1
  ) +
  labs(
    title = "Centre moyen pondéré de la population des États-Unis",
    subtitle = "Le cercle représente la distance standard pondérée. Année : {closest_state}",
    x = "", y = "",
    caption = "Données : United States Census Bureau, 2025. Réalisation : Antoine Le Doeuff, 2026"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(size = 18, face = "bold"),
    plot.subtitle = element_text(size = 14, face = "italic"),
  ) +
  transition_states(year) +
  shadow_mark(
    past = TRUE,
    alpha = 0.3,
    colour = "red",
    exclude_layer = c(3, 4)
  )

# Rendre l'animation
anim <- animate(
  p,
  duration = 10,
  fps = 20,
  width = 800,
  height = 500,
  renderer = gifski_renderer("figures/weighted_center.gif")
)