3  Mobilités et flux

Durant ce TD, vous apprendrez à réaliser une analyse descriptive de flux.

NoteMéthode

Voilà les étapes de l’analyse :

  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.

Question : Comment sont réparties les migrations résidentielles des finistériens sur le territoire du Finistère et que cela dit-il de ce territoire ?

Méthode : analyse descriptive d’une matrice origine/destination (OD)

3.1 Données

Données nécessaires :

  • Une donnée, la plus récente possible, indiquant les flux entre chaque commune du Finistère.
  • Comme je souhaite réaliser des cartes, cette donnée doit-être spatiale. Si elle ne l’est pas, il faudra que je la joigne à une donnée spatiale des communes.

Le producteur des données de mobilité de référence à l’échelle du territoire français est l’INSEE (encore une fois). La donnée fournie par l’INSEE est issue du recensement de la population, vous la trouverez ici.

TipQuestion
  • Quand a été produite la donnée ?
  • Cette donnée renseigne les flux sur quelle période ?
  • S’agit-il d’une donnée spatiale ?
NoteInstructions : chargement des données
  1. Charger les migrations résidentielles.
  2. Télécharger les communes du Finistère avec happign::get_wfs(). Vous utiliserez l’argument query qui permet de filtrer les données via le langage de requête ECQL. Ici, on veut récupérer uniquement les communes dont le code INSEE commence par 29 (donc, le Finistère). Dans la mesure où le ECQL possède une syntaxe proche du SQL, on écrira la requête suivante : code_insee LIKE '29%'.

Pour rappel, pour trouver de l’aide sur une fonction, vous pouvez regarder la documentation de la fonction directement dans R : ?get_wfs(). Vous pouvez également regarder la documentation sur le site web du package s’il existe (les fonctions du package sont indiquées dans l’onglet Reference).

Code
# Charger les données INSEE
dt_mobi <- read_delim("data/src/base_flux_mobilite_residentielle_2020.csv") |>
  janitor::clean_names()
## Rows: 482070 Columns: 5
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ";"
## chr (4): CODGEO, LIBGEO, DCRAN, L_DCRAN
## dbl (1): NBFLUX_C20_POP01P
## 
## ℹ 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.

# Récupérer les communes sur le serveur de l'IGN
# Ne télécharger les données que si elles n'existent pas en local
com_path <- "data/src/com_finistere_bdtopov3_2026.gpkg"
if (file.exists(com_path)) {
  cat("Le fichier existe déjà, chargement...")
  coms <- read_sf(com_path)
} else {
  coms <- get_wfs(
    layer = "BDTOPO_v3:commune",
    ecql_filter = "code_insee_du_departement = '29%'",
    interactive = false
  ) |>
    st_transform(2154)
  write_sf(coms, com_path)
}
## Le fichier existe déjà, chargement...

Quand vous téléchargez des données, vous consommez de l’énergie. Si vous refaites tourner votre script de nombreuses fois, vous téléchargerez de nouveau les données autant de fois ; ce n’est pas très écolo. Aussi, une fois que vous avez téléchargé la data, vous pouvez l’enregistrer sur votre machine. Vous pouvez par exemple écrire :

com_path <- "data/src/com_finistere_bdtopov3_2026.gpkg"
if (file.exists(com_path)) {
  print("File already exists, loading it...")
  coms <- read_sf(com_path)
} else {
  # [téléchargez les données avec happign]
  write_sf(coms, com_path)
}

La syntaxe IF/ELSE (alternative) est une des briques fondamentales de l’algorithmique. Si vous voulez plus d’informations, référez à la Section 6.1.

NoteInstructions : prétraitement
  1. Filtrer les données de mobilités résidentielles pour ne retenir que les mobilités qui ont eu lieu en Finistère. Retirer également les mobilités intra-communales (codgeo != dcran).
Code
dt_mobi_fin <- filter(
  dt_mobi,
  startsWith(x = codgeo, "29"),
  startsWith(x = dcran, "29"),
  codgeo != dcran
)
  1. Convertir le data.frame en matrix pour obtenir une matrice origine/destination (OD) :
# Récupérer tous les codes insee
all_ids <- sort(unique(c(dt_mobi_fin$codgeo, dt_mobi_fin$dcran)))

# Créer des facteurs avec les mêmes niveaux pour chaque O/D
dt_mobi_fin$i <- factor(dt_mobi_fin$dcran, levels = all_ids)
dt_mobi_fin$j <- factor(dt_mobi_fin$codgeo, levels = all_ids)

# Créer la sparse matrix (origin × destination)
m <- Matrix::sparseMatrix(
  i = as.integer(dt_mobi_fin$i),
  j = as.integer(dt_mobi_fin$j),
  x = dt_mobi_fin$nbflux_c20_pop01p,
  dims = c(length(all_ids), length(all_ids)), # Passer les dimensions explicitement
  dimnames = list(all_ids, all_ids)
)
print(m[1:5, 1:5])
5 x 5 sparse Matrix of class "dgCMatrix"
      29001    29002 29003     29004 29005
29001     . .            .  .            .
29002     . .            . 10.050391     .
29003     . .            .  .            .
29004     . 4.933139     .  .            .
29005     . .            .  5.050315     .

Notez que l’on utilise une sparseMatrix (matrice creuse) et non une matrix ordinaire (matrice dense). Les sparseMatrix sont beaucoup plus efficientes que les matrix dans la mesure où elles ne stockent que les valeurs non-nulles.

3.2 Rappels sur les indices

Soit une matrice origine/destination (OD). On admet que les indices \(i\) et \(j\) identifie les mêmes entités spatiales. Ainsi, le flux \(O_i\) est affilié à la même entité spatiale que le flux \(D_j\).

\[ \begin{array}{c|cccc|c} & D_1 & D_2 & \cdots & D_j & \text{Flux total émis} \\ \hline O_1 & n_{11} & n_{12} & \cdots & n_{1j} & \sum_{j}n_{1j} \\ O_2 & n_{21} & n_{22} & \cdots & n_{2j} & \sum_{j}n_{2j} \\ \vdots& \vdots & \vdots & \ddots & \vdots & \vdots \\ O_i & n_{i1} & n_{i2} & \cdots & n_{ij} & \sum_{j}n_{ij} \\ \hline \text{Flux total reçu} & \sum_{i}n_{i1} & \sum_{i}n_{i2} & \cdots & \sum_{i}n_{ij} & \sum_{j}\sum_{i}n_{ij} \end{array} \]

Par caractériser les flux de chaque entité spatiale de la matrice OD, on commence généralement par calculer le volume, le solde et le taux d’attractivité de chaque entité spatiale.

Le volume \(V\) de l’entité spatiale \(i\) est la somme de son flux total émis et reçu.

\[ V_{i} = \sum_{k=1}^{N}n_{ik} + \sum_{k=1}^{N}n_{ki} \]

Le solde \(S\) de l’entité \(i\) est la différence entre le flux total reçu et le flux total émis.

\[ S_{i} = \sum_{k=1}^{N}n_{ki} - \sum_{k=1}^{N}n_{ik} \]

Le taux d’attractivité \(A\) de l’entité \(i\) correspond au solde de l’entité \(i\) divisé par le volume de l’entité \(i\).

\[ A_{i} = \frac{S_{i}}{V_{i}} \]

