Replication code for:

Mobilizing Europe’s citizens to take action on migration and climate change: Behavioral evidence from 27 EU member states (JEPP 2025)

Author

Hieko Giebler, Johannes Giesseke, Macartan Humphreys, Swen Hutter, Heike Klüver

Abstract
This paper investigates how two politicized issues—migration and climate change—mobilize citizens across European countries. Building on the concept of issue-specific mobilization potentials, we examine citizens’ willingness to support petitions related to the two issues using an original behavioral measure embedded in the 2024 European Parliament Election Study. We document variation in political engagement and examine how opposing stances on issues owned by the left or the right mobilize citizens, how citizens’ agreement with issue positions affects support, and whether grievances, participation cultures, politicization levels, and the ideology of the national government can explain national-level variation. Our results indicate substantial variation in petition support across countries and issues, with the right-wing petition on migration attracting the most support. However, our country-level measures do not explain this variation well. Overall, our findings highlight the need for more nuanced, issue-specific approaches to understanding cross-national patterns of political participation.

1 Setup

1.1 Import data

Dataset from EES 2024 merged with country level indicators.

Code
# source("data_prep.R")
data <- read_rds("assets/ees_petition_data.rds")

1.2 Labels

Labels defined for graphs.

Code
# Treatment group labels in data and for figures
tgg <- c("Climate (left)",
         "Climate (right)",
         "Migration (left)",
         "Migration (right)")
names(tgg) <- tgg

tg <- c("CLI left", "CLI right", "MIG left", "MIG right")
treatment_groups <- as.list(tg)
names(treatment_groups) <- tgg


# Core country covariates

vs <- c(
  "grievances",
  "protest_participants",
  "politicization",
  "lfirstp",
  "lsecondp",
  "gov_lr"
)
vs_labels <- c(
  "Grievances",
  "Protest participants\n(per million per year)",
  "Politicization\n(polarization x salience)",
  "Lijphart 1",
  "Lijphart 2",
  "Left / Right \n(government orientation)"
)

vars <- as.list(vs)
names(vars) <- vs

# Additional country covariates
vs_other <-
  c(
    "sal",
    "pol",
    "vulnerability",
    "cri",
    "ims_abs",
    "ims_perc",
    "ref_abs",
    "ref_perc",
    "protest_events",
    "protest_intensity"
  )
vs_other_labs <-
  c(
    "Salience",
    "Polarization",
    "Vulnerability",
    "Climate risk index",
    "Immigrants (absolute)",
    "Immigrants (per capita)",
    "Refugees (absolute)",
    "Refugees (per capita)",
    "Protest events",
    "Protest intensity"
  )

1.3 Prepare country df

Country level data frame.

Code
country_df <-
  data |> 
  group_by(country, left, topic, treatment_grp) |>
  summarize(position = mean(position, na.rm = TRUE), 
            outcome1b = mean(outcome1b, na.rm = TRUE),
            grievances = mean(grievances, na.rm = TRUE),
            politicization = mean(politicization, na.rm = TRUE),
            protest_participants = mean(protest_participants, na.rm = TRUE)) |>
  ungroup() |>
  mutate(groups = haven::as_factor(treatment_grp),
         Left = 1*(left == "Left"),
         Left = Left - mean(Left),
         Climate = 1*(topic == "Climate change"),
         Climate = Climate - mean(Climate)
         )

2 Results

2.1 Figure 1: Distribution of positions in migration and climate, country averages

Code
data |> 
  group_by(Country) |> 
  summarize(
    antimigration = mean(pos_mig, na.rm = TRUE)/10,
    anticlimate = mean(pos_cli, na.rm = TRUE)/10) |> 
  ggplot(aes(antimigration, anticlimate, label = Country)) +
  geom_text()  + 
  xlab("average support: right  position on migration")  + 
  ylab("average support: right  position on climate")

2.2 Figure 2: Share of respondents supporting petitions

Code
data |> 
  group_by(Country, topic, left) |> 
  summarize(petition = mean(outcome1b), .groups = 'drop') |> 
  pivot_wider(names_from = left, values_from = petition) |> 
  ggplot(aes(Right, Left, label = Country)) +  
    geom_point(size = 3) +  # Change the size of the points as needed
    geom_text_repel() +  # Use ggrepel to repel text labels
    facet_grid(~topic) + 
    geom_abline(color = "red") + 
    theme_minimal()  # Optional: change theme for better appearance

Share signing petitions of each type. Each dot represents a country average participation rate for Left and Right sides of a petition.

2.3 Figure 3: Petition support as a function of citizens’ position

Code
data |>  
  ggplot(aes(position, outcome1b)) +  
  geom_smooth()  + 
  facet_grid(topic ~  left) + ylim(0, NA)

Petition signing as a function of position on each issue. All are increasing but are remarkable high on the left of the x axis

2.4 Figure 4: Predicted probabilities

Sample estimation:

Code
# Fit the logistic mixed-effects model
if(run)
  rstanarm::stan_glmer(outcome1b ~ 1 + (1 | Country), data = data, family = binomial(link = "logit")) |>
  write_rds("saved/M_1.1.rds")

posterior_samples <- read_rds("saved/M_1.1.rds") |> as.data.frame()
Code
# Extract the posterior samples of the random effects

# Get the unique country names
Countries <- unique(data$Country)
Countries_list <- as.list(Countries)
names(Countries_list) <- Countries

