Code
library(conflicted) # create errors for function name conflicts
library(tidyverse)
conflicts_prefer(dplyr::filter, .quiet = TRUE)
library(knitr) # layout // tables in RMarkdown with kable()
ggplot2::theme_set(theme_void())
options(scipen = 999) # suppress scientific notation
library(estimatr) # robust standard errors
library(patchwork) # arrrange plots
library(sf) # plot maps and gespatial tools
crs_deu <- "+proj=lcc +lat_1=48 +lat_2=53 +lat_0=51 +lon_0=10" # LCC Germany parameters
Source — The Federal Returning Officer (“Der Bundeswahlleiter”): Bundestag election 2021
- Results constituencies
- © Der Bundeswahlleiter, Statistisches Bundesamt, Wiesbaden 2021
- Geometric data of constituencies
- © Der Bundeswahlleiter, Statistisches Bundesamt, Wiesbaden 2020, Wahlkreiskarte für die Wahl zum 20. Deutschen Bundestag, Basis of the geological information © Geobasis-DE / BKG 2020
Code
btw_raw <- read_csv("data/btw21-results-party.csv")
party_level <- c("AfD", "CDU", "CSU", "FDP", "SPD", "GRÜNE", "DIE LINKE")
btw_dt <-
btw_raw |>
mutate(party = fct_relevel(party, party_level))
btw_tier1 <- btw_dt |> filter(level == "district", votes_type == "Erststimmen")
btw_tier2 <- btw_dt |> filter(level == "district", votes_type == "Zweitstimmen")
mp_raw <- read_csv("data/btw21-gewaehlte-wahlkreis.csv")
mp <-
mp_raw |>
mutate(party = fct_relevel(party, party_level))
wk_shp <-
read_rds("data/btw21-districts-geometry.rds") |>
left_join(mp |> distinct(wkr_nr, region))
wk_ctr <- read_rds("data/btw21-districts-centroid.rds")
Votes · 🗳
Results
party vote share federal level
- candidate tier (first tier) — (“Erststimmen”)
- party tier (second tier) — (“Zweitstimmen”)
Code
btw_dt |>
filter(level == "federal") |>
mutate(party = if_else(party %in% c("CDU", "CSU"), "CDU/CSU", party)) |>
group_by(votes_type, party) |>
summarise(share = sum(share) |> round(1)) |>
pivot_wider(names_from = votes_type, values_from = share)
AfD |
10.2 |
10.4 |
CDU/CSU |
28.6 |
24.1 |
DIE LINKE |
5.0 |
4.9 |
FDP |
8.7 |
11.4 |
GRÜNE |
13.9 |
14.7 |
SPD |
26.4 |
25.7 |
Party votes
Code
pl_dt <-
wk_shp |>
select(wkr_nr, geometry) |>
right_join(btw_tier2) |>
mutate(party = if_else(party %in% c("CDU", "CSU"), "CDU/CSU", party)) |>
group_by(party) |>
mutate(share_diff = share - mean(share))
party tier (“Zweitstimmen”) vote share electoral districts
Code
pl1 <-
ggplot() +
geom_sf(data = pl_dt, aes(fill = share), lwd = 0.05) +
scale_fill_viridis_c(option = "magma", direction = -1) +
coord_sf(crs = crs_deu) +
facet_wrap(vars(party))
pl1 + plot_annotation(caption = "Source: Bundeswahlleiter 2021")
difference (+/-) of vote share party district from national average vote share party
Code
pl2 <-
ggplot() +
geom_sf(data = pl_dt, aes(fill = share_diff), lwd = 0.05) +
scale_fill_gradient2(name = "+/-") +
coord_sf(crs = crs_deu) +
facet_wrap(vars(party))
pl2 + plot_annotation(caption = "Source: Bundeswahlleiter 2021")
Code
pl_out <- pl1 / pl2 + plot_annotation(caption = "Source: Bundeswahlleiter 2021")
ggsave("z-btw21-vote-share.png", pl_out, width = 5, height = 7)
Candidate votes
candidate tier (“Erststimmen”) vote share by party
- A — plurality vote winning party
- B — sum of candidate vote share difference by ideology
- right — AfD, CDU, CSU, FDP
- left — DIE LINKE, GRÜNE, SPD
- I – vote share plurality winner
- II – vote share difference top 2 results
Code
party_scale <- c("#A6CEE3", "#1F78B4", "#1b699e", "#E31A1C", "#33A02C", "#FB9A99")
ideo_share <-
btw_tier1 |>
group_by(wkr_nr, ideology) |>
summarise(share = sum(share)) |>
pivot_wider(names_from = ideology, values_from = share) |>
mutate(rl_diff = right - left)
pl_dt <-
wk_shp |>
select(wkr_nr, geometry) |>
inner_join(ideo_share) |>
inner_join(mp |> select(wkr_nr, party))
pl_dt_ctr <- wk_ctr |> left_join(mp)
pl1 <-
ggplot() +
geom_sf(data = pl_dt, aes(fill = party), lwd = 0.05, alpha = 0.8) +
geom_sf(data = pl_dt_ctr, aes(shape = party), alpha = 0.4) +
coord_sf(crs = crs_deu) +
scale_fill_manual(values = party_scale)
# print(pl1)
pl2 <-
ggplot() +
geom_sf(data = pl_dt, aes(fill = rl_diff), lwd = 0.05) +
coord_sf(crs = crs_deu) +
scale_fill_gradient2(name = "right +/-")
# print(pl2)
pl1 + pl2 + plot_annotation(tag_levels = "A") +
plot_annotation(caption = "Source: Bundeswahlleiter 2021")
Code
diff_1_2 <-
btw_tier1 |>
arrange(wkr_nr, desc(share)) |>
group_by(wkr_nr) |>
filter(row_number() %in% c(1, 2)) |>
summarise(
top_2_diff = max(share) - min(share),
share = max(share)
)
pl_dt <-
wk_shp |>
select(wkr_nr, geometry) |>
inner_join(diff_1_2)
pl1 <-
ggplot() +
geom_sf(data = pl_dt, aes(fill = share), lwd = 0.05) +
coord_sf(crs = crs_deu) +
scale_fill_viridis_c(option = "cividis", direction = -1)
# print(pl1)
pl2 <-
ggplot() +
geom_sf(data = pl_dt, aes(fill = top_2_diff), lwd = 0.05) +
coord_sf(crs = crs_deu) +
scale_fill_viridis_c(option = "magma", direction = -1)
# print(pl2)
pl1 + pl2 + plot_annotation(tag_levels = "I") +
plot_annotation(caption = "Source: Bundeswahlleiter 2021")
MPs (SMD) · 🗺
Maps
plurality winner candidate (first) tier vote by party and sex
Germany
Code
pl_dt_shp <- wk_shp |> left_join(mp)
pl_dt_ctr <- wk_ctr |> left_join(mp)
pl1 <-
ggplot() +
geom_sf(data = pl_dt_shp, aes(fill = party), lwd = 0.1) +
# scale_fill_brewer(type = "qual", palette = 4) +
scale_fill_manual(values = party_scale) +
geom_sf(data = pl_dt_ctr, aes(shape = sex), colour = "grey15", alpha = 0.7) +
coord_sf(crs = crs_deu)
pl2 <-
ggplot() +
geom_sf(data = pl_dt_shp, colour = "lightgrey", fill = "grey95", lwd = 0.1) +
geom_sf(data = pl_dt_ctr, aes(shape = sex, colour = party)) +
scale_colour_manual(values = party_scale) +
guides(colour = "none", fill = "none", shape = "none") +
coord_sf(crs = crs_deu)
pl1 + pl2 + plot_annotation(caption = "Source: Bundeswahlleiter 2021")
NRW and Berlin
Code
plot_state <- function(state_select, party_scale_select) {
pl_dt_shp_re <- pl_dt_shp |> filter(state == state_select)
pl_dt_ctr_re <- pl_dt_ctr |> filter(state == state_select)
ggplot() +
geom_sf(data = pl_dt_shp_re, aes(fill = party), lwd = 0.2) +
scale_fill_manual(values = party_scale_select) +
geom_sf(data = pl_dt_ctr_re, aes(shape = sex), colour = "grey30", size = 2) +
guides(colour = "none", fill = "none", shape = "none")
}
pl1 <- plot_state("NW", party_scale[c(2, 4, 5)])
pl2 <- plot_state("BE", party_scale[c(2, 4, 5, 6)])
# plot_state("HH", party_scale[c(2, 5, 6)])
pl1 + pl2 + plot_layout(widths = c(55, 45)) +
plot_annotation(caption = "Source: Bundeswahlleiter 2021")
Descriptives
number (n) of districts won by party and sex (f – female, m – male)
Party
Code
count_party <-
mp |>
count(party, sex) |>
pivot_wider(names_from = sex, values_from = n) |>
mutate(share_f = round(100 * f / (m + f)))
count_party |> arrange(share_f)
AfD |
2 |
14 |
12 |
CDU |
17 |
81 |
17 |
CSU |
10 |
35 |
22 |
SPD |
39 |
82 |
32 |
DIE LINKE |
1 |
2 |
33 |
GRÜNE |
9 |
7 |
56 |
Code
mp |>
count(party) |>
mutate(share = round(100 * n / sum(n))) |>
arrange(desc(share))
SPD |
121 |
40 |
CDU |
98 |
33 |
CSU |
45 |
15 |
AfD |
16 |
5 |
GRÜNE |
16 |
5 |
DIE LINKE |
3 |
1 |
States
Code
count_state <-
mp |>
left_join(wk_shp |> select(wkr_nr, state_name = land_name)) |>
count(state_name, sex) |>
pivot_wider(names_from = sex, values_from = n) |>
mutate(share_f = round(100 * f / (m + f)))
count_state |> arrange(share_f)
Nordrhein-Westfalen |
11 |
53 |
17 |
Sachsen |
3 |
13 |
19 |
Sachsen-Anhalt |
2 |
7 |
22 |
Hessen |
5 |
17 |
23 |
Bayern |
11 |
35 |
24 |
Saarland |
1 |
3 |
25 |
Rheinland-Pfalz |
4 |
11 |
27 |
Schleswig-Holstein |
3 |
8 |
27 |
Baden-Württemberg |
11 |
27 |
29 |
Berlin |
4 |
8 |
33 |
Niedersachsen |
10 |
20 |
33 |
Bremen |
1 |
1 |
50 |
Hamburg |
3 |
3 |
50 |
Mecklenburg-Vorpommern |
3 |
3 |
50 |
Brandenburg |
6 |
4 |
60 |
Thüringen |
NA |
8 |
NA |
Models · 📈
- area_km2 — size of district as square length (root of area)
- coord_y — north/south position (latitude)
- left_share — share of left parties (SPD, Linke, Grüne)
- region – western or eastern (former GDR) state
Code
library(broom)
library(ggeffects)
mo_dt <-
pl_dt_shp |>
left_join(ideo_share) |>
mutate(
female = if_else(sex == "f", 1, 0),
left_share = left
)
Female first-tier winners
Logit model to predict female winners of candidate (first) tier vote
Code
mo_glm <- glm(female ~ area_km2 + coord_y + left_share, data = mo_dt, family = "binomial")
tidy(mo_glm) |> mutate(across(where(is.numeric), round, 2))
(Intercept) |
-3.98 |
4.62 |
-0.86 |
0.39 |
area_km2 |
0.02 |
0.01 |
1.96 |
0.05 |
coord_y |
0.01 |
0.11 |
0.12 |
0.90 |
left_share |
0.03 |
0.02 |
1.62 |
0.11 |
Code
pl1 <-
ggpredict(mo_glm, terms = "area_km2 [all]") |>
plot(add.data = TRUE, show.title = FALSE)
pl2 <-
ggpredict(mo_glm, terms = "coord_y [all]") |>
plot(add.data = TRUE, show.title = FALSE, show.y.title = FALSE)
pl1 + pl2
Left vote-share
Linear model of left parties (SPD, Linke, Grüne) vote share
Code
mo_lm <- lm_robust(left_share ~ area_km2 * region + coord_y * region, data = mo_dt)
tidy(mo_lm) |>
mutate(across(where(is.numeric), round, 2)) |>
select(-conf.low, -conf.high, -df, -outcome)
(Intercept) |
-286.70 |
59.84 |
-4.79 |
0.00 |
area_km2 |
-0.27 |
0.07 |
-3.77 |
0.00 |
regionwest |
181.81 |
60.87 |
2.99 |
0.00 |
coord_y |
6.54 |
1.19 |
5.50 |
0.00 |
area_km2:regionwest |
-0.13 |
0.08 |
-1.73 |
0.08 |
regionwest:coord_y |
-3.34 |
1.21 |
-2.76 |
0.01 |
Code
pl1 <-
ggpredict(mo_lm, c("area_km2", "region")) |>
plot(add.data = TRUE, show.title = FALSE, show.legend = FALSE)
pl2 <-
ggpredict(mo_lm, terms = c("coord_y", "region")) |>
plot(add.data = TRUE, show.title = FALSE, show.y.title = )
pl1 + pl2
Party vote-share
Linear model of party vote share
Code
mo_dt2 <-
btw_tier2 |>
mutate(party = if_else(party %in% c("CDU", "CSU"), "CDU/CSU", party)) |>
# mutate(party = fct_relevel(party, "CDU")) |>
left_join(wk_shp)
mo_lm2 <- lm_robust(share ~ party * area_km2 * region, data = mo_dt2)
# tidy(mo_lm2) |> mutate(across(where(is.numeric), round, 2))
ggpredict(mo_lm2, terms = c("area_km2", "region", "party")) |>
plot(add.data = TRUE, show.title = FALSE)