Il est également possible de construire des indices qui tirent parti de l’ensemble des informations de la matrice. On peut par exemple construire un indicateur d’attractivité \(\text{IA}_i\) pour l’entité spatiale \(i\). Ce dernier correspond au total du flux reçu dans l’entité \(i\) divisé par le total des flux émis et reçu dans l’aire d’étude.

\[ \text{IA}_i = \frac{\sum_{k=1}^{N} n_{ki}}{\sum_{k=1}^{N}\sum_{l=1}^{N} n_{kl}} \]

Un indice d’émissivité \(\text{IE}_i\) peut-être calculé de façon similaire en divisant le total flux émis dans une entité spatiale \(i\) par le total des flux émis et reçus dans l’aire d’étude.

\[ \text{IE}_i = \frac{\sum_{k=1}^{N} n_{ik}}{\sum_{k=1}^{N}\sum_{l=1}^{N} n_{kl}} \]

Il est possible d’évaluer le caractère préférentiel du lien entre deux entités spatiales, en comparant le flux effectif au flux attendu : c’est l’indice de relation préférentielle \(\text{IRP}_{ij}\) entre les entités \(i\) et \(j\).

\[ \text{IRP}_{ij} = \frac{\text{Flux effectif}}{\text{Flux attendu}} = n_{ij} \large/ \frac{\sum_{k=1}^{N} n_{ik} \cdot \sum_{k=1}^{N} n_{kj}}{\sum_{k=1}^{N}\sum_{l=1}^{N} n_{kl}} \]

Si \(\text{IRP}_{ij} > 1\), alors les lieux \(i\) et \(j\) ont une relation préférentielle : le flux observé de \(i\) vers \(j\) est plus important que celui qu’on attendrait étant donné l’émissivité de \(i\) et l’attractivité de \(j\).

3.3 Calcul des indices

NoteInstructions
  1. À l’aide de la matrice OD, calculez, pour chaque entité spatiale : le total des flux émis, le total des flux reçus, le volume, le solde et le taux d’attractivité. Vous aurez à utiliser les fonctions rowSums() et colSums().
  2. Pour chaque entité spatiale, calculer l’indice d’attractivité et d’émissivité.
  3. Pour chaque combinaison d’origine/destination, calculez l’indice de relation préférentielle. Pour calculer le produit du flux total émis par l’entité \(i\) par le flux total reçu par l’entité \(j\) pour les combinaisons en une seule fois, vous utiliserez l’opérateur %o% qui symbolise le produit extérieur. Au final, vous obtiendrez une matrice des relations préférentielles. Remplacez les valeurs de la diagonale par 0 (diag(m_pref) <- 0) et remplacez les Inf par 0 (m_pref[!is.finite(m_pref)] <- 0).
Code
# Volume, arrivés, départs ...................................................
# Flux total émis
sum_dep <- rowSums(m)

# Flux total reçu
sum_arr <- colSums(m)

# Volumes
volumes <- sum_dep + sum_arr

# Soldes
soldes <- sum_arr - sum_dep

# Taux d'attractivité
attrac_ratio <- soldes / volumes

# Indices ......................................................................
tot <- sum(m)

# Indice d'attractivité
ia <- sum_arr / tot

# Indice d'émissivité
ie <- sum_dep / tot

# Relations préférentielles .....................................................
# Matrice des flux attendus
expected <- (sum_dep %o% sum_arr) / tot  # produit extérieur
# Calcul des relations
m_pref <- m / expected

# Nettoyage de la matrice
diag(m_pref) <- 0
m_pref[!is.finite(m_pref)] <- 0

3.4 Analyse des résultats

3.4.1 Statistiques des indices

Les indices ainsi calculé ne sont pas très lisibles. Avant d’analyser les résultats, on les enregistre dans un DataFrame pour faciliter leur analyse.

NoteInstructions : prétraitement des indices
  1. Créez un dataframe Avec l’ensemble des indices précédemment calculés (sauf les relations préférentielles bien entendu). Chaque ligne correspondra à une commune.
  2. Proposez des statistiques élémentaires pour chaque indices (c.f. Section 9.1.1).
Code
# Création du tableau des indicateurs
indices <- data.frame(
  code_insee = names(sum_dep),
  departure = sum_dep,
  arrival = sum_arr,
  volumes = volumes,
  soldes = soldes,
  attrac_ratio = attrac_ratio,
  ia = ia,
  ie = ie
)

# Calcul des statistiques
# Conversion en format long
indices_long <- tidyr::pivot_longer(indices, cols = !code_insee)

# Calcul des stats par indice
group_by(indices_long, name) |>
  summarise(
    mean = mean(value),
    median = median(value),
    sd = sd(value),
    min = min(value),
    max = max(value)
  )
## # A tibble: 7 × 6
##   name              mean    median        sd     min      max
##   <chr>            <dbl>     <dbl>     <dbl>   <dbl>    <dbl>
## 1 arrival       1.40e+ 2  74.7     335.         0    4660.   
## 2 attrac_ratio -1.52e- 2  -0.00401   0.326     -1       1    
## 3 departure     1.40e+ 2  66.9     365.         0    5204.   
## 4 ia            3.61e- 3   0.00193   0.00866    0       0.120
## 5 ie            3.61e- 3   0.00173   0.00944    0       0.134
## 6 soldes       -1.26e-14  -0.190    67.7     -543.    455.   
## 7 volumes       2.79e+ 2 138.      698.         5.00 9864.

3.4.2 Cartographie

Le DataFrame des indices n’est pas spatial. Il faut donc effectuer une jointure avec les communes pour pouvoir cartographier les résultats. En fonction de ce que l’on souhaite cartographier, il faudra calculer les centroïdes des communes pour réaliser une carte en cercles proportionnels.

NoteInstructions : cartographie
  1. Joignez le dataframe des indices que vous venez de créer aux données spatiales des communes.
Code
polygons <- left_join(select(coms, code_insee), indices, by = "code_insee")
  1. Si on souhaite cartographier des stocks, on utilisera des cercles proportionnels. Dans ce cadre, il faut d’abord transformer les polygones en points. Pour cela, vous créerez un nouveau data.frame spatial de géométrie points avec la fonction sf::st_centroids().
Code
centroids <- st_centroid(polygons)
## Warning: st_centroid assumes attributes are constant over geometries
  1. Cartographier l’ensemble des indices. Attention à la sémiologie graphique, il n’y a pas que des stocks…
Important

Les cartes que je vous donne sont assez élaborées et donc le code R peut paraître complexe. L’idée est que vous trouviez le maximum d’informations pour que vous puissiez vous approprier les différentes façon de configurer vos cartes.

Pour cartographier des cercles proportionnels avec ggplot2 et un objet sf de géométrie point, on utilise l’argument size dans l’esthétique (aes) de la couche. Pour un rendu plus léché, vous pouvez également doubler l’information en colorisant les cercles proportionnels via l’argument fill. Pour que la coloration fonctionne, il faut utilisant un symbole de point spécial pch = 21.

Code
# Je crée un thème que j'appliquerai à toutes les cartes
my_theme <-
  theme_minimal() +
  theme(
    legend.position = "bottom",
    plot.title   = ggtext::element_markdown(family = "roboto", size = 16, face = "bold"),
    plot.subtitle = ggtext::element_markdown(family = "roboto", size = 12, lineheight = 1.2),
    axis.title.x = ggtext::element_markdown(family = "roboto", size = 12),
    axis.title.y = ggtext::element_markdown(family = "roboto", size = 12),
    plot.caption = ggtext::element_markdown(family = "roboto", size = 8, lineheight = 1.5),
    legend.title = ggtext::element_markdown(family = "roboto", size = 8),
  )