# Function to calculate predicted probabilities for each country
get_predicted_probs <- function(country, fit = posterior_samples) {
  random_intercept <-  fit |> 
    select(starts_with(paste0("b[(Intercept) Country:", country))) 
  common_intercept <- fit$`(Intercept)`
  log_odds <- (common_intercept + random_intercept)[[1]]
  # Convert log-odds to probabilities
  data.frame(probs = exp(log_odds) / (1 + exp(log_odds)))
}

# Create a dataframe to store the results
results <- lapply(Countries_list, get_predicted_probs) |>
  bind_rows(.id = "Country")

# Summarize the predicted probabilities
summary_results <- results %>%
  group_by(Country) %>%
  summarize(
    expectation = mean(probs),
    min_prob = min(probs),
    max_prob = max(probs)
  ) |>
  left_join(data |> group_by(Country) |> summarize(raw = mean(outcome1b))) |>
  mutate(Country = fct_reorder(Country, expectation))


# Plot the results
ggplot(summary_results, aes(x = Country, y = expectation)) +
  geom_point() +
  geom_errorbar(aes(ymin = min_prob, ymax = max_prob)) +
  labs(title = "Predicted Probabilities of Petition Signing by Country",
       y = "Predicted Probability",
       x = "Country") +
  theme_minimal() + coord_flip() +
  geom_point(aes(Country, raw), color = "red")

Code
# Prep data so that position is in line with the petition content

decomp_model <- 
  function(g) 
  rstanarm::stan_glmer(
    outcome1b ~ 1 + position + (1 + position | Country),
    family = binomial(link = "logit"),
    data = data |> filter(groups == g) 
    )

if(run)
  treatment_groups |> 
  lapply(decomp_model)|>
  write_rds("saved/M_1.2.rds")

M_1.2 <-  read_rds("saved/M_1.2.rds")

posterior_1.2 <- lapply(M_1.2,  as.data.frame)

2.5 Figure 5: Model predictions on support probabilities

Code
lapply(posterior_1.2, 
       function(df)
         df|> select(b = position, a = `(Intercept)`)) |> 
  bind_rows(.id = "petition") |>
  mutate(y0 = 1/(1+exp(-a)),
         y1 = 1/(1+exp(-a-b)),
         change = y1 - y0) |>
  ggplot() + 
  geom_histogram(aes(x = y0), fill = "red", alpha = .5)  +
  geom_histogram(aes(x = y1), fill = "blue", alpha = .5) + facet_wrap(~petition) + 
  xlab("Signing propensity")

2.6 Figure 6: Decomposition of variation

Decomposition function:

Code
decomposition <- function(g) {
  
 names(posterior_1.2) <- treatment_groups
 posterior <- posterior_1.2[[g]]

 c_df <- country_df |>  filter(groups  == g)

 X_diff <- c_df$position - mean(c_df$position)
  

delta_Y  <- {

  aj <- 
    (posterior |> select(starts_with("b[(I")) |> apply(2, mean)) + 
    (posterior |> select(starts_with("(I")) |> apply(2, mean)) 
  
  bj <- 
    (posterior |> select(starts_with("b[po")) |> apply(2, mean)) + 
    (posterior |> pull(position) |> mean()) 
  
  Y = aj + bj*c_df$position 
  
  Y - mean(Y)
}

delta_X  <- {

  bj <- 
    (posterior |> select(starts_with("b[po")) |> apply(2, mean)) + 
    (posterior |> pull(position) |> mean()) 
  
  bj*X_diff
  }

delta_b <-  {
 a_diff <- posterior |> select(starts_with("b[(I")) |> apply(2, mean)
 b_diff <- posterior |> select(starts_with("b[po")) |> apply(2, mean)
 a_diff + b_diff*mean(c_df$position) 
 }


bind_cols(
  country_df |> filter(groups == g),
  data.frame(
    delta_Y  = delta_Y,
    delta_X  = delta_X,
    delta_b  = delta_b
  )
)  |>
  select(-left, -topic) |>
  mutate(Country = haven::as_factor(country)) 

}
Code
decomps <- lapply(treatment_groups, decomposition)

decomps |> bind_rows(.id = "condition") |> 
  select(condition, Country, starts_with("delta")) |>
  gather(var, val, -condition, - Country,  - delta_Y) |> 
  mutate(var = factor(var, c("delta_b", "delta_X"), c("Difference in average response", 
                                                      "Difference in average position"))) |>
  
  ggplot(aes(delta_Y, val, label = Country, color = var)) + geom_text()+
  xlab("Difference in outcomes") + ylab("contribution") + facet_wrap(~condition) +
  theme(legend.position = "bottom")

2.7 Figure 7: Country level correlates of engagement

Stan model:

Code
library(rstan)

stan_code <- "
data {
    int<lower=1> N;                // Number of observations
    int<lower=1> m;                // Number of countries
    int<lower=1,upper=m> Country[N]; // Country indices for each observation
    vector[N] y;                   // Observed data
    vector[m] X;                   // Country level predictor variable
}

parameters {
    vector[m] alpha_raw;           // Non-centered group-level intercepts
    real beta;                     // Coefficient for X
    real<lower=0> sigma;           // Error term
    real<lower=0> tau;             // Standard deviation of group-level intercepts
    real mu;                       // Mean of group-level intercepts
}

transformed parameters {
    vector[m] alpha;               // Centered group-level intercepts
    alpha = mu + tau * alpha_raw;  // Non-centered parameterization
}

