5  ParlGov losers’ consent

ESS linking example – see also sections “Party-voted-for in government” and “Performance of Party Facts linking” in manuscript.

Code
library(conflicted)

library(tidyverse)
conflicts_prefer(dplyr::filter, .quiet = TRUE)
library(glue)
library(knitr)

library(broom) # tidy model results
library(broom.mixed) # tidy model results for lme4
library(estimatr) # robust standard errors
library(ggeffects) # effects plots
library(lme4) # multi-level models
library(modelsummary) # model tables and coefficient plots
library(patchwork) # combine plots
library(reactable) # dynamic tables
library(skimr) # summary statistics

options(knitr.kable.NA = "")

round_numeric_variables <- function(data, digits = 0) {
  mutate(data, across(
    where(is.numeric),
    \(.x) format(round(.x, digits), scientific = FALSE)
  ))
}
Code
ess_raw <- read_rds("data/02-ess-select.rds")
ess_cabinet_raw <- read_rds("data/07-parlgov-ess_cabinets.rds")

5.2 Variables

Variables used in losers’ consent models and context information

  • stfdem — How satisfied with the way democracy works in country?
    • 0 // Extremely dissatisfied — 10 // Extremely satisfied
  • cabinet — “party-voted-for” (prtv) in government after election
    • ParlGov based calculation
    • excluding caretaker governments
  • lrscale — Placement on left right scale
    • 0 // Left — 10 // Right
  • gndr — Gender
  • agea — Age of respondent, calculated
  • eduyrs — Years of full-time education completed
  • ESS identifiers
    • cntry — Country
    • essround — ESS round
    • pspwght — Post-stratification weight // see ESS survey weights
    • inw_date — Date of interview // various ESS inw* variables
  • Party information
    • prtv — Party voted for in last national election // aggregated ESS IDs
    • prtv_name — Party voted for in last national election // party name
    • first_ess_id — unique ESS party ID used in Party Facts

5.3 Summary statistics

Code
ess_cabinet <-
  ess_cabinet_raw |>
  select(essround, cntry, idno, cabinet = cabinet_party)

ess_lm <-
  ess_raw |>
  left_join(ess_cabinet) |>
  mutate(
    across(c(lrscale, stfdem), \(.x) as.integer(.x) - 1),
    cabinet = case_when(
      cabinet == 1 ~ "Yes",
      cabinet == 0 ~ "No",
      .default = NA
    ),
    cabinet = as.factor(cabinet) |> fct_rev()
  ) |>
  filter(!all(is.na(cabinet)), .by = c(cntry, essround))

ess_lm |>
  select(-idno) |>
  skim() |>
  round_numeric_variables(2)
Data summary
Name select(ess_lm, -idno)
Number of rows 433599
Number of columns 14
_______________________
Column type frequency:
character 3
Date 1
factor 4
numeric 6
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
cntry 0 1.00 2 2 0 32 0
prtv 171780 0.60 8 14 0 2704 0
prtc 240202 0.45 8 10 0 2642 0

Variable type: Date

skim_variable n_missing complete_rate min max median n_unique
inw_date 912 1.00 2002-01-14 2022-09-02 2011-06-03 4827

Variable type: factor

skim_variable n_missing complete_rate ordered n_unique top_counts
gndr 331 1.00 FALSE 2 Fem: 231527, Mal: 201741, No : 0
prtv_party 171780 0.60 FALSE 888 Lab: 6580, Con: 6077, Chr: 5660, Soc: 4972
prtc_party 240202 0.45 FALSE 900 Lab: 4949, Con: 4578, Chr: 4290, Soc: 3484
cabinet 209306 0.52 FALSE 2 Yes: 121092, No: 103201

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
essround 0 1.00 5.39 2.80 1 3.0 5.00 8.00 10.00 ▇▇▇▇▇
pspwght 0 1.00 1.01 0.52 0 0.7 0.93 1.18 6.85 ▇▁▁▁▁
agea 2155 1.00 48.49 18.62 13 33.0 48.00 63.00 123.00 ▆▇▆▁▁
eduyrs 5075 0.99 12.43 4.13 0 10.0 12.00 15.00 65.00 ▇▅▁▁▁
lrscale 55413 0.87 5.13 2.23 0 4.0 5.00 7.00 10.00 ▂▃▇▃▁
stfdem 15516 0.96 5.28 2.51 0 4.0 5.00 7.00 10.00 ▅▅▇▇▂

5.4 Multi-level models (ML)

Model variables preparation

  • removing outliers age (99% quantile)
  • selecting only variables used in models
  • removing incomplete observations
  • centering of continuous variables (age, education, left-right)