# On défini le nom complet de le la variable avec un named vector
selected_vars <- c(
  "Flux total reçus" = "arrival",
  "Flux total émis"  = "departure",
  "Volume"           = "volumes"
)

# On définit une liste vide (réceptacle qui va recevoir les cartes)
maps <- list()

# On itère sur les noms
for (name in names(selected_vars)) {
  # On récupère le nom présent dans le tableau à partir du nom complet
  short_name <-  selected_vars[name]

  # Créée la carte
  m <- ggplot() +
    geom_sf(data = coms) +
    geom_sf(
      data = centroids,
      aes(size = .data[[short_name]]),
      pch = 21, fill = "steelblue", color = "white"
    ) +
    scale_size(
      name = name,
      range = c(1, 9),
      breaks = scales::pretty_breaks(n = 5)
    ) +
    ggtitle(name) +
    # Échelle
    ggspatial::annotation_scale(
      location = "bl", height = unit(0.15, "cm")
    ) +
    # Nord
    ggspatial::annotation_north_arrow(
        location = "tl", which_north = "true",
        height = unit(1, "cm"), width = unit(1, "cm"),
        style = north_arrow_fancy_orienteering
    ) +
    guides(
      size = guide_legend(
        direction = "horizontal",
        nrow = 1,
        label.position = "bottom"
      )
    ) +
    my_theme

  maps[[name]] <- m
}
patchwork::wrap_plots(maps, ncol = 2) +
  plot_annotation(
    title = "Volumes et flux total émis et reçus des communes finistériennes",
    subtitle = "Les flux analysés correspondent aux données de migrations résidentielles entre<br>le lieu de résidence en 2020 et le lieu de résidence précédent",
    caption = "**Données :** Base flux de mobilité (INSEE, 2023); BDTOPO V3 (IGN, 2026). **Réalisation :** Antoine Le Doeuff, 2026",
    theme = theme(
      plot.title = element_text(face = "bold", size = 18),
      plot.subtitle = ggtext::element_markdown(size = 14, lineheight = 1.2),
      plot.caption = ggtext::element_markdown(size = 11)
    )
  )

La cartographie par cercles proportionnels bicolores permet de représenter simultanément deux dimensions d’une variable : son intensité (encodée par la taille du cercle, proportionnelle à la valeur absolue) et son signe (encodée par la couleur, ici un dégradé bleu–blanc–rouge).

La construction de la légende nécessite ici une intervention manuelle, car les deux aesthetics (aes) mobilisées — size et fill — reposent sur des transformations incompatibles : size est mappé sur abs(soldes) (valeurs positives uniquement), tandis que fill est mappé sur soldes (valeurs négatives et positives). ggplot2 ne peut fusionner automatiquement deux légendes que si elles partagent exactement les mêmes breaks et la même échelle ; ce n’est pas le cas ici puisque les breaks de taille sont symétriques autour de 0 alors que les breaks de couleur couvrent les deux signes.

Code
# Création de la taille des cercles à la main
fill_breaks <- c(-750, -500, -250, 0, 250, 500)
max_val <- max(abs(centroids$soldes), na.rm = TRUE)
legend_sizes <- scales::rescale(abs(fill_breaks), to = c(1, 9), from = c(0, max_val))

ggplot() +
  geom_sf(data = coms) +
  geom_sf(
    data = centroids,
    aes(size = abs(soldes), fill = soldes),
    pch = 21, color = "white"
  ) +
  # Cacher la taille dans la légende
  scale_size(range = c(1, 9), guide = "none") +
  scale_fill_gradient2(
    name = "Solde",
    low = "#4575B4",
    mid = "white",
    high = "#D73027",
    midpoint = 0,
    space = "Lab",
    breaks = fill_breaks,
    limits = c(-750, 500),
    guide = "legend"
  ) +
  guides(
    fill = guide_legend(
      direction = "horizontal",
      nrow = 1,
      label.position = "bottom",
      # Mettre la taille directement ici
      override.aes = list(size = legend_sizes)
    )
  ) +
  labs(
    title = "Solde des communes finistériennes",
    subtitle = "Les flux analysés correspondent aux données de migrations résidentielles entre<br>le lieu de résidence en 2020 et le lieu de résidence précédent",
    caption = "**Données :** Base flux de mobilité (INSEE, 2023); BDTOPO V3 (IGN, 2026). **Réalisation :** Antoine Le Doeuff, 2026"
  ) +
  ggspatial::annotation_scale(
    location = "bl", height = unit(0.15, "cm")
  ) +
  ggspatial::annotation_north_arrow(
      location = "tl", which_north = "true",
      height = unit(1, "cm"), width = unit(1, "cm"),
      style = north_arrow_fancy_orienteering
  ) +
  my_theme

Pour cartographier des choroplètes, il suffit d’utiliser l’argument fill de l’esthétique de la couche :

Code
ggplot() +
  geom_sf(
    data = polygons,
    aes(fill = attrac_ratio),
    pch = 21
  ) +
  scale_fill_gradient2(
    name = "Ratio d'attractivité",
    low = "#4575B4",
    mid = "white",
    high = "#D73027",
    midpoint = 0,
    space = "Lab"
  ) +
  labs(
    title = "Taux d'attractivité des communes finistériennes",
    subtitle = "Les flux analysés correspondent aux données de migrations résidentielles entre<br>le lieu de résidence en 2020 et le lieu de résidence précédent",
    caption = "**Données :** Base flux de mobilité (INSEE, 2023); BDTOPO V3 (IGN, 2026). **Réalisation :** Antoine Le Doeuff, 2026"
  ) +
  ggspatial::annotation_scale(
    location = "bl", height = unit(0.15, "cm"), width = unit(0.15, "cm")
  ) +
  ggspatial::annotation_north_arrow(
      location = "tl", which_north = "true",
      height = unit(1, "cm"), width = unit(1, "cm"),
      style = north_arrow_fancy_orienteering
  ) +
  my_theme

Code
to_plot_vars <- c("Indice d'emmissivité" = "ie", "Indice d'attractivité" = "ia")
maps <- list()
for (name in names(to_plot_vars)) {
  short_name <- to_plot_vars[name]
  m <- ggplot() +
    geom_sf(
      data = polygons,
      aes(fill = .data[[short_name]]),
      color = "white",
      pch = 21
    ) +
    scale_fill_gradientn(
      name = name,
      colors = MexBrewer::mex.brewer("Frida"),
    ) +
    labs(title = name) +
    ggspatial::annotation_scale(
      location = "bl", height = unit(0.15, "cm")
    ) +
    ggspatial::annotation_north_arrow(
        location = "tl", which_north = "true",
        height = unit(1, "cm"), width = unit(1, "cm"),
        style = north_arrow_fancy_orienteering
    ) +
    theme_minimal() +
    theme(
      legend.position = "bottom",
      plot.title   = ggtext::element_markdown(family = "roboto", size = 16, face = "bold"),
      plot.subtitle = ggtext::element_markdown(family = "roboto", size = 12, lineheight = 1.2),
      axis.title.x = ggtext::element_markdown(family = "roboto", size = 12),
      axis.title.y = ggtext::element_markdown(family = "roboto", size = 12),
      plot.caption = ggtext::element_markdown(family = "roboto", size = 8, lineheight = 1.5)
    )
  maps[[short_name]] <- m
}
patchwork::wrap_plots(maps) +
  plot_annotation(
    title = "Indice d'émmissivité et d'attractivité des communes finistériennes",
    subtitle = "Les flux analysés correspondent aux données de migrations résidentielles entre<br>le lieu de résidence en 2020 et le lieu de résidence précédent",
    caption = "**Données :** Base flux de mobilité (INSEE, 2023); BDTOPO V3 (IGN, 2026). **Réalisation :** Antoine Le Doeuff, 2026",
    theme = theme(
      plot.title = element_text(face = "bold", size = 18),
      plot.subtitle = ggtext::element_markdown(size = 14, lineheight = 1.2),
      plot.caption = ggtext::element_markdown(size = 11)
    )
  )

  1. Pour une commune que vous choisirez, cartographiez ses relations préférentielles. Vous devrez d’abord convertir la matrice des relations préférentielles en data.frame :