model {
    // Priors
    alpha_raw ~ normal(0, 1);       // Standard normal for non-centered intercepts
    beta ~ normal(0, 2.5);          // Prior for beta
    sigma ~ normal(0, 1);           // Prior for sigma
    mu ~ normal(0, 10);             // Prior for mu 
    tau ~ normal(0, 5);             // Prior for tau
    
    // Likelihood
    for (i in 1:N) {
        y[i] ~ normal(alpha[Country[i]] + beta * X[Country[i]], sigma);
    }
}
"


f <- function(df, var, y = "outcome1b", ...) {
  
    names(df)[names(df) == var] <- "X"
    names(df)[names(df) == y] <- "y"
    
    # Select relevant columns and drop NA
    df <- df |> 
      select(X, y, Country) |> drop_na() |>
      mutate(Country = as.integer(factor(Country))) 

    stan_data <- list(
        N = nrow(df),
        m = length(unique(df$Country)),
        Country = df$Country,
        y = df$y,
        X = df |> group_by(Country) |> summarize(X = mean(X)) |> 
          pull(X) |>  as.vector()
    )

    fit <- stan(model_code = stan_code, data = stan_data, chains = 4, ...)

    return(fit)
}
Code
# Core variables
if(run)
  vars |>
  lapply(function(v) {
    treatment_groups |> lapply(function(g) {
    f(data |> filter(groups==g & position > .5), v, iter = 5000)})}) |>
  write_rds("saved/M_1.3_by_group.rds")

M_1.3_by <-  read_rds("saved/M_1.3_by_group.rds")


sumy <- function(v, g, model_list = M_1.3_by)
  model_list[[v]][[g]] |>
  as.data.frame() |> summarize(mean =  mean(beta), sd =  sd(beta), 
                               lower = quantile(beta, probs = .025),
                               upper = quantile(beta, probs = .975)) |>
  mutate(max_rhat = max(rhat(model_list[[v]][[g]])))


fig.1.3.by <- 
  vars[c(1:3, 6)] |> lapply(function(v)
  tgg |>
    lapply(function(g) sumy(v,g)) |> 
    bind_rows(.id = "group")) |>
    bind_rows(.id = "var") |>
  mutate(var = factor(var, vs, vs_labels)) |>
  ggplot(aes(mean, group)) + geom_point() + facet_wrap(~var, scales = "free_x") + 
  geom_errorbarh(aes(xmin = lower, xmax = upper, height = .2)) +
  geom_vline(xintercept = 0, color = "red")

fig.1.3.by  

3 Appendices

3.1 Descriptives

3.1.1 Summary statistics on country level variables

Code
vlong <- c("position", "grievances", "politicization", "gov_lr",
          "protest_participants", "sal", "pol", "vulnerability", "cri",  
          "ims_perc",   "ref_perc", "protest_events",   "protest_intensity",
          "outcome1b")

cdf <-   data |> 
  group_by(country, left, topic, treatment_grp) |>
  summarize(across(all_of(vlong), \(x) mean(x, na.rm = TRUE)), .groups = 'drop')  |>
  ungroup() |>
  rename(support_petition = outcome1b)


cdf |>
st()
Summary Statistics
Variable N Mean Std. Dev. Min Pctl. 25 Pctl. 75 Max
left 108
... Right 54 50%
... Left 54 50%
topic 108
... Migration 54 50%
... Climate change 54 50%
position 108 0.5 0.093 0.3 0.44 0.56 0.72
grievances 108 0.35 0.25 0 0.15 0.53 1
politicization 108 0.24 0.2 0 0.1 0.34 1
gov_lr 108 0.21 0.61 -1 -0.26 0.75 1
protest_participants 104 0.24 0.28 0.0027 0.055 0.3 1.1
sal 108 0.51 0.053 0.41 0.48 0.55 0.69
pol 108 3 0.22 2.6 2.9 3.2 3.5
vulnerability 108 0.33 0.033 0.27 0.3 0.35 0.4
cri 108 76 27 18 58 106 106
ims_perc 108 13 9 2.2 6.1 17 48
ref_perc 108 1.8 1 0.46 0.8 2.6 4.2
protest_events 104 639 594 38 270 725 2324
protest_intensity 104 -0.00048 0.87 -0.85 -0.56 0.069 2.4
support_petition 108 0.21 0.061 0.099 0.17 0.25 0.41

3.1.2 Descriptives on petition versions

Code
country_summary
Numbers and shares signing by petition type
left topic n signing
Right Migration 6456 0.259
Right Climate change 6569 0.216
Left Migration 6442 0.184
Left Climate change 6437 0.184

text 3

Code
data |> group_by(left, topic) |> 
  filter(position > .5) |> 
  summarize(n = n(), 
            signing = mean(outcome1b), 
            .groups = "drop") |> ungroup() |>
  kable(digits = 3, caption = "Numbers and shares signing by petition type among supporters (position >0.5)")
Numbers and shares signing by petition type among supporters (position >0.5)
left topic n signing
Right Migration 3021 0.312
Right Climate change 2030 0.253
Left Migration 2144 0.212
Left Climate change 2889 0.217
Code
av <- 
  data |> 
  group_by(Country) |> 
  summarize(`migration left` = mean(outcome1b[topic =="Migration" & left =="Left"], na.rm = TRUE),
            `migration right` = mean(outcome1b[topic =="Migration" & left !="Left"], na.rm = TRUE),
            `climate left` = mean(outcome1b[topic !="Migration" & left =="Left"], na.rm = TRUE),
            `climate right` = mean(outcome1b[topic !="Migration" & left !="Left"], na.rm = TRUE)) 

