Election

Cartogram
R
spatial
webscraping
Author

Michaël

Published

2024-06-12

Modified

2024-06-12

Paris, France. French and European flags floating in the wind on the Élysée palace entrance gates

French and European flags – CC-BY-NC-ND by Ibrahim Ajaja / World Bank

Election maps are hard: Land doesn’t vote, people do !. So most maps made on these occasions (generally choropleths) are possibly misleading.

Figure 1: Carte des communes de France selon la couleur de la liste arrivée en tête aux élections – Le Monde

A solution is a cartogram, however we generally only see the winner of each constituency.

Figure 2: Each municipality was transformed into a dot, with the area of the dot proportional to the number of voters 👉 a more accurate representation of voting patterns – Karim Douieb

So it’s biased again, especially when several parties have very similar scores.

Here I show a cartogram of the main parties (those who got at least one deputy), aggregated by département; so each département has generally 5 dots whose area is proportional to the number of votes.

Setup

Some packages are needed…

library(tidyverse)
library(sf)
library(xml2)
library(glue)
library(janitor)
library(cartogram)
library(arrow)
library(colorspace)

Data

Provisional results by communes or département are only available in (many) XML files1, so some data wrangling is necessary.

I get the list of département from INSEE to iterate on.

Département polygons come from Adminexpress COG Carto, simplified beforehand with Mapshaper.

dep <- read_csv("https://www.insee.fr/fr/statistiques/fichier/7766585/v_departement_2024.csv") |>
  clean_names()

dep_aex <- read_sf("DEPARTEMENT.shp") |>
  st_transform("EPSG:2154") |> 
  st_make_valid() |>
  clean_names()

And the 101 XML files are scraped from the Ministère de l’intérieur. We get the results for 34,929 communes.

if (!file.exists("resultats.parquet")) {
  extraire_dep <- function(dep, reg,...) {
    message(glue("{dep}"))
    read_xml(glue("https://www.resultats-elections.interieur.gouv.fr/telechargements/EU2024/resultatsT1/{reg}/{dep}/R1{dep}COM.xml")) |>
      as_list() |> 
      pluck("Election", "Departement", "Communes") |> 
      map(as_tibble) |> 
      list_rbind() |> 
      unnest_wider(Tours) |> 
      unnest_wider(Resultats) |> 
      unnest_wider(Mentions) |> 
      unnest_wider(Abstentions) |> 
      rename(n_abstentions = Nombre) |> 
      select(-RapportInscrits) |> 
      unnest_wider(Votants) |> 
      rename(n_votants = Nombre) |> 
      select(-RapportInscrits) |>
      unnest_wider(Blancs) |> 
      rename(n_blancs = Nombre) |> 
      select(-RapportInscrits, -RapportVotants) |>
      unnest_wider(Nuls) |> 
      rename(n_nuls = Nombre) |> 
      select(-RapportInscrits, -RapportVotants) |>
      unnest_wider(Exprimes) |> 
      rename(n_exprimes = Nombre) |> 
      select(-RapportInscrits, -RapportVotants) |>
      unnest_longer(Listes) |> 
      unnest_wider(Listes) |> 
      mutate(across(everything(), unlist))
  }
  
  resultats <- dep |>
    pmap(safely(extraire_dep, quiet = FALSE),
         .progress = "   Import") |> 
    map(~ pluck(.x, "result")) |> 
    list_rbind() |> 
    clean_names() |> 
    mutate(across(c(inscrits:n_exprimes, nb_voix), parse_integer),
           across(c(rapport_exprimes, rapport_inscrits),
                  ~ parse_number(.x, locale = locale(decimal_mark = ",")))) |>
    select(-listes_id)
  
  write_parquet(resultats, "resultats.parquet")
  date_fichier <- Sys.Date()
} else {
  resultats <- read_parquet("resultats.parquet")
  date_fichier <- as.Date(file.info("resultats.parquet")$mtime)
}

Download it here, in parquet format.

Processing

We aggregate some political parties together after keeping only the parties having at least one deputy, and compute the votes by département (only for metropolitan France, sorry).

nuances_deputes_elus <- tribble(
  ~cod_nua_liste, ~affiliation,     ~couleur,
  "LRN",          "Extrême droite", "tan4",
  "LENS",         "Centre",         "orange3",
  "LUG",          "Gauche",         "violetred3",
  "LFI",          "Gauche",         "violetred3", 
  "LLR",          "Droite",         "lightblue3",
  "LVEC",         "Verts",          "darkgreen",
  "LREC",         "Extrême droite", "tan4")

res_carto <- resultats |> 
  filter(cod_nua_liste %in% nuances_deputes_elus$cod_nua_liste,
         cod_com < "971",
         nb_voix > 0) |> 
  inner_join(nuances_deputes_elus,
             join_by(cod_nua_liste)) |> 
  group_by(affiliation,
           dep = str_sub(cod_com, 1, 2))  |> 
  summarise(nb_voix = sum(nb_voix),
            .groups = "drop")

Several methods are available to make a cartogram; I’ll chose a Dorling cartogram. The {cartogram} package does its hard work:

resultats_dorling <- dep_aex |>
  filter(insee_dep < "971") |> 
  left_join(res_carto,
            join_by(insee_dep == dep)) |> 
  mutate(affiliation = factor(affiliation, 
                              levels = c(
                                "Gauche",        
                                "Verts",    
                                "Centre",        
                                "Droite",        
                                "Extrême droite"))) |> 
  arrange(affiliation, insee_dep) |> 
  cartogram_dorling("nb_voix", k = .6)

Map

And we map the result:

palette_affiliation <- nuances_deputes_elus |> 
  distinct(affiliation, couleur) |> 
  deframe()

dep_aex |>
  filter(insee_dep < "971") |> 
  ggplot() +
  geom_sf(fill = NA, color = "#aaaaaa") +
  geom_sf(data = resultats_dorling,
          aes(fill = affiliation, 
              color = affiliation),
          alpha = 0.7) +
  scale_fill_manual(values = palette_affiliation) +
  scale_color_manual(values = set_names(darken(palette_affiliation, 0.2),
                                        names(palette_affiliation))) +
  labs(title = "Élections europénnes 2024",
       subtitle = "France métropolitaine",
       fill = "",
       color = "",
       caption = glue("données MIOM {date_fichier}
                  filtre sur les nuances ayant élu des députés
                  https://r.iresmi.net/ {Sys.Date()}")) +
  guides(fill = guide_legend(label.position = "bottom")) +
  theme_void() +
  theme(legend.position = "bottom",
        legend.key.width = unit(1.5, "cm"),
        legend.key.height = unit(.5, "cm"),
        legend.text = element_text(size = 8),
        plot.caption = element_text(size = 7))
Cartogram for the main political parties
Figure 3: 2024 European election in France

Not perfect but maybe more faithful…

Footnotes

  1. It seems that a nicer dataset is now available.↩︎