Code
od_tidy <- as.data.frame(as.matrix(m_pref)) |>
  tibble::rownames_to_column("origin") |>
  tidyr::pivot_longer(
    cols = -origin,
    names_to = "destination",
    values_to = "rel_pref"
  )

# Les communes que je souhaites cartographier
coms_to_plot <- c(
  "Brest"      = "29019",
  "Quimper"    = "29232",
  "Morlaix"    = "29151",
  "Landerneau" = "29041"
)
# Seuil de définition d'un relation préférentielle. Les relations inférieure à
# ce seuil ne seront pas cartographiées.
th <- 1

# Filtre du df des rel. pref. pour ne garder que les communes sélectionnées
# Ne garder que les communes sélectionnées
to_plot <- filter(od_tidy, origin %in% coms_to_plot)
# On récupère les géométries
to_plot <- left_join(polygons, to_plot, by = c("code_insee" = "destination"))

maps <- list()
for (com_name in names(coms_to_plot)) {
  ci <- coms_to_plot[com_name]
  data_current_com <-
    filter(to_plot, origin == ci) |>
    select(origin, rel_pref)

  # discrétisation quantile
  breaks <- quantile(
    pull(filter(data_current_com, rel_pref > th), rel_pref),
    probs = seq(0, 1, by = 0.2),
    na.rm = TRUE
  )
  # Ajouter la légende pour les valeurs < th (on round pour la légende)
  breaks <- round(c(0, 1, breaks), 1)
  # Créer une variable avec le seuil des quantiles
  data_current_com <- data_current_com |>
    mutate(
      rel_pref_q = cut(
        round(rel_pref, 1), # Il faut round ici aussi
        breaks = unique(breaks),
        include.lowest = TRUE,
        dig.lab = 3
      )
    )

  m <- ggplot() +
    geom_sf(data = data_current_com, aes(fill = rel_pref_q)) +
    scale_fill_manual(
      name     = "**Intensité de la relation** (disc. quantile)",
      # Mettre une couleur partiuclière pour les valeurs < 1 (th)
      values   = c("ivory", RColorBrewer::brewer.pal(6, "Reds")),
      na.value = "grey90",
      guide = guide_legend(
        position       = "bottom",
        direction      = "horizontal",
        title.position = "top",
        nrow           = 2
      )
    ) +
    new_scale_fill() +
    geom_sf(
      data = filter(coms, nom_officiel == com_name),
      aes(fill = "Commune d'origine")
    ) +
    scale_fill_manual(
      name   = NULL,
      values = c("Commune d'origine" = "mediumturquoise"),
      guide  = guide_legend(position = "inside")
    ) +
    labs(title = com_name) +
    ggspatial::annotation_scale(location = "bl", height = unit(0.15, "cm")) +
    ggspatial::annotation_north_arrow(
      location = "tl", which_north = "true",
      height = unit(1, "cm"), width = unit(1, "cm"),
      style = north_arrow_fancy_orienteering
    ) +
    my_theme +
    theme(
      legend.position.inside      = c(0, 0.15),
      legend.justification.inside = c(0, 0),
      legend.key.size             = unit(0.4, "cm"),
      legend.text                 = element_text(family = "roboto", size = 8),
      legend.background           = element_rect(fill = NA, color = NA)
    )

  maps[[com_name]] <- m
}
patchwork::wrap_plots(maps, ncol = 2) +
  plot_annotation(
    title = "Relations préférentielles des principaux pôles urbains finistériens",
    subtitle = "Les flux analysés correspondent aux données de migrations résidentielles entre le lieu<br>de résidence en 2020 et le lieu de résidence précédent",
    caption = "**Données :** Base flux de mobilité (INSEE, 2023); BDTOPO V3 (IGN, 2026). **Réalisation :** Antoine Le Doeuff, 2026",
    theme = theme(
      plot.title = element_text(face = "bold", size = 18),
      plot.subtitle = ggtext::element_markdown(size = 14, lineheight = 1.2),
      plot.caption = ggtext::element_markdown(size = 11)
    )
  )

3.4.2.1 Les carte de flux sous R

Les cartes en cercles proportionnels ne sont pas vraiment appropriées quand on veut représenter des flux. On privilégiera des cartes en oursin ou des cartes de flux (Flow map) stricto sensu.

Carte de flux. Export des vins français en 1864 par Charles Joseph Minards.

Carte en oursin valué. La clientèle des services palois (services moteurs) Di Méo and Guérit (1992)
Di Méo, Guy, and Franck Guérit, eds. 1992. Chapitre II. Étude comparée de l’offre des services aux entreprises à Pau et à Bayonne : caractères et rayonnement géographique.” In La ville moyenne dans sa région : Pau, les Pays de l’Adour et l’Aquitaine, 79–115. Politiques urbaines. Pessac: Maison des Sciences de l’Homme d’Aquitaine. https://doi.org/10.4000/books.msha.15216.

ggplot2 ne possède pas de fonction pour concevoir ce genre de cartes. Rassurez vous, je ne vais pas vous demander d’en produire une ! À la place, on peut s’appuyer sur les packages mapsf et ttt.

install.packages("mapsf")
install.packages("cartograflow")
remotes::install_github("MiboraMinima/ttt")

On commence par effectuer un petit prétraitement pour pouvoir utiliser les fonctions de mapsf et ttt1.

1 la version que vous téléchargez est une version différente de la version originale du package. J’ai modifié la fonction qui génère la légende.

polygons$code_insee <- as.numeric(polygons$code_insee)
polygons$id <- polygons$code_insee
dtp <-
  select(dt_mobi_fin, -i, -j) |>
  mutate(i = as.numeric(codgeo), j = as.numeric(dcran), fij = nbflux_c20_pop01p) |>
  select(i, j, fij)

Par exemple, on peut cartographier l’ensemble les sortants pour une entité spatiale avec des flèches plus ou moins épaisses en fonction de la quantité échangée.

Code
# Paramètres ...................................................................
# Les flux associées à la commune
com_name <- "Brest"
# Le type de flux que l'on souhaite cartographier
type <- "sortant" # "entrant"
# Récupération du code INSEE associé à la commune
ci <- unique(pull(filter(coms, nom_officiel == com_name), code_insee))
# Seuil pour cartographier les flux
th <- 100
# Couleur des lignes
col <- "steelblue"
# Couleur du fond de carte
col_back <- "#E0E0E0"

# Carto ......................................................................
# Récupérer tous les flux entrants ou sortants associés à la commune
# et filrer les flux < au seuil.
if (type == "entrant") {
  dt_plot <- filter(dtp, j == as.numeric(ci), fij > th)
} else {
  dt_plot <- filter(dtp, i == as.numeric(ci), fij > th)
}