cor(av[,-1]) |> kable(caption = "Country level correlations in signing across petitions", digits = 2)
Country level correlations in signing across petitions
migration left migration right climate left climate right
migration left 1.00 -0.08 0.45 0.18
migration right -0.08 1.00 0.58 0.72
climate left 0.45 0.58 1.00 0.57
climate right 0.18 0.72 0.57 1.00

3.2 Tables for core analyses

We provide tables of estimates provided in the text.

3.2.1 Figure 4

Code
summary_results |>  
  kable(digits = 2, col.names = c("Country", "Predicted mean", "Lower", "Upper", "Raw mean"),
                caption = "Table of results used for Figure 4")
Table of results used for Figure 4
Country Predicted mean Lower Upper Raw mean
Austria 0.19 0.15 0.23 0.18
Belgium 0.23 0.19 0.28 0.24
Bulgaria 0.23 0.19 0.28 0.23
Croatia 0.24 0.19 0.28 0.24
Cyprus 0.27 0.21 0.33 0.28
Czech 0.22 0.17 0.26 0.22
Denmark 0.23 0.18 0.28 0.23
Estonia 0.20 0.16 0.25 0.20
Finland 0.13 0.10 0.17 0.12
France 0.18 0.14 0.22 0.17
Germany 0.21 0.16 0.25 0.21
Greece 0.21 0.16 0.25 0.21
Hungary 0.24 0.19 0.29 0.25
Ireland 0.21 0.17 0.25 0.21
Italy 0.17 0.13 0.21 0.17
Latvia 0.20 0.16 0.25 0.20
Lithuania 0.20 0.16 0.24 0.20
Luxembourg 0.18 0.13 0.23 0.17
Malta 0.28 0.22 0.36 0.30
Netherlands 0.24 0.20 0.29 0.24
Poland 0.20 0.16 0.26 0.20
Portugal 0.22 0.18 0.27 0.22
Romania 0.26 0.20 0.30 0.26
Slovakia 0.19 0.16 0.24 0.19
Slovenia 0.20 0.16 0.25 0.20
Spain 0.25 0.20 0.30 0.25
Sweden 0.18 0.14 0.22 0.18

3.2.2 Figure 5

Code
lapply(posterior_1.2, 
       function(df)
         df|> select(b = position, a = `(Intercept)`)) |> 
  bind_rows(.id = "petition") |>
  mutate(y0 = 1/(1+exp(-a)),
         y1 = 1/(1+exp(-a-b)),
         change = y1 - y0) |> 
  group_by(petition) |>
  summarize(y_0 = mean(y0),
         sd_0 = sd(y0),
         y_1 = mean(y1),
         sd_1 = sd(y1),
         difference = mean(change),
         sd_difference = sd(change), .groups = 'drop') |>
  kable(digits = 2, col.names = c("Petition", "Rates | opposed", "sd", "Rates given support", "sd", "Difference", "sd"), align = c("l", "c", "c", "c", "c", "c", "c"),
                caption = "Table of results used for Figure 5")
Table of results used for Figure 5
Petition Rates | opposed sd Rates given support sd Difference sd
Climate (left) 0.13 0.01 0.24 0.01 0.12 0.02
Climate (right) 0.20 0.01 0.25 0.01 0.05 0.02
Migration (left) 0.16 0.01 0.22 0.02 0.07 0.02
Migration (right) 0.19 0.01 0.33 0.02 0.14 0.02

3.2.3 Figure 6

Code
decomps |> bind_rows(.id = "condition") |> 
  select(Petition = condition, Country, starts_with("delta")) |>
  kable(digits = 2, col.names = c("Petition", "Country", "delta Y", "delta X", "delta b"),
                caption = "Table of results used for Figure 6")
