Election
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Election maps are hard: Land doesn’t vote, people do !
. So most maps made on these occasions (generally choropleths) are possibly misleading.
A solution is a cartogram, however we generally only see the winner of each constituency.
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))
Not perfect but maybe more faithful…
Footnotes
It seems that a nicer dataset is now available.↩︎
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.