# Créer un df avec les communes qui échange des flux
polygons_f <-
  filter(polygons, id %in% unique(c(dt_plot$i, dt_plot$j))) |>
  select(id)

mf_map(
  coms,
  col = col_back,
  border = "black",
  lwd = 0.5
)
flows <- ttt_flowmapper(
  x = polygons_f,
  df = dt_plot,
  dfid = c("i", "j"),
  dfvar = "fij",
  xid = "id",
  size = "thickness",
  type = "arrows",
  decreasing = FALSE,
  # Configuration des lignes
  col = col,        # couleur des arcs
  border = "white", # couleur du contour
  lwd = 2,          # épaisseur du contour
  k = 15,           # augmenter ou diminuer l'épaisseur des arcs
  # Configuration des cercles
  col2 = "white", # couleur du cercle
  border2 = col,  # couleur du contour
  lwd2 = 0.7,     # épaisseur du contour
  k2 = 1e3L,      # augmenter ou diminuer la taille des cercles
  add = TRUE
)
# Ajouter la valeur du flux sur les arcs
# Si vous voulez les enlevez, commentez le code.
flows_txt <- mutate(flows$flows, fij = round(fij, 0)) |> distinct()
mf_label(
  flows_txt,
  var = "fij",
  halo = TRUE,
  cex = 0.7,
  col = "black",
  bg = "white",
  r = 0.1,
  overlap = FALSE,
  lines = FALSE
)
# La légende
ttt_flowmapperlegend(
  x = flows,
  title = "Flux",
  col = col,
  txtcol = 'black',
  title.cex = 0.6, # Taille du titre de la légende
  hshift = 3e3     # Espace entre le titre et légende
)
mf_title(
  txt = glue::glue("Flux {type} de {com_name}"),
  bg = "#d7d7d7", fg = "black", pos = "center"
)
mf_credits(
  txt = "Données : Base flux de mobilité (INSEE, 2023); BDTOPO V3 (IGN, 2026). Réalisation : Antoine Le Doeuff, 2026",
  col = "black", pos = "bottomright"
)
mf_arrow(col="black")
mf_scale(col="black", pos="bottomright", adj = c(0, 10))

Comme vous avez pu le constater, avec mapsf il existe des fonctions qui permettent de créer directement une flèche d’orientation (mf_arrow()) ou une échelle (mf_scale()).

Il s’agit d’un exemple. Vous pouvez bien sûr réaliser d’autres cartes qui prendraient en compte à la fois les flux entrants et sortants. Vous pourriez aussi cartographier les flux intra-communaux. À vous de modifier le code pour obtenir de telles cartes ! Référez vous à ce tuto pour voir comment procéder.

NoteInstructions
  1. Regardez les flux pour d’autres communes.
  2. Comparez les flux (entrants et sortants) de deux communes
  3. Cartographiez l’ensemble des flux sur le territoire.

Quand on a beaucoup de flux à cartographier, il est courant de filtrer les flux de moindre importance (par-ex. par quantile).

TipQuestion

Au regard des différentes analyses réalisées, que pouvez vous dire des migrations résidentielles en Finistère ?

3.5 Scripts complets

Le nombre de cartes pouvant être assez conséquent, je vous encourage à utiliser deux scripts pour organiser votre travail de manière optimale. Le premier est dédié au chargement et au prétraitement des données puis au calcul des indices. Les jeux de données générés dans ce premier script sont enregistrés dans le dossier data/res et chargés dans le deuxième script qui n’est dédié qu’à la cartographie. Vous pouvez bien sûr trouver une autre organisation ; il faut juste qu’elle soit cohérente.

scripts/process.R :

Code
################################################################################
# Script Name: process.r
# Description: Analyse descriptive des mobilités résidentielles en finistère
#              entre 2020 et l'années de résidence prédédent. Données de l'INSEE.
#              Ce script est dédié au chargement, au pré-traitement et au
#              calcul d'indices de flux.
# Author: Antoine Le Doeuff
################################################################################

library(readr)
library(happign)
library(sf)
library(dplyr)
library(stringi)
library(Matrix)

# //////////////////////////////////////////////////////////////////////////////
# Constante --------------------------------------------------------------------
# Où sont enregistré les données (vous, ce sera juste `data`)
DATA_DIR <- "02_flux/data"

# //////////////////////////////////////////////////////////////////////////////
# Charger les données ----------------------------------------------------------
# Migrations résidentielles ....................................................
dt_mobi <- read_delim(
  glue::glue("{DATA_DIR}/src/base_flux_mobilite_residentielle_2020.csv")
) |> janitor::clean_names()

# Communes .....................................................................
com_path <- "02_flux/data/src/com_finistere_bdtopov3_2026.gpkg"
if (file.exists(com_path)) {
  cli_alert_info("Le fichier existe déjà, chargement...")
  coms <- read_sf(com_path)
} else {
  coms <- get_wfs(
    x = NULL,
    layer = "BDTOPO_V3:commune",
    query = "code_insee LIKE '29%'"
  ) |>
    st_transform(2154)
  write_sf(coms, com_path)
}

# //////////////////////////////////////////////////////////////////////////////
# Prétraitements ---------------------------------------------------------------
# Filtrer les communes .........................................................
dt_mobi_pb <- filter(
  dt_mobi,
  startsWith(codgeo, "29"),
  startsWith(dcran, "29"),
  codgeo != dcran
)

# Conversion en matrice OD .....................................................
# Récupérer tous les code insee
all_ids <- sort(unique(c(dt_mobi_pb$codgeo, dt_mobi_pb$dcran)))

# Créer des facteurs avec les mêmes niveaux pour chaque O/D
dt_mobi_pb$i <- factor(dt_mobi_pb$dcran, levels = all_ids)
dt_mobi_pb$j <- factor(dt_mobi_pb$codgeo, levels = all_ids)

# Créer la sparse matrix (origin × destination)
M <- sparseMatrix(
  i = as.integer(dt_mobi_pb$i),
  j = as.integer(dt_mobi_pb$j),
  x = dt_mobi_pb$nbflux_c20_pop01p,
  dims = c(length(all_ids), length(all_ids)), # Passer les dimensions explicitement
  dimnames = list(all_ids, all_ids)
)
print(dim(M))

# //////////////////////////////////////////////////////////////////////////////
# Analyse des marges -----------------------------------------------------------
# Volume, arrival, departure ...................................................
# Flux total émis
sum_dep <- rowSums(M)

# Flux total reçus
sum_arr <- colSums(M)

# Volumes
volumes <- sum_dep + sum_arr

# Soldes
soldes <- sum_arr - sum_dep

# Ratio d'attractivité
attrac_ratio <- soldes / volumes

# Indices ......................................................................
tot <- sum(M)

# Indice d'attractivité
ia <- sum_arr / tot

# Indice d'emmissivité
ie <- sum_dep / tot

# Relations préférentielles ....................................................
# Flux totaux
tot <- sum(M)

# Calcul du flux attendu
expected <- (sum_dep %o% sum_arr) / tot
# Comparaison des flux observés au flux attendus
m_pref <- M / expected

# Remplacer la diagonale avec 0
diag(m_pref) <- 0
m_pref[!is.finite(m_pref)] <- 0

# //////////////////////////////////////////////////////////////////////////////
# Mise en forme de la donnée ---------------------------------------------------
# Créez un dataframe avec les indices
indices <- data.frame(
  code_insee = names(sum_dep),
  departure = sum_dep,
  arrival = sum_arr,
  volumes = volumes,
  soldes = soldes,
  attrac_ratio = attrac_ratio,
  ia = ia,
  ie = ie
)