Table of results used for Figure 6
Petition Country delta Y delta X delta b
Climate (left) Belgium 0.09 -0.02 0.11
Climate (left) Bulgaria 0.19 0.01 0.18
Climate (left) Czech Republic -0.26 -0.04 -0.23
Climate (left) Denmark 0.21 -0.01 0.21
Climate (left) Germany 0.00 -0.06 0.06
Climate (left) Estonia -0.17 -0.10 -0.07
Climate (left) Ireland 0.05 -0.04 0.08
Climate (left) Greece 0.04 0.04 0.00
Climate (left) Spain 0.25 0.02 0.22
Climate (left) France -0.29 0.00 -0.29
Climate (left) Croatia -0.02 0.08 -0.10
Climate (left) Italy -0.15 0.09 -0.24
Climate (left) Cyprus 0.45 0.07 0.37
Climate (left) Latvia -0.09 -0.08 -0.01
Climate (left) Lithuania -0.23 -0.05 -0.18
Climate (left) Luxembourg -0.10 0.01 -0.11
Climate (left) Hungary 0.18 0.06 0.12
Climate (left) Malta 0.36 0.13 0.22
Climate (left) Netherlands 0.14 -0.08 0.21
Climate (left) Austria -0.16 -0.02 -0.14
Climate (left) Poland -0.20 -0.05 -0.15
Climate (left) Portugal 0.29 0.08 0.21
Climate (left) Romania 0.27 0.00 0.27
Climate (left) Slovenia -0.15 0.00 -0.15
Climate (left) Slovakia -0.18 -0.04 -0.14
Climate (left) Finland -0.33 0.00 -0.33
Climate (left) Sweden -0.16 0.00 -0.16
Climate (right) Belgium 0.09 0.01 0.08
Climate (right) Bulgaria 0.12 0.00 0.12
Climate (right) Czech Republic -0.02 0.04 -0.05
Climate (right) Denmark 0.01 0.00 0.00
Climate (right) Germany 0.02 0.00 0.01
Climate (right) Estonia 0.04 0.04 0.00
Climate (right) Ireland -0.16 0.01 -0.17
Climate (right) Greece -0.05 0.00 -0.04
Climate (right) Spain 0.08 0.00 0.08
Climate (right) France -0.02 0.00 -0.01
Climate (right) Croatia 0.18 -0.02 0.20
Climate (right) Italy -0.30 -0.03 -0.26
Climate (right) Cyprus 0.05 -0.02 0.08
Climate (right) Latvia 0.01 -0.01 0.02
Climate (right) Lithuania -0.03 0.03 -0.06
Climate (right) Luxembourg -0.05 -0.02 -0.03
Climate (right) Hungary 0.16 -0.01 0.17
Climate (right) Malta 0.21 0.00 0.21
Climate (right) Netherlands 0.02 0.01 0.01
Climate (right) Austria -0.14 0.00 -0.14
Climate (right) Poland 0.07 0.02 0.05
Climate (right) Portugal -0.13 -0.04 -0.08
Climate (right) Romania 0.18 0.00 0.18
Climate (right) Slovenia 0.09 -0.01 0.10
Climate (right) Slovakia 0.01 0.01 0.00
Climate (right) Finland -0.30 0.01 -0.31
Climate (right) Sweden -0.13 0.00 -0.13
Migration (left) Belgium 0.04 -0.01 0.05
Migration (left) Bulgaria -0.06 0.02 -0.08
Migration (left) Czech Republic -0.28 0.03 -0.31
Migration (left) Denmark 0.14 0.01 0.13
Migration (left) Germany 0.10 0.06 0.04
Migration (left) Estonia -0.19 -0.03 -0.16
Migration (left) Ireland 0.15 0.00 0.15
Migration (left) Greece 0.13 -0.03 0.16
Migration (left) Spain 0.24 0.01 0.23
Migration (left) France -0.15 -0.02 -0.13
Migration (left) Croatia 0.19 0.01 0.17
Migration (left) Italy -0.03 0.02 -0.05
Migration (left) Cyprus -0.10 -0.05 -0.05
Migration (left) Latvia -0.05 0.00 -0.06
Migration (left) Lithuania -0.05 -0.03 -0.02
Migration (left) Luxembourg -0.29 0.00 -0.29
Migration (left) Hungary -0.19 0.00 -0.18
Migration (left) Malta 0.11 -0.04 0.15
Migration (left) Netherlands 0.01 0.01 0.00
Migration (left) Austria 0.19 0.01 0.18
Migration (left) Poland 0.04 -0.01 0.05
Migration (left) Portugal 0.16 -0.01 0.17
Migration (left) Romania 0.34 0.06 0.28
Migration (left) Slovenia -0.32 0.00 -0.32
Migration (left) Slovakia -0.04 0.00 -0.05
Migration (left) Finland -0.30 0.03 -0.33
Migration (left) Sweden 0.23 -0.03 0.26
Migration (right) Belgium 0.14 -0.02 0.16
Migration (right) Bulgaria 0.17 -0.03 0.20
Migration (right) Czech Republic 0.60 -0.06 0.66
Migration (right) Denmark 0.08 -0.03 0.11
Migration (right) Germany -0.15 -0.05 -0.10
Migration (right) Estonia 0.08 0.04 0.04
Migration (right) Ireland -0.20 0.05 -0.25
Migration (right) Greece -0.18 0.04 -0.22
Migration (right) Spain 0.21 0.03 0.18
Migration (right) France -0.24 0.02 -0.26
Migration (right) Croatia 0.13 -0.02 0.16
Migration (right) Italy -0.22 -0.01 -0.21
Migration (right) Cyprus 0.51 0.13 0.39
Migration (right) Latvia -0.04 0.01 -0.04
Migration (right) Lithuania 0.00 0.07 -0.06
Migration (right) Luxembourg -0.03 0.00 -0.02
Migration (right) Hungary 0.41 -0.01 0.43
Migration (right) Malta 0.54 0.14 0.41
Migration (right) Netherlands 0.28 -0.06 0.35
Migration (right) Austria -0.44 0.00 -0.44
Migration (right) Poland -0.15 0.01 -0.15
Migration (right) Portugal -0.12 0.00 -0.12
Migration (right) Romania 0.05 -0.06 0.11
Migration (right) Slovenia 0.03 0.00 0.03
Migration (right) Slovakia -0.20 -0.07 -0.12
Migration (right) Finland -0.71 -0.02 -0.68
Migration (right) Sweden -0.55 -0.01 -0.54

3.2.4 Figure 7

Code
  vars[c(1:3, 6)] |> lapply(function(v)
  tgg |>
    lapply(function(g) sumy(v,g)) |> 
    bind_rows(.id = "group")) |>
    bind_rows(.id = "var") |>
  kable(digits = 2, 
        col.names=c("Covariate", "Petition", "Posterior mean", "Posterior sd", "Cred lower" , "Cred upper", "Rhat"),
        caption = "Table of results used for Figure 7. Rhat is a `stan`  convergence diagnostic.")
