This analysis of the EP election results 2024 is structured as follows:
1. The EU map shows the Member States coloured according to the winning EP group.
2. Distribution of votes among EP groups
3a. Total electorate share without EP representation:
- share of votes to parties below the representation threshold
- share of electors who abstained
3b. Number of voters represented by each parliamentary seat
The election results data are downloaded from https://results.elections.europa.eu/ as follows:
The situation depicted below is the one as of 16 July 2024.
EPgroups <- groups[[1]][[1]] #includes 'Others'
## lists Party id and acronym 2 be linked to national results file extracted from filed
pparties <- tibble(NatParties = parties$countries$candidates)
NationalParties <- pparties |>
unnest_longer(NatParties) |>
unnest_wider(NatParties) |>
select(candidateId, candidateAcronym)
NationalParties <- unique(NationalParties) # to avoid repetition of the belgian constituencies.
#i - turnout by MS
TO24byMS <- turnout[["years"]][["turnoutByYear.turnoutByCountry"]][[16]]
#ii - share of votes without EPg representation(seatsTotal == 0)
Natres <- tibble(nres=filesd)
notRpSh <- as_data_frame(Natres |>
unnest_wider(nres) |>
unnest_wider(partySummary) |>
unnest_longer(seatsByParty) |>
select(9)) |> filter(seatsByParty$seatsTotal == 0)
notRpSh <- as_data_frame(cbind(notRpSh[[1]][[1]],notRpSh[[1]][[4]]))
colnames(notRpSh) <- c("partyId","votesPercent")
notRpSh$MS <- str_sub(notRpSh$partyId, 1,2)
notRpSh$votesPercent <- as.numeric(notRpSh$votesPercent)
## Per MS
ngNotRpSh <- group_by(notRpSh,MS) |>
summarise(natNRSh = sum(votesPercent)) |>
arrange(desc(natNRSh))
#iii - Number of EP seats by MS
SeatbyMS <- Natres |>
unnest_wider(nres) |>
select(id, seatsTotal, seatsPercentEU)
colnames(SeatbyMS) <- c("MS", "sTotal", "sPercentEU")
#iv - EP group distribution by MS
MSres <- Natres |>
unnest_wider(nres) |>
unnest_wider(groupSummary) |>
unnest_longer(groupDistribution) |>
select(1,9, 10)
MSres |> select(!partySummary) -> MSresbyEPg
colnames(MSresbyEPg) <- c("MS", "groupD")
MSresbyEPg <- MSresbyEPg |> unnest_wider(groupD) ## seats by MS and epgroup
colnames(MSresbyEPg) <- c("MS", "EPg", "seatsEP", "seatsPercentg")
rm(Natres)
#associate color upon palette #EPP, SD, Renew, GREENSEFA, ECR, ID, Theleft, Others, NI
EPPolG <- c("#0155A0", "#FF0000", "#0099FF", "#008200", "#1866a5", "#0077c8", "#e42527", "#666666", "#999999")
lbs <- c( "EPP", "SD", "Renew", "GREENSEFA", "ECR", "PatriotsforEurope", "Theleft", "Others", "NI")
cols <- c("EPP" = "#0155A0", "SD" = "#FF0000", "Renew" = "#0099FF", "GREENSEFA" = "#008200", "ECR" ="#1866a5", "PatriotsforEurope" = "#0077c8", "Theleft" = "#e42527", "Others" = "#666666", "NI" = "#999999")
The map is coloured according to the the EP group who won the most seats
library(tidyverse)
library(ggiraph)
library(giscoR)
library(sf)
countries <- gisco_get_countries(year = "2020", epsg = "3035")
#Custom bbox
bb <- sf::st_bbox(countries)
bb[1] <- 2377294
bb[2] <- 533597
bb[3] <- 6559800
bb[4] <- 5628510
countries_cp <- sf::st_crop(countries, bb)
epGWon <- MSresbyEPg |>
group_by(MS) |>
slice(which.max(seatsEP)) |>
ungroup()
epGWon$colr <- case_when(
epGWon$EPg == "EPP" ~ EPPolG[1],
epGWon$EPg == "SD" ~ EPPolG[2],
epGWon$EPg == "Renew" ~ EPPolG[3],
epGWon$EPg == "GREENSEFA" ~ EPPolG[4],
epGWon$EPg == "ECR" ~ EPPolG[5],
epGWon$EPg == "PatriotsforEurope" ~ EPPolG[6],
epGWon$EPg == "Theleft" ~ EPPolG[7],
epGWon$EPg == "Others" ~ EPPolG[8],
epGWon$EPg == "NI" ~ EPPolG[9],
)
epGWon <- epGWon |> left_join(SeatbyMS)
#add geo
epGWongeo <- epGWon |> right_join(countries_cp, by = c("MS"= "CNTR_ID")) |> st_as_sf()
winMap <- ggplot(data = epGWongeo) +
#map bkgrnd
geom_sf(
data = countries_cp,
fill = "#ffffff",
color = "#8e9dae",
linewidth = 0.15,
show.legend = F
) +
#map winning party
geom_sf_interactive(
mapping = aes(
fill = EPg,
tooltip = paste0(EPg, " - ", as.character(seatsEP), c(" out of "), sTotal), data_id = MS),
color = "#ffffff",
linewidth = 0.15,
show.legend = F
) +
#manual color scale
scale_fill_manual(
values = cols,
labels = lbs,
name = "Political groups in the EP",
guide = guide_legend(
keyheight = unit(2.5, units = "mm"),
title.position = "top",
reverse = F)) +
labs(x=NULL,
y=NULL,
title = "<span style = 'font-size:14pt; font-family:Optima'>Number of seats won by most voted group</span>") +
theme_light() +
theme(plot.title = element_markdown(size = rel(1.4), lineheight = 1.2))
girafe(ggobj = winMap,
options = list(
opts_hover(css=''),
opts_hover_inv(css = "opacity:0.6"),
opts_sizing(rescale = FALSE)),
height_svg = 8,
width_svg = 12
)
library(htmltools)
#Distributions among EP groups: MSresbyEPg; EP groups colours
MSresbyEPg$colr <- case_when(
MSresbyEPg$EPg == "EPP" ~ EPPolG[1],
MSresbyEPg$EPg == "SD" ~ EPPolG[2],
MSresbyEPg$EPg == "Renew" ~ EPPolG[3],
MSresbyEPg$EPg == "GREENSEFA" ~ EPPolG[4],
MSresbyEPg$EPg == "ECR" ~ EPPolG[5],
MSresbyEPg$EPg == "PatriotsforEurope" ~ EPPolG[6],
MSresbyEPg$EPg == "Theleft" ~ EPPolG[7],
MSresbyEPg$EPg == "ESN" ~ EPPolG[8],
MSresbyEPg$EPg == "NI" ~ EPPolG[9],
)
#Ordering MS by number of seats
OrdMSp <- SeatbyMS |> arrange(sTotal)
OrdMSp$OrdMS <- factor(OrdMSp$MS, levels=OrdMSp$MS)
MSresbyEPg <- MSresbyEPg |> mutate(tTp= paste0(MSresbyEPg$MS, " - ", MSresbyEPg$EPg, ": ", MSresbyEPg$seatsEP)) #tooltip text
MSresbyEPg$ordMS <- factor(MSresbyEPg$MS, levels = OrdMSp$OrdMS )
MSresbyEPg <- MSresbyEPg |> arrange(ordMS, ordered = TRUE)
gplot <-ggplot(MSresbyEPg, aes(x= ordMS, y = seatsEP, fill = EPg)) +
geom_bar_interactive(stat = "identity",
position = "stack",
aes(tooltip = tTp,
data_id = MS),
show.legend = F) +
#manual color scale
scale_fill_manual(
values = cols,
labels = lbs,
name = "Political groups in the EP",
guide = "none") +
theme_light() +
labs(x = NULL,
y = NULL,
title = "<span style = 'font-size:14pt; font-family:Optima'>Number of seats by EP political group <span style='color:#0155A0;'> EPP</span>, <span style='color:#FF0000;'> SD</span>, <span style='color:#0099FF;'> Renew</span>, <span style='color:#008200;'> Greens/EFA</span>, <span style='color:#1866A5;'> ECR</span>, <span style='color:#0077C8;'> Patriots for Europe</span>, <span style='color:#E42527;'> The Left</span>, <span style='color:#666666;'> ESN</span>, <span style='color:#999999;'> NI</span>")+
theme_light() +
theme(plot.title = element_markdown(size = rel(1.4), lineheight = 1.2))
girafe(ggobj=gplot,
options=list(
opts_hover(css=''),
opts_hover_inv(css = "opacity:0.6"),
opts_sizing(rescale = FALSE)),
#heigth_svg = 8,
width_svg = 12
)
Not everyone who can vote does vote, but only those who vote get represented. Among the voted parties, only those who get more than a fixed share of the total votes get a seat, while the votes of those with a smaller share get lost. More about it on this EPRS briefing. The chart below shows for each country the share of voters who do not get a seat representing their choice.
ngNotRpSh <- ngNotRpSh |> left_join(TO24byMS, by=c("MS" = "countryId"))
#Abstention share (100 - #Turnout share)
ngNotRpSh$AbstSh <- (100 - ngNotRpSh$percent)
#Not represented share: converting from share of voters to share of all electors
ngNotRpSh$nrSh <- round(ngNotRpSh$natNRSh*(ngNotRpSh$percent/100),2)
ngNotRpSh$Total <- ngNotRpSh$AbstSh + ngNotRpSh$nrSh
colnames(ngNotRpSh) <- c("MS", "nrPartiesSh", "status", "time", "TurnOutSh", "AbstentionSh", "nrVotersSh", "nrTotalSh" )
#MS ordered by total share of representation
NROrd <- ngNotRpSh %>% arrange(nrTotalSh)
NROrd$OrdMS <- factor(NROrd$MS, levels = NROrd$MS)
nr2P <- NROrd |>
select(OrdMS, nrVotersSh, AbstentionSh) |>
pivot_longer(
cols = c("nrVotersSh", "AbstentionSh"),
names_to = "Abs_NoRep",
values_to = "share")
nr2P <- nr2P |> mutate(tTp= paste0(OrdMS, " - ", case_when(
Abs_NoRep == "AbstentionSh" ~ "Abstention",
Abs_NoRep == "nrVotersSh"~ "Small party"),
": ", share)) #tooltip text
P12p <- ggplot(nr2P, aes(x = share, y = OrdMS, fill = Abs_NoRep, data_id = OrdMS)) +
geom_bar_interactive(stat = "identity", position = "stack",
aes(tooltip = tTp, data_id = OrdMS),
show.legend = F) +
#manual color scale
scale_fill_manual(
values = c("AbstentionSh" = "#fdae6b", "nrVotersSh" = "#3182bd"),
labels = c("Abstained", "Party not represented"),
name = "Share of not represented electors ",
guide = "none") +
labs(x = NULL,
y = NULL,
title = "<span style = 'font-size:14pt; font-family:Optima'>Part of the electorate not represented in the EP, because of<span style='color:#fdae6b;'> abstention </span> or <span style='color:#3182bd;'> small party</span> vote") +
theme_light() +
theme(plot.title = element_markdown(size = rel(1.4), lineheight = 1.2))
girafe(ggobj=P12p,
options=list(
opts_hover(css=''),
opts_hover_inv(css = "opacity:0.6"),
opts_sizing(rescale = FALSE)),
#heigth_svg = 8,
width_svg = 12
)
The number of Members of the European Parliament is fixed according to the population share of each State and to the principle of degressive proportionality, making it so that every Parliamentarian from a small country represents a smaller number of citizens than one from a bigger state. In practice, due to the abstention shares and to the votes to parties below the electoral threshold, the representation ratio of Members of the European Parliament varies according to their nationality. The chart below shows the EU member states ordered upon their representation ratio (Number of EP members divided among the number of voters who either did not abstain or voted for a non represented party). For comparison the grey circles show the representation ratio when taking into account the total electorate, or citizens with a right to vote, including both abstainers and voters.
library(eurostat)
#potential voters
#pvt <- get_eurostat("demo_popep")# 410 error: download&read
pvt <- read_delim("estat_demo_popep.tsv", delim = "," ,col_names = TRUE)
colnames(pvt)[5] <- "geoTime"
pvt_df <- as_data_frame(pvt) |>
select(vot_cat, geoTime) |>
mutate(MS = str_sub(geoTime,1,2)) |>
mutate(persons = as.numeric(str_sub(geoTime,4, str_length(geoTime)-2))) |>
filter(vot_cat != "FRST")
rm(pvt)
#Representation rate = Seats/(Voters-(NotRepresented*Voters)/100000)
RepSh <- pvt_df |> select(MS, persons) |>
left_join(ngNotRpSh) |>
left_join(SeatbyMS) |>
#not represented voters, voters by seats(Representativity share), electors by seats Seats share
mutate(nrVtrs = round(nrTotalSh/100 * persons, 0)) |>
mutate(RpSh = round(((persons - nrVtrs)/sTotal)/1000,0)) |>
mutate(StSh = round(((persons)/sTotal)/1000,0))
#"Representation ratio"mutate(RpSh = round((sTotal/(persons - nrVtrs))*1000000,2))
#MS ordered by total share of representation
RpShOrd <- RepSh |>
select(colnames(RepSh)[c(1,2, 9,10, 12, 13,14)]) |>
arrange(RpSh)
RpShOrd$OrdMS <- factor(RpShOrd$MS, levels = RpShOrd$MS)
Tbl <- RpShOrd |> select(c(1:7))
colnames(Tbl) <- c("Country code", "Electors number", "Not represented %", "Seats", "Not represented number", "Represented/Seats ratio", "Electors/Seats ratio")
printTbl <- DT::datatable(select(Tbl,c(1,4,2,5,3,6,7)),
caption = "",
rownames = FALSE, filter = "none"
)
RpShOrd <- RpShOrd|>
mutate(tTp = paste0(
OrdMS, " - ", "\\\n",
"Representation ratio: ", RpSh, "\\\n",
"Not represented share: ", nrTotalSh)) |> #tooltip text
mutate(ms_cat = case_when(
nrTotalSh/10 >= 6 ~ "big",
nrTotalSh/10 >= 4 ~ "medium big",
nrTotalSh/10 >= 2 ~ "medium small",
nrTotalSh/10 >= 1 ~ "small" )
)#not represented share categorised
Rp_pt <- ggplot(RpShOrd,aes(x = OrdMS, y = RpSh, colour = ms_cat, data_id = OrdMS)) +
geom_point_interactive(aes(size = nrTotalSh/10,
tooltip = tTp)) +
#manual color scale
scale_colour_manual(
values = c("small" = "#465775" , "medium small" = "#3182bd","medium big" = "#fdae6b","big" = "#cb4f25" ),
guide = "none") +
#manual color scale
scale_fill_manual(
values = c("small" = "#465775" , "medium small" = "#3182bd","medium big" = "#fdae6b","big" = "#cb4f25" ),
guide = "none") +
#manual size scale
scale_size_area(
#values = c("small" = 2 , "medium small" = 3,"medium big" = 4, "big" = 5),
guide = "none") +
geom_point(aes(x = OrdMS, y = StSh, colour = "black", fill = "white", size = nrTotalSh/10)) +
labs(x = NULL,
y = NULL,
title = "<span style = 'font-size:14pt; font-family:Optima'>Number of voters (1 000) represented by each parliamentary seat</span>",
caption = "<span style = 'font-size:12pt; font-family:Optima;'> Share of not represented electors: </span> <span style= color:#465775;'>Small, </span> <span style='color:#3182bd;'>medium small, </span> <span style='color:#fdae6b;'>medium big, </span> <span style='color:#cb4f25;'>big " ) +
theme_light() +
theme(plot.title = element_markdown(size = rel(1.4), lineheight = 1.2),
plot.caption = element_markdown(size = rel(1.2), lineheight = 1.2))
girafe(ggobj=Rp_pt,
options=list(
opts_hover(css=''),
opts_hover_inv(css = "opacity:0.6"),
opts_sizing(rescale = FALSE)),
#heigth_svg = 8,
width_svg = 12
)
printTbl