# Ajouter la géométries des communes
indices_spa <- left_join(select(coms, code_insee), indices, by = "code_insee")

# //////////////////////////////////////////////////////////////////////////////
# Statistiques -----------------------------------------------------------------
# On calcul les statiques élémentaires des indices
indices_long <-
  tidyr::pivot_longer(
    indices,
    cols = !code_insee,
    names_to = "indices",
    values_to = "value"
  )

stat <- group_by(indices_long, indices) |>
  summarise(
    mean = mean(value),
    median = median(value),
    sd = sd(value),
    min = min(value),
    max = max(value)
  )

# //////////////////////////////////////////////////////////////////////////////
# Écriture des données ---------------------------------------------------------
# Le tableau des mobilités résidentielles filtées
write_csv(dt_mobi_pb, glue::glue("{DATA_DIR}/res/mobi_res_29.csv"))

# Les indices avec la géométrie
write_sf(indices_spa, glue::glue("{DATA_DIR}/res/indices_spa_29.gpkg"))

# La matrice des relations préférentielles
# Le format RDS permet de sauvegarder n'importe quel objet R qui est dans
# l'environnement. C'est un format exclusif à R.
saveRDS(m_pref, glue::glue("{DATA_DIR}/res/rel_pref.rds"))

scripts/maps.R :

Code
################################################################################
# Script Name: process.r
# Description: Analyse descriptive des mobilités résidentielles en finistère
#              entre 2020 et l'années de résidence prédédent. Données de l'INSEE.
#              Ce script est dédié à la cartograpie des indices calculés dans le
#              script `scripts/process.r`
# Author: Antoine Le Doeuff
################################################################################

library(readr)
library(sf)
library(dplyr)
library(Matrix)
library(ggplot2)
library(ggspatial)
library(ggnewscale)
library(patchwork)
library(ttt) # remotes::install_github("MiboraMinima/ttt")
library(mapsf)
library(ggtext)

# //////////////////////////////////////////////////////////////////////////////
# Constantes -------------------------------------------------------------------
# Où sont enregistré les données (vous, ce sera juste `data`)
DATA_DIR <- "02_flux/data"

# Où seront enregistrées vos cartes (vous, ce sera juste `figures`)
FIGURES_DIR <- "02_flux/figures"

# //////////////////////////////////////////////////////////////////////////////
# Chargement des données -------------------------------------------------------
# Le tableau des mobilités résidentielles filtées
dt_mobi_fin <- read_csv(glue::glue("{DATA_DIR}/res/mobi_res_29.csv"))

# Table des indices spatiaux
indices_spa <- read_sf(glue::glue("{DATA_DIR}/res/indices_spa_29.gpkg"))

# Communes du finistères
coms <- read_sf(glue::glue("{DATA_DIR}/src/com_finistere_bdtopov3_2026.gpkg"))

# Matrice des relations préférentielles
m_pref <- readRDS(glue::glue("{DATA_DIR}/res/rel_pref.rds"))

# //////////////////////////////////////////////////////////////////////////////
# Prétraitements ---------------------------------------------------------------
# Calcul des centroïdes (pour les cartes en cercles proportionnelles)
centroids <- st_centroid(indices_spa)

# //////////////////////////////////////////////////////////////////////////////
# Cartographie -----------------------------------------------------------------
# Themes .......................................................................
# Je crée un thème que j'appliquerai à toutes les cartes
sysfonts::font_add_google(name = "Roboto", family = "roboto")
theme_ggplot <-
  theme_minimal() +
  theme(
    legend.position = "bottom",
    # La font chargé dans `text` est automatiquement appliquée à tous les
    # autres texts de l'objet ggplot. Vous pouvez la changer manuellement
    # pour un paramètre en particulier si vous le shouaitez.
    text            = element_text(family = "roboto"),
    plot.title      = ggtext::element_markdown(size = 16, face = "bold"),
    plot.subtitle   = ggtext::element_markdown(size = 12, lineheight = 1.2),
    axis.title.x    = ggtext::element_markdown(size = 12),
    axis.title.y    = ggtext::element_markdown(size = 12),
    plot.caption    = ggtext::element_markdown(size = 8, lineheight = 1.5),
    legend.title    = ggtext::element_markdown(size = 8)
  )
# Theme pour patchwork
theme_patch <-
  theme(
    text          = element_text(family = "roboto"),
    plot.title    = element_text(face = "bold", size = 18),
    plot.subtitle = ggtext::element_markdown(size = 14, lineheight = 1.2),
    plot.caption  = ggtext::element_markdown(size = 11)
  )

# Plot departure, arrival, volumes .............................................
# On défini le nom complet de le la variable avec un named vector
selected_vars <- c(
  "Flux total reçus" = "arrival",
  "Flux total émis"  = "departure",
  "Volume"           = "volumes"
)

# On définit une liste vide (réceptacle qui va recevoir les cartes)
maps <- list()

# On itère sur les noms
for (name in names(selected_vars)) {
  # On récupère le nom présent dans le tableau à partir du nom complet
  short_name <-  selected_vars[name]

  # Créée la carte
  m <- ggplot() +
    geom_sf(data = coms) +
    geom_sf(
      data = centroids,
      aes(size = .data[[short_name]]),
      pch = 21, fill = "steelblue", color = "white"
    ) +
    scale_size(
      name = name,
      range = c(1, 9),
      breaks = scales::pretty_breaks(n = 5)
    ) +
    ggtitle(name) +
    ggspatial::annotation_scale(
      location = "bl", height = unit(0.15, "cm")
    ) +
    ggspatial::annotation_north_arrow(
        location = "tl", which_north = "true",
        height = unit(1, "cm"), width = unit(1, "cm"),
        style = north_arrow_fancy_orienteering
    ) +
    guides(
      size = guide_legend(
        direction = "horizontal",
        nrow = 1,
        label.position = "bottom"
      )
    ) +
    theme_ggplot

  maps[[name]] <- m
}
# Je combine les graphs et je fais la mise en page de la carte finale
map_volumes_emis_recus <- patchwork::wrap_plots(maps, ncol = 2) +
  plot_annotation(
    title = "Volumes et flux total émis et reçus des communes finistériennes",
    subtitle = "Les flux analysés correspondent aux données de migrations résidentielles entre<br>le lieu de résidence en 2020 et le lieu de résidence précédent",
    caption = "**Données :** Base flux de mobilité (INSEE, 2023); BDTOPO V3 (IGN, 2026). BDTOPO V3 (IGN, 2026). **Réalisation :** Antoine Le Doeuff, 2026",
    theme = theme_patch
  )
# J'enregistre la carte
ggsave(
  glue::glue("{FIGURES_DIR}/map_volumes_emis_recus.png"),
  map_volumes_emis_recus,
  width = 21 - 1, # Largeur en cm (A4 moins 1 cm pour la marge)
  height = 27 - 1,
  units = "cm", dpi = 300
)
# J'enregistre la carte en .eps ou svg ou pdf (pour pouvoir la modifier avec
# logiciel de DAO a prosteriori)
ggsave(
  glue::glue("{FIGURES_DIR}/map_volumes_emis_recus.eps"),
  map_volumes_emis_recus,
  width = 21 - 1, # Largeur en cm (A4 moins 1 cm pour la marge)
  height = 27 - 1,
  units = "cm"
)