Table of results used for Figure 7. Rhat is a stan convergence diagnostic.
Covariate Petition Posterior mean Posterior sd Cred lower Cred upper Rhat
grievances Climate (left) 0.09 0.04 0.02 0.17 1
grievances Climate (right) 0.06 0.04 -0.03 0.14 1
grievances Migration (left) -0.02 0.08 -0.18 0.13 1
grievances Migration (right) 0.02 0.10 -0.19 0.22 1
protest_participants Climate (left) -0.01 0.04 -0.09 0.06 1
protest_participants Climate (right) -0.03 0.04 -0.11 0.04 1
protest_participants Migration (left) 0.06 0.05 -0.05 0.16 1
protest_participants Migration (right) -0.04 0.07 -0.19 0.10 1
politicization Climate (left) 0.06 0.05 -0.05 0.16 1
politicization Climate (right) 0.07 0.05 -0.04 0.17 1
politicization Migration (left) -0.05 0.08 -0.20 0.11 1
politicization Migration (right) 0.10 0.10 -0.11 0.30 1
gov_lr Climate (left) -0.02 0.02 -0.05 0.02 1
gov_lr Climate (right) -0.01 0.02 -0.05 0.02 1
gov_lr Migration (left) -0.01 0.02 -0.06 0.04 1
gov_lr Migration (right) -0.05 0.03 -0.11 0.01 1

3.3 Frequentist results

3.3.1 Country level models, across all petition types

Code
lm_models <-
  list(
    lm_robust(outcome1b ~ position, data = country_df),
    lm_robust(outcome1b ~ Left*Climate, data = country_df),
    lm_robust(outcome1b ~ Left*Climate + position, data = country_df),
    lm_robust(outcome1b ~ Left*Climate*grievances + position,  data = country_df),
    lm_robust(outcome1b ~ Left*Climate*politicization + position,  data = country_df),
    lm_robust(outcome1b ~ Left*Climate*protest_participants + position, data = country_df),
    lm_robust(outcome1b ~ Left*Climate*grievances + Left*Climate*politicization + Left*Climate*protest_participants + position, data = country_df)
)

lm_models |> texreg::htmlreg(include.ci = FALSE)
Statistical models
  Model 1 Model 2 Model 3 Model 4 Model 5 Model 6 Model 7
(Intercept) 0.13*** 0.21*** 0.16*** 0.14** 0.15** 0.15** 0.13**
  (0.04) (0.01) (0.04) (0.04) (0.05) (0.05) (0.05)
position 0.17*   0.11 0.11 0.09 0.13 0.11
  (0.07)   (0.09) (0.09) (0.09) (0.09) (0.09)
Left   -0.06*** -0.06*** -0.06** -0.04* -0.06*** -0.05*
    (0.01) (0.01) (0.02) (0.02) (0.02) (0.02)
Climate   -0.02* -0.02* -0.06*** -0.03 -0.02 -0.05*
    (0.01) (0.01) (0.02) (0.02) (0.02) (0.02)
Left:Climate   0.05* 0.02 0.03 -0.01 0.03 -0.01
    (0.02) (0.03) (0.04) (0.03) (0.03) (0.05)
grievances       0.04     0.03
        (0.03)     (0.03)
Left:grievances       0.00     0.01
        (0.06)     (0.06)
Climate:grievances       0.10     0.08
        (0.06)     (0.06)
Left:Climate:grievances       -0.02     0.01
        (0.12)     (0.12)
politicization         0.05   0.04
          (0.03)   (0.03)
Left:politicization         -0.05   -0.06
          (0.05)   (0.06)
Climate:politicization         0.05   0.01
          (0.05)   (0.06)
Left:Climate:politicization         0.13   0.14
          (0.11)   (0.12)
protest_participants           -0.01 -0.01
            (0.02) (0.02)
Left:protest_participants           0.03 0.04
            (0.04) (0.04)
Climate:protest_participants           -0.01 -0.00
            (0.04) (0.04)
Left:Climate:protest_participants           -0.06 -0.07
            (0.08) (0.08)
R2 0.06 0.29 0.30 0.36 0.35 0.31 0.41
Adj. R2 0.06 0.27 0.27 0.31 0.30 0.25 0.30
Num. obs. 108 108 108 108 108 104 104
RMSE 0.06 0.05 0.05 0.05 0.05 0.05 0.05
***p < 0.001; **p < 0.01; *p < 0.05

3.3.2 Country level models, across all petition types, including country fixed effects

Code
fe_models <-
  list(
    lm_robust(outcome1b ~ position, fixed_effects = ~country,  data = country_df),
    lm_robust(outcome1b ~ Left*Climate, fixed_effects = ~country,  data = country_df),
    lm_robust(outcome1b ~ Left*Climate + position, fixed_effects = ~country,  data = country_df),
    lm_robust(outcome1b ~ Left*Climate*grievances + position, fixed_effects = ~country,  data = country_df),
    lm_robust(outcome1b ~ Left*Climate*politicization + position, fixed_effects = ~country,  data = country_df),
    lm_robust(outcome1b ~ Left*Climate*protest_participants + position, fixed_effects = ~country,  data = country_df),
    lm_robust(outcome1b ~ Left*Climate*grievances + Left*Climate*politicization + Left*Climate*protest_participants + position, fixed_effects = ~country,  data = country_df)

)
fe_models |> texreg::htmlreg(include.ci = FALSE)
Statistical models
  Model 1 Model 2 Model 3 Model 4 Model 5 Model 6 Model 7
position 0.17**   0.10 0.11 0.09 0.13* 0.12
  (0.06)   (0.06) (0.06) (0.06) (0.06) (0.06)
Left   -0.06*** -0.06*** -0.06*** -0.04** -0.06*** -0.05**
    (0.01) (0.01) (0.01) (0.01) (0.01) (0.02)