Code
# quantile(ess_lm$eduyrs, probs = c(0, 0.5, 0.9, 0.95, 0.99, 0.999), na.rm = TRUE)
eduyrs_remove <- quantile(ess_lm$eduyrs, probs = 0.99, na.rm = TRUE)

ess_lm_c <-
  ess_lm |>
  filter(eduyrs < eduyrs_remove) |>
  select(stfdem, cabinet, gndr, eduyrs, agea, lrscale, cntry, essround, pspwght) |>
  na.omit() |>
  mutate(
    essround_cntry = paste(essround, cntry),
    across(c(agea, eduyrs, lrscale),
      \(.x) scale(.x, scale = FALSE) |> as.vector(),
      .names = "{.col}_c"
    )
  )

plot_ggpredict <- function(model, plot_terms) {
  ggpredict(model, terms = plot_terms) |>
    plot(show.title = FALSE, show.legend = FALSE)
}

ml_formula <- "stfdem ~ gndr +  cabinet*eduyrs_c + cabinet*poly(agea_c, 2) + cabinet*poly(lrscale_c, 2)"

5.4.1 Three ML models

Multi-level models with quadric terms and interactions. Structure of models:

  • Model 1 (ML-1) — ESS-Round/country and country
  • Model 2 (ML-2) — ESS-Round and country
  • Model 3 (ML-3) — country

Visualization of results in Figure 5.1 and Figure 5.2 – see variable information in Section 5.3

Code
ml1 <- lmer(
  as.formula(glue("{ml_formula} + (1 | cntry/essround_cntry)")),
  weights = pspwght,
  data = ess_lm_c
)

ml2 <- lmer(
  as.formula(glue("{ml_formula} + (1 | essround) + (1 | cntry)")),
  weights = pspwght,
  data = ess_lm_c
)

ml3 <- lmer(
  as.formula(glue("{ml_formula} + (1 | cntry)")),
  weights = pspwght,
  data = ess_lm_c
)
Code
models <- list("ML-1" = ml1, "ML-2" = ml2, "ML-3" = ml3)

if (knitr::is_html_output()) {
  modelsummary(models)
} else {
  modelsummary(models, output = "markdown")
}
ML-1 ML-2 ML-3
(Intercept) 5.782 5.790 5.775
(0.169) (0.184) (0.172)
gndrFemale -0.182 -0.178 -0.179
(0.009) (0.010) (0.010)
cabinetNo -0.637 -0.645 -0.640
(0.010) (0.010) (0.010)
eduyrs_c 0.048 0.045 0.048
(0.002) (0.002) (0.002)
poly(agea_c, 2)1 23.624 21.075 25.719
(3.151) (3.192) (3.189)
poly(agea_c, 2)2 30.470 30.403 31.438
(2.913) (2.957) (2.967)
poly(lrscale_c, 2)1 103.697 105.407 108.606
(3.333) (3.204) (3.208)
poly(lrscale_c, 2)2 35.905 39.358 40.284
(3.116) (3.149) (3.160)
cabinetNo × eduyrs_c 0.013 0.014 0.015
(0.003) (0.003) (0.003)
cabinetNo × poly(agea_c, 2)1 -4.329 -2.065 -3.641
(4.620) (4.675) (4.691)
cabinetNo × poly(agea_c, 2)2 13.465 14.204 13.904
(4.284) (4.350) (4.366)
cabinetNo × poly(lrscale_c, 2)1 -22.081 -23.826 -26.594
(4.862) (4.494) (4.496)
cabinetNo × poly(lrscale_c, 2)2 -108.724 -113.550 -113.758
(4.367) (4.397) (4.413)
SD (Intercept cntry) 0.931 0.965 0.970
SD (Observations) 2.106 2.142 2.150
SD (Intercept essround_cntrycntry) 0.500
SD (Intercept essround) 0.219
:———————————– ———: ———: ———:
Num.Obs. 205611 205611 205611
R2 Marg. 0.040 0.040 0.041
R2 Cond. 0.233 0.209 0.203
AIC 918254.1 924409.6 925869.4
BIC 918417.9 924573.3 926022.9
ICC 0.2 0.2 0.2
RMSE 2.13 2.16 2.17

Analysis of variance (ANOVA) models and refitting with Maximum Likelihood instead of Restricted Maximum Likelihood.

Code
anova(ml1, ml2, ml3) |>
  tidy() |>
  arrange(term)
term npar AIC BIC logLik deviance statistic df p.value
ml1 16 918249.0 918412.7 -459108.5 918217.0 7617.436 1 0
ml2 16 924404.7 924568.4 -462186.3 924372.7 0.000 0
ml3 15 925864.4 926017.9 -462917.2 925834.4

5.4.2 Effects plot ML-1

Effects plot Multi-Level Model 1 (ML-1, see Section 5.4.1)

