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.
<- read_csv("https://www.insee.fr/fr/statistiques/fichier/7766585/v_departement_2024.csv") |>
dep clean_names()
<- read_sf("DEPARTEMENT.shp") |>
dep_aex 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")) {
<- function(dep, reg,...) {
extraire_dep 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))
}
<- dep |>
resultats 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")
<- Sys.Date()
date_fichier else {
} <- read_parquet("resultats.parquet")
resultats <- as.Date(file.info("resultats.parquet")$mtime)
date_fichier }
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).
<- tribble(
nuances_deputes_elus ~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")
<- resultats |>
res_carto filter(cod_nua_liste %in% nuances_deputes_elus$cod_nua_liste,
< "971",
cod_com > 0) |>
nb_voix 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:
<- dep_aex |>
resultats_dorling 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:
<- nuances_deputes_elus |>
palette_affiliation 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.↩︎