Climate   -0.02** -0.02** -0.03 -0.02 -0.02 -0.02
    (0.01) (0.01) (0.03) (0.01) (0.01) (0.04)
Left:Climate   0.05** 0.02 0.03 -0.01 0.04 -0.01
    (0.02) (0.02) (0.03) (0.02) (0.03) (0.03)
grievances       0.00     -0.01
        (0.02)     (0.03)
Left:grievances       0.00     0.01
        (0.04)     (0.05)
Climate:grievances       0.03     0.02
        (0.08)     (0.08)
Left:Climate:grievances       -0.02     0.01
        (0.08)     (0.09)
politicization         0.03   0.04
          (0.04)   (0.04)
Left:politicization         -0.05   -0.06
          (0.05)   (0.05)
Climate:politicization         0.02   0.03
          (0.05)   (0.05)
Left:Climate:politicization         0.13   0.14
          (0.10)   (0.11)
protest_participants              
               
Left:protest_participants           0.03 0.04
            (0.02) (0.02)
Climate:protest_participants           -0.01 -0.02
            (0.02) (0.03)
Left:Climate:protest_participants           -0.06 -0.07
            (0.04) (0.05)
R2 0.45 0.67 0.68 0.68 0.70 0.70 0.72
Adj. R2 0.26 0.55 0.56 0.54 0.56 0.56 0.55
Num. obs. 108 108 108 108 108 104 104
RMSE 0.05 0.04 0.04 0.04 0.04 0.04 0.04
***p < 0.001; **p < 0.01; *p < 0.05

In these models, Left and Migration are centered on zero and so the “main” terms report estimated average effects.

3.3.3 Individual level models, across all petition types

Code
data <- mutate(data, Left = 1*(left == "Left"), Climate = 1*(topic == "Climate change"))

lm_models_i <-
  list(
    lm_robust(outcome1b ~ position, data = data, cluster = Country, se_type = 'stata'),
    lm_robust(outcome1b ~ Left*Climate, data = data, cluster = Country, se_type = 'stata'),
    lm_robust(outcome1b ~ Left*Climate + position, data = data, cluster = Country, se_type = 'stata'),
    lm_robust(outcome1b ~ Left*Climate*grievances + position,  data = data, cluster = Country, se_type = 'stata'),
    lm_robust(outcome1b ~ Left*Climate*politicization + position,  data = data, cluster = Country, se_type = 'stata'),
    lm_robust(outcome1b ~ Left*Climate*protest_participants + position, data = data, cluster = Country, se_type = 'stata'),
    lm_robust(outcome1b ~ Left*Climate*grievances +  Left*Climate*politicization + 
                Left*Climate*protest_participants + 
                position, data = data, cluster = Country, se_type = 'stata')
  )
    
lm_models_i |> texreg::htmlreg(include.ci = FALSE)
Statistical models
  Model 1 Model 2 Model 3 Model 4 Model 5 Model 6 Model 7
(Intercept) 0.17*** 0.26*** 0.21*** 0.22*** 0.19*** 0.22*** 0.20***
  (0.01) (0.01) (0.01) (0.02) (0.03) (0.02) (0.04)
position 0.10***   0.09*** 0.09*** 0.09*** 0.09*** 0.09***
  (0.01)   (0.01) (0.01) (0.01) (0.01) (0.01)
Left   -0.08*** -0.06*** -0.08* -0.03 -0.08** -0.06
    (0.02) (0.02) (0.03) (0.03) (0.02) (0.03)
Climate   -0.04*** -0.03** -0.08*** -0.03 -0.04* -0.06
    (0.01) (0.01) (0.02) (0.02) (0.01) (0.03)
Left:Climate   0.04* 0.02 0.04 -0.01 0.04 0.01
    (0.02) (0.02) (0.03) (0.03) (0.02) (0.04)
grievances       -0.04     -0.04
        (0.07)     (0.07)
Left:grievances       0.06     0.05
        (0.10)     (0.10)
Climate:grievances       0.13     0.09
        (0.07)     (0.07)
Left:Climate:grievances       -0.06     -0.03
        (0.11)     (0.11)
politicization         0.06   0.08
          (0.09)   (0.09)
Left:politicization         -0.11   -0.12
          (0.11)   (0.11)
Climate:politicization         0.01   -0.03
          (0.09)   (0.09)
Left:Climate:politicization         0.11   0.11
          (0.12)   (0.12)
protest_participants           -0.03 -0.03
            (0.04) (0.05)
Left:protest_participants           0.06 0.07
            (0.04) (0.04)
Climate:protest_participants           0.02 0.03
            (0.03) (0.04)
Left:Climate:protest_participants           -0.06 -0.06
            (0.05) (0.05)
R2 0.01 0.01 0.01 0.01 0.01 0.01 0.01
Adj. R2 0.01 0.01 0.01 0.01 0.01 0.01 0.01
Num. obs. 24943 25904 24943 24943 24943 23978 23978
RMSE 0.41 0.41 0.41 0.41 0.41 0.41 0.41
N Clusters 27 27 27 27 27 26 26
***p < 0.001; **p < 0.01; *p < 0.05

3.3.4 Individual level models, across all petition types, including country fixed effects