Code
plot_ggpredict(ml1, c("lrscale_c [all]", "cabinet")) +
  plot_ggpredict(ml1, c("agea_c [all]", "cabinet")) +
  plot_ggpredict(ml1, c("eduyrs_c [all]", "cabinet"))

Figure 5.1: Effects plot (95% CIs) — Satisfaction with democracy
Code
pl_dt_lr <- ggpredict(ml1, c("lrscale_c [all]", "cabinet"))
pl_dt_edu <- ggpredict(ml1, c("eduyrs_c [all]", "cabinet"))
pl_dt_age <- ggpredict(ml1, c("agea_c [all]"))
Code
add_plot_layers <- function(pl, var_name = "x") {
  pl +
    geom_hline(yintercept = 5, color = "grey", size = 0.5) +
    geom_ribbon(aes(ymin = conf.low, ymax = conf.high), alpha = 0.1) +
    scale_y_continuous(limits = c(3.5, 7)) +
    labs(x = var_name, y = "") +
    theme_minimal()
}

color_values <- c("Yes" = "#E41A1C", "No" = "#377EB8")

pl_lr <-
  ggplot(pl_dt_lr, aes(x, predicted, fill = group)) |>
  add_plot_layers("left-right") +
  geom_vline(xintercept = median(ess_lm_c$lrscale_c), color = "grey", size = 0.5, linetype = "dashed") +
  geom_line(aes(colour = group)) +
  guides(color = "none", fill = "none") +
  scale_color_manual(values = color_values) +
  scale_fill_manual(values = color_values)

pl_edu <-
  ggplot(pl_dt_edu, aes(x, predicted, fill = group)) |>
  add_plot_layers("education") +
  geom_vline(xintercept = median(ess_lm_c$eduyrs_c), color = "grey", size = 0.5, linetype = "dashed") +
  geom_line(aes(colour = group)) +
  guides(color = "none", fill = "none") +
  scale_color_manual(values = color_values) +
  scale_fill_manual(values = color_values)

pl_age <-
  ggplot(pl_dt_age, aes(x, predicted)) |>
  add_plot_layers("age") +
  geom_vline(xintercept = median(ess_lm_c$agea_c), color = "grey", size = 0.5, linetype = "dashed") +
  geom_line()

pl <- pl_lr + pl_edu + pl_age

ggsave("figures-tables/figure-2_ml-model-effects.png", pl, width = 9, height = 6, dpi = 300)
pl

Figure 5.2: Effects plot (95% CIs) — Satisfaction with democracy // Article version

5.5 Linear effects (ML)

Multi-level model with linear terms and no interactions.

Visualization of results in Figure 5.3 (standardized coefficients) and Figure 5.4 (effects) – see variable information in Section 5.3

Code
ml_le <- lmer(
  "stfdem ~ cabinet + gndr + eduyrs_c + agea_c + lrscale_c + (1 | cntry/essround_cntry)",
  weights = pspwght,
  data = ess_lm_c
)
Code
ml_le |>
  tidy() |>
  kable(digits = 3)
effect group term estimate std.error statistic
fixed (Intercept) 5.775 0.170 33.996
fixed cabinetNo -0.636 0.010 -64.341
fixed gndrFemale -0.178 0.009 -18.849
fixed eduyrs_c 0.051 0.001 37.025
fixed agea_c 0.002 0.000 6.679
fixed lrscale_c 0.094 0.002 44.172
ran_pars essround_cntry:cntry sd__(Intercept) 0.503
ran_pars cntry sd__(Intercept) 0.934
ran_pars Residual sd__Observation 2.112
Code
cm <- c(
  "cabinetNo" = "Opposition voter",
  "eduyrs_c" = "Education years",
  "gndrFemale" = "Women",
  "agea_c" = "Age",
  "lrscale_c" = "Left-right"
)

# parameters::parameters(ml1, standardize = "refit")
# modelplot(ml_le)

modelplot(ml_le, coef_map = rev(cm), standardize = "refit") +
  labs(x = "")

Figure 5.3: Standardized coefficients (95% CIs)– Linear effects model
Code
plot_ggpredict(ml_le, c("lrscale_c [all]", "cabinet")) +
  plot_ggpredict(ml_le, c("agea_c [all]", "cabinet")) +
  plot_ggpredict(ml_le, c("eduyrs_c [all]", "cabinet"))

Figure 5.4: Linear effects plot (95% CIs) — Satisfaction with democracy

5.6 Fixed effects model

Fixed effects model with quadric terms and interactions.

Visualization of results in Figure 5.5 and variable information in Section 5.3

Code
m_fe <-
  lm_robust(as.formula(glue("{ml_formula} + cntry + factor(essround)")),
    weights = pspwght,
    data = ess_lm_c
  )