# Solde ........................................................................
# Création de la taille des cercles à la main
fill_breaks <- c(-750, -500, -250, 0, 250, 500)
max_val <- max(abs(centroids$soldes), na.rm = TRUE)
legend_sizes <- scales::rescale(abs(fill_breaks), to = c(1, 9), from = c(0, max_val))

map_solde <- ggplot() +
  geom_sf(data = coms) +
  geom_sf(
    data = centroids,
    aes(size = abs(soldes), fill = soldes),
    pch = 21, color = "white"
  ) +
  # Cacher la taille dans la légende
  scale_size(range = c(1, 9), guide = "none") +
  scale_fill_gradient2(
    name = "Solde",
    low = "#4575B4",
    mid = "white",
    high = "#D73027",
    midpoint = 0,
    space = "Lab",
    breaks = fill_breaks,
    limits = c(-750, 500),
    guide = "legend"
  ) +
  guides(
    fill = guide_legend(
      direction = "horizontal",
      nrow = 1,
      label.position = "bottom",
      # Mettre la taille directement ici
      override.aes = list(size = legend_sizes)
    )
  ) +
  labs(
    title = "Solde des communes finistériennes",
    subtitle = "Les flux analysés correspondent aux données de migrations résidentielles entre<br>le lieu de résidence en 2020 et le lieu de résidence précédent",
    caption = "**Données :** Base flux de mobilité (INSEE, 2023); BDTOPO V3 (IGN, 2026). BDTOPO V3 (IGN, 2026). **Réalisation :** Antoine Le Doeuff, 2026"
  ) +
  ggspatial::annotation_scale(
    location = "bl", height = unit(0.15, "cm")
  ) +
  ggspatial::annotation_north_arrow(
      location = "tl", which_north = "true",
      height = unit(1, "cm"), width = unit(1, "cm"),
      style = north_arrow_fancy_orienteering
  ) +
  theme_ggplot
ggsave(
  glue::glue("{FIGURES_DIR}/map_solde.png"),
  map_solde,
  width = 20 - 1,
  height = 20 - 1,
  units = "cm", dpi = 300
)
ggsave(
  glue::glue("{FIGURES_DIR}/map_solde.pdf"),
  map_solde,
  width = 20 - 1,
  height = 20 - 1,
  units = "cm"
)

# Attractivity .................................................................
map_attrac_ratio <- ggplot() +
  geom_sf(
    data = indices_spa,
    aes(fill = attrac_ratio),  # use transformed color
    pch = 21
  ) +
  scale_fill_gradient2(
    name = "Ratio d'attractivité",
    low = "#4575B4",   # blue for negatives
    mid = "white",
    high = "#D73027",  # red for positives
    midpoint = 0,      # ensures 0 stays white
    space = "Lab"
  ) +
  labs(
    title = "Taux d'attractivité des communes finistériennes",
    subtitle = "Les flux analysés correspondent aux données de migrations résidentielles entre<br>le lieu de résidence en 2020 et le lieu de résidence précédent",
    caption = "**Données :** Base flux de mobilité (INSEE, 2023); BDTOPO V3 (IGN, 2026). BDTOPO V3 (IGN, 2026). **Réalisation :** Antoine Le Doeuff, 2026"
  ) +
  ggspatial::annotation_scale(
    location = "bl", height = unit(0.15, "cm")
  ) +
  ggspatial::annotation_north_arrow(
      location = "tl", which_north = "true",
      height = unit(1, "cm"), width = unit(1, "cm"),
      style = north_arrow_fancy_orienteering
  ) +
  theme_ggplot
ggsave(
  glue::glue("{FIGURES_DIR}/map_attrac_ratio.png"),
  map_attrac_ratio,
  width = 20 - 1,
  height = 20 - 1,
  units = "cm", dpi = 300
)
ggsave(
  glue::glue("{FIGURES_DIR}/map_attrac_ratio.eps"),
  map_attrac_ratio,
  width = 20 - 1,
  height = 20 - 1,
  units = "cm"
)

# Indices d'emmissivité et d'attractivité ......................................
to_plot_vars <- c("Indice d'emmissivité" = "ie", "Indice d'attractivité" = "ia")
maps <- list()
for (name in names(to_plot_vars)) {
  short_name <- to_plot_vars[name]
  m <- ggplot() +
    geom_sf(
      data = indices_spa,
      aes(fill = .data[[short_name]]),
      color = "white",
      pch = 21
    ) +
    scale_fill_gradientn(
      name = name,
      colors = MexBrewer::mex.brewer("Frida"),
    ) +
    labs(title = name) +
    ggspatial::annotation_scale(
      location = "bl", height = unit(0.15, "cm")
    ) +
    ggspatial::annotation_north_arrow(
        location = "tl", which_north = "true",
        height = unit(1, "cm"), width = unit(1, "cm"),
        style = north_arrow_fancy_orienteering
    ) +
    theme_minimal() +
    theme(
      legend.position = "bottom",
      plot.title   = ggtext::element_markdown(family = "roboto", size = 16, face = "bold"),
      plot.subtitle = ggtext::element_markdown(family = "roboto", size = 12, lineheight = 1.2),
      axis.title.x = ggtext::element_markdown(family = "roboto", size = 12),
      axis.title.y = ggtext::element_markdown(family = "roboto", size = 12),
      plot.caption = ggtext::element_markdown(family = "roboto", size = 8, lineheight = 1.5)
    )
  maps[[short_name]] <- m
}
map_ia_ie <- patchwork::wrap_plots(maps) +
  plot_annotation(
    title = "Indice d'émmissivité et d'attractivité des communes finistériennes",
    subtitle = "Les flux analysés correspondent aux données de migrations résidentielles entre<br>le lieu de résidence en 2020 et le lieu de résidence précédent",
    caption = "**Données :** Base flux de mobilité (INSEE, 2023); BDTOPO V3 (IGN, 2026). BDTOPO V3 (IGN, 2026). **Réalisation :** Antoine Le Doeuff, 2026",
    theme = theme_patch
  )
ggsave(
  glue::glue("{FIGURES_DIR}/map_ia_ie.png"),
  map_ia_ie,
  width = 21 - 1, # Largeur en cm (A4 moins 1 cm pour la marge)
  height = 27 - 1,
  units = "cm", dpi = 300
)
ggsave(
  glue::glue("{FIGURES_DIR}/map_ia_ie.eps"),
  map_ia_ie,
  width = 21 - 1, # Largeur en cm (A4 moins 1 cm pour la marge)
  height = 27 - 1,
  units = "cm"
)

# Relations préférentielles ....................................................
# Conversion de la matrice des relations préférentielles en data.frame
od_tidy <- as.data.frame(as.matrix(m_pref)) |>
  tibble::rownames_to_column("origin") |>
  tidyr::pivot_longer(
    cols = -origin,
    names_to = "destination",
    values_to = "rel_pref"
  )

# Les communes que je souhaites cartographier
coms_to_plot <- c(
  "Brest"      = "29019",
  "Quimper"    = "29232",
  "Morlaix"    = "29151",
  "Landerneau" = "29041"
)
# Seuil de définition d'un relation préférentielle. Les relations inférieure à
# ce seuil ne seront pas cartographiées.
th <- 1

# Filtre du df des rel. pref. pour ne garder que les communes sélectionnées
# Ne garder que les communes sélectionnées
to_plot <- filter(od_tidy, origin %in% coms_to_plot)
# On récupère les géométries
to_plot <- left_join(indices_spa, to_plot, by = c("code_insee" = "destination"))

maps <- list()
for (com_name in names(coms_to_plot)) {
  ci <- coms_to_plot[com_name]
  data_current_com <-
    filter(to_plot, origin == ci) |>
    select(origin, rel_pref)

  # discrétisation quantile
  breaks <- quantile(
    pull(filter(data_current_com, rel_pref > th), rel_pref),
    probs = seq(0, 1, by = 0.2),
    na.rm = TRUE
  )
  # Ajouter la légende pour les valeurs < th (on round pour la légende)
  breaks <- round(c(0, 1, breaks), 1)
  # Créer une variable avec le seuil des quantiles
  data_current_com <- data_current_com |>
    mutate(
      rel_pref_q = cut(
        round(rel_pref, 1), # Il faut round ici aussi
        breaks = unique(breaks),
        include.lowest = TRUE,
        dig.lab = 3
      )
    )

  m <- ggplot() +
    geom_sf(data = data_current_com, aes(fill = rel_pref_q)) +
    scale_fill_manual(
      name     = "**Intensité de la relation** (disc. quantile)",
      # Mettre une couleur partiuclière pour les valeurs < 1 (th)
      values   = c("ivory", RColorBrewer::brewer.pal(6, "Reds")),
      na.value = "grey90",
      guide = guide_legend(
        position       = "bottom",
        direction      = "horizontal",
        title.position = "top",
        nrow           = 2
      )
    ) +
    new_scale_fill() +
    geom_sf(
      data = filter(coms, nom_officiel == com_name),
      aes(fill = "Commune d'origine")
    ) +
    scale_fill_manual(
      name   = NULL,
      values = c("Commune d'origine" = "mediumturquoise"),
      guide  = guide_legend(position = "inside")
    ) +
    labs(title = com_name) +
    ggspatial::annotation_scale(location = "bl", height = unit(0.15, "cm")) +
    ggspatial::annotation_north_arrow(
      location = "tl", which_north = "true",
      height = unit(1, "cm"), width = unit(1, "cm"),
      style = north_arrow_fancy_orienteering
    ) +
    theme_ggplot +
    theme(
      legend.position.inside      = c(0, 0.15),
      legend.justification.inside = c(0, 0),
      legend.key.size             = unit(0.4, "cm"),
      legend.text                 = element_text(family = "roboto", size = 8),
      legend.background           = element_rect(fill = NA, color = NA)
    )

  maps[[com_name]] <- m
}
map_rel_pref <- patchwork::wrap_plots(maps, ncol = 2) +
  plot_annotation(
    title = "Relations préférentielles des principaux pôles urbains finistériens",
    subtitle = "Les flux analysés correspondent aux données de migrations résidentielles entre le lieu<br>de résidence en 2020 et le lieu de résidence précédent",
    caption = "**Données :** Base flux de mobilité (INSEE, 2023); BDTOPO V3 (IGN, 2026). BDTOPO V3 (IGN, 2026). **Réalisation :** Antoine Le Doeuff, 2026",
    theme = theme_patch
  )
ggsave(
  glue::glue("{FIGURES_DIR}/map_rel_pref.png"),
  map_rel_pref,
  width = 21 - 1,
  height = 27 - 1,
  units = "cm", dpi = 300
)
ggsave(
  glue::glue("{FIGURES_DIR}/map_rel_pref.eps"),
  map_rel_pref,
  width = 21 - 1,
  height = 27 - 1,
  units = "cm"
)

# Flux map .....................................................................
# [Prétraitements]
indices_spa$code_insee <- as.numeric(indices_spa$code_insee)
indices_spa$id <- indices_spa$code_insee
dtp <-
  select(dt_mobi_fin, -i, -j) |>
  mutate(i = as.numeric(codgeo), j = as.numeric(dcran), fij = nbflux_c20_pop01p) |>
  select(i, j, fij)

# [Paramètres]
# Les flux associées à la commune
com_name <- "Brest"
# Le type de flux que l'on souhaite cartographier
type <- "sortant" # "entrant"
# Récupération du code INSEE associé à la commune
ci <- unique(pull(filter(coms, nom_officiel == com_name), code_insee))
# Seuil pour cartographier les flux
th <- 100
# Couleur des lignes
col <- "steelblue"
# Couleur du fond de carte
col_back <- "#E0E0E0"

# [Carto]
# Récupérer tous les flux entrants ou sortants associés à la commune
# et filrer les flux < au seuil.
if (type == "entrant") {
  dt_plot <- filter(dtp, j == as.numeric(ci), fij > th)
} else {
  dt_plot <- filter(dtp, i == as.numeric(ci), fij > th)
}

# Créer un df avec les communes qui échange des flux
indices_spa_f <-
  filter(indices_spa, id %in% unique(c(dt_plot$i, dt_plot$j))) |>
  select(id)

# On enregistre ici l'endroit où va être sauvegardé la carte
mf_png(
  x = coms,
  filename = glue::glue("{FIGURES_DIR}/map_oursin_{type}_{com_name}.png"),
  width = 20 - 1, height = 20 - 1, unit="cm"
)
mf_map(
  coms,
  col = col_back,
  border = "black",
  lwd = 0.5
)
flows <- ttt_flowmapper(
  x = indices_spa_f,
  df = dt_plot,
  dfid = c("i", "j"),
  dfvar = "fij",
  xid = "id",
  size = "thickness",
  type = "arrows",
  decreasing = FALSE,
  # Configuration des lignes
  col = col,        # couleur des arcs
  border = "white", # couleur du contour
  lwd = 2,          # épaisseur du contour
  k = 15,           # augmenter ou diminuer l'épaisseur des arcs
  # Configuration des cercles
  col2 = "white", # couleur du cercle
  border2 = col,  # couleur du contour
  lwd2 = 0.7,     # épaisseur du contour
  k2 = 1e3L,      # augmenter ou diminuer la taille des cercles
  add = TRUE
)
# Ajouter la valeur du flux sur les arcs
# Si vous voulez les enlevez, commentez le code.
flows_txt <- mutate(flows$flows, fij = round(fij, 0)) |> distinct()
mf_label(
  flows_txt,
  var = "fij",
  halo = TRUE,
  cex = 0.7,
  col = "black",
  bg = "white",
  r = 0.1,
  overlap = FALSE,
  lines = FALSE
)
# La légende
ttt_flowmapperlegend(
  x = flows,
  title = "Flux",
  col = col,
  txtcol = 'black',
  title.cex = 0.6, # Taille du titre de la légende
  hshift = 3e3     # Espace entre le titre et légende
)
mf_title(
  txt = glue::glue("Flux {type} de {com_name}"),
  bg = "#d7d7d7", fg = "black", pos = "center"
)
mf_credits(
  txt = "Données : Base flux de mobilité (INSEE, 2023); BDTOPO V3 (IGN, 2026). BDTOPO V3 (IGN, 2026). Réalisation : Antoine Le Doeuff, 2026",
  col = "black", pos = "bottomright"
)
mf_arrow(col="black")
mf_scale(col="black", pos="bottomright", adj = c(0, 10))
dev.off() # Indique l'enregistrement de la carte avec les paramètres indiqués dans mf_png()

3.6 Références et ressources

Ressources :

Faire des cartes de flux sur QGIS ?

  • Sur QGIS, on peut utiliser le plugin : mmqgis. Vous trouverez un tuto réalisé par des étudiants du master SIGAT ici.