Code
fe_models_i <-
  list(
    lm_robust(outcome1b ~ position, data = data, cluster = Country, se_type = 'stata', fixed_effects = ~country),
    lm_robust(outcome1b ~ Left*Climate, data = data, cluster = Country, se_type = 'stata', fixed_effects = ~country),
    lm_robust(outcome1b ~ Left*Climate + position, data = data, cluster = Country, se_type = 'stata', fixed_effects = ~country),
    lm_robust(outcome1b ~ Left*Climate*grievances + position,  data = data, cluster = Country, se_type = 'stata', fixed_effects = ~country),
    lm_robust(outcome1b ~ Left*Climate*politicization + position,  data = data, cluster = Country, se_type = 'stata', fixed_effects = ~country),
    lm_robust(outcome1b ~ Left*Climate*protest_participants + position, data = data, cluster = Country, se_type = 'stata', fixed_effects = ~country),
    lm_robust(outcome1b ~ Left*Climate*grievances +  Left*Climate*politicization + 
                Left*Climate*protest_participants + 
                position, data = data, cluster = Country, se_type = 'stata', fixed_effects = ~country)
 )



fe_models_i |> texreg::htmlreg(include.ci = FALSE)
Statistical models
  Model 1 Model 2 Model 3 Model 4 Model 5 Model 6 Model 7
position 0.10***   0.09*** 0.09*** 0.09*** 0.09*** 0.09***
  (0.01)   (0.01) (0.01) (0.01) (0.01) (0.01)
Left   -0.08*** -0.06*** -0.08* -0.03 -0.08** -0.06
    (0.02) (0.02) (0.03) (0.03) (0.02) (0.03)
Climate   -0.04*** -0.03** -0.05 -0.02 -0.04** -0.02
    (0.01) (0.01) (0.03) (0.02) (0.01) (0.03)
Left:Climate   0.04* 0.02 0.04 -0.01 0.04 0.01
    (0.02) (0.02) (0.03) (0.03) (0.02) (0.04)
grievances       -0.04     -0.04
        (0.06)     (0.06)
Left:grievances       0.06     0.05
        (0.10)     (0.10)
Climate:grievances       0.06     0.03
        (0.08)     (0.08)
Left:Climate:grievances       -0.07     -0.03
        (0.11)     (0.11)
politicization         0.07   0.09
          (0.08)   (0.07)
Left:politicization         -0.11   -0.11
          (0.11)   (0.11)
Climate:politicization         -0.04   -0.04
          (0.07)   (0.07)
Left:Climate:politicization         0.11   0.11
          (0.12)   (0.12)
protest_participants              
               
Left:protest_participants           0.06 0.07
            (0.04) (0.04)
Climate:protest_participants           0.02 0.01
            (0.03) (0.04)
Left:Climate:protest_participants           -0.06 -0.06
            (0.05) (0.05)
R2 0.01 0.01 0.02 0.02 0.02 0.02 0.02
Adj. R2 0.01 0.01 0.02 0.02 0.02 0.02 0.02
Num. obs. 24943 25904 24943 24943 24943 23978 23978
RMSE 0.41 0.41 0.41 0.41 0.41 0.41 0.41
N Clusters 27 27 27 27 27 26 26
***p < 0.001; **p < 0.01; *p < 0.05

3.4 Duration and signing

Evidence on whether “speeders” are more or less likely to sign.

Code
data |> mutate(signing = factor(outcome1b)) |> 
  ggplot(aes(duration, linetype = signing)) + 
  geom_density() +  xlim(0, 2500)

3.5 Additional Bayesian analyses

The “grievance” measure in the text is generated using a migration measure for migration petitions and climate measure for climate petitions. The “politicization” measure is generated by combining salience and polarization measures.

The below analyses implement Analysis 3 using these subcomponents and close substitute measures.

Code
if(run)
  vs_other |>
  lapply(function(v) {
    treatment_groups |> lapply(function(g) {
    f(data |> filter(groups==g & position > .5), v, iter = 5000)})}) |>
  write_rds("M_1.3_by_group_other.rds")

M_1.3_by_group_other <-  read_rds("saved/M_1.3_by_group_other.rds")
names(M_1.3_by_group_other) <- vs_other

names(vs_other) <- vs_other

fig.1.3.by_appendix_df <- 
  vs_other |> lapply(function(v)
  tgg |>
    lapply(function(g) sumy(v,g, model_list = M_1.3_by_group_other)) |> 
    bind_rows(.id = "group")) |>
    bind_rows(.id = "var") |>
  mutate(var = factor(var, vs_other, vs_other_labs))

fig.1.3.by_appendix_df |> filter(max_rhat < 1.01) |>
  ggplot(aes(mean, group)) + geom_point() + facet_wrap(~var, scales = "free_x") + 
  geom_errorbarh(aes(xmin = lower, xmax = upper, height = .2)) +
  geom_vline(xintercept = 0, color = "red")

Alternative covariates

In response to comments by reviewers, the below analyses implement Analysis 3 using additional measures capturing Lijphart’s distinction between majoritarian and consensus democracy to investigate the effect of political opportunity structures.

Code
fig.1.3.by.extra <- 
  vars[c(4,5)] |> lapply(function(v)
  tgg |>
    lapply(function(g) sumy(v,g)) |> 
    bind_rows(.id = "group")) |>
    bind_rows(.id = "var") |>
  mutate(var = factor(var, vs, vs_labels)) |>
  ggplot(aes(mean, group)) + geom_point() + facet_wrap(~var, scales = "free_x") + 
  geom_errorbarh(aes(xmin = lower, xmax = upper, height = .2)) +
  geom_vline(xintercept = 0, color = "red")

fig.1.3.by.extra  

Additional covariates