Code
m_fe |>
  tidy() |>
  mutate(term = str_remove_all(term, "poly\\(|, 2\\)1") |> str_replace(fixed(", 2)2"), "^2")) |>
  filter(!str_starts(term, "cntry|factor")) |>
  select(-df, -outcome) |>
  kable(digits = 3)
term estimate std.error statistic p.value conf.low conf.high
(Intercept) 6.407 0.037 173.718 0.000 6.335 6.479
gndrFemale -0.178 0.011 -16.569 0.000 -0.199 -0.157
cabinetNo -0.645 0.011 -58.835 0.000 -0.667 -0.624
eduyrs_c 0.045 0.002 21.591 0.000 0.041 0.050
agea_c 21.004 3.505 5.993 0.000 14.135 27.874
agea_c^2 30.381 3.306 9.190 0.000 23.901 36.860
lrscale_c 105.418 3.903 27.011 0.000 97.769 113.068
lrscale_c^2 39.400 4.134 9.531 0.000 31.297 47.502
cabinetNo:eduyrs_c 0.014 0.003 4.744 0.000 0.008 0.020
cabinetNo:agea_c -2.033 5.236 -0.388 0.698 -12.295 8.229
cabinetNo:agea_c^2 14.216 4.958 2.867 0.004 4.499 23.933
cabinetNo:lrscale_c -23.832 5.585 -4.267 0.000 -34.779 -12.885
cabinetNo:lrscale_c^2 -113.566 5.823 -19.502 0.000 -124.979 -102.153

Fixed effects for countries (“cnty”) and ESS rounds (“essround”) not shown.

Code
m_fe |>
  glance() |>
  kable(digits = 2)
r.squared adj.r.squared statistic p.value df.residual nobs se_type
0.18 0.18 756.54 0 205558 205611 HC2
Code
plot_ggpredict(m_fe, c("lrscale_c [all]", "cabinet")) +
  plot_ggpredict(m_fe, c("agea_c [all]", "cabinet")) +
  plot_ggpredict(m_fe, c("eduyrs_c [all]", "cabinet"))

Figure 5.5: Fixed effects model (95% CIs) — Satisfaction with democracy

5.7 Share covered

Code
id_select <- "parlgov_id" # "ches_id" + "parlgov_id"
ess_check <- ess_cabinet_raw # ess_raw + ess_cabinet_raw
tbl_file_name <- "figures-tables/table-2b_parlgov-coverage.csv"

We calculate the share of matches for the “party-voted-for” (prtv) question. Excluded from the calculation are instances of other, independent, and technical (see Party Facts codebook).

Code
link_table_technical <-
  read_rds("data/03-party-facts-links-technical.rds")

prtv <-
  ess_check |>
  left_join(link_table_technical, by = c("prtv" = "ess_id")) |>
  select(cntry, essround, prtv, prtv_party, all_of(id_select), partyfacts_name, technical) |>
  filter(!is.na(prtv)) |>
  mutate(is_match = if_else(is.na(.data[[id_select]]), 0, 1))

prtv_match <-
  prtv |>
  filter(technical != 7 & technical != 8 & technical != 12 | is.na(technical)) |>
  summarise(
    prvt_n = n(),
    is_match = first(is_match),
    .by = c(cntry, essround, prtv, prtv_party)
  )

prtv_share <-
  prtv_match |>
  summarise(
    share_match = (sum(prvt_n * is_match) * 100 / sum(prvt_n)) |> round(1),
    .by = c(cntry, essround)
  )

The table summarizes the share of party matches across all countries and ESS rounds.

Code
tbl_out <-
  prtv_share |>
  reframe(
    enframe(
      quantile(share_match, c(0, 0.1, 0.25, 0.5, 0.75, 1)),
      "quantile", "share_match"
    )
  ) |>
  mutate(share_match = round(share_match, 1))

write_csv(tbl_out, tbl_file_name)
tbl_out
quantile share_match
0% 11.4
10% 65.4
25% 81.9
50% 95.8
75% 99.2
100% 100.0

The share of matched parties is weighted by the number of “party-voted-for” responses and is calculated for each country in every ESS round.

The next table summarizes the country level share of party matches for ESS rounds with data set matches.

Code
tbl_out <-
  prtv_share |>
  summarise(
    min = min(share_match),
    median = median(share_match) |> round(1),
    max = max(share_match),
    ess_rounds = n_distinct(essround),
    .by = cntry
  ) |>
  filter(max > 0) |>
  arrange(min, median)

if (knitr::is_html_output()) {
  tbl_out |>
    reactable(searchable = TRUE, striped = TRUE)
} else {
  tbl_out
}