How to read this file

This is a replication file. It contains every line of R code needed to reproduce the tables and figures of Chapter 10, together with an explanation of what each line does. The code is exactly the code we ran for the book; nothing has been simplified.

Chapter 10 briefly discusses what the literature says about the relationship between vote-seeking incentives and campaigns, captured with multiple indicators. It uses data from a large, ongoing project — the Comparative Candidate Survey (CCS) — to examine whether what respondents say about the focus of their campaigns is related to the intraparty incentives they face from the electoral systems in which they are competing. We find that candidates who face strong personal vote-seeking incentives — systems where Average Personalism (AP) is high — say they put greater emphasis on themselves as individuals, not their parties as collectives.

Two levels of analysis, run in parallel. This is the organising feature of the chapter and the reason so much code appears twice.

The Comparative Candidate Survey interviews individual candidates, so the data exist at two levels: the country, where one AP score describes a whole system, and the individual candidate, who sits in a particular district with its own AP score. Both are informative, and they can disagree — a candidate in a low-magnitude district of a generally party-centred system faces different incentives from the national average.

So the chapter loads two sets of GBM objects (..._district_objects and ..._country_nofamily_objects), produces two sets of predictions, estimates models at both levels, and reports Figure 10.2 as two panels. When you meet a block that looks like one you have already read, check which level it is operating at.

If you have never used R. Click the grey Code button above any block to show or hide the code. Every line beginning with # is a comment, ignored by R and written purely for you. Conventions used throughout:

  • <- is R’s assignment arrow; x <- 5 stores 5 under the name x.
  • list() holds several objects at once; mylist[[3]] retrieves the third.
  • for (i in 1:5) { ... } repeats the braced instructions five times.
  • df$col pulls the column col out of the data frame df.
  • %>% is the “pipe”: a %>% f() is another way of writing f(a).
  • A model formula like y ~ x reads “y is explained by x”.

Setup

Nothing on this page is cached. knitr can store a slow block’s results and reuse them on the next knit (the cache=TRUE chunk option). That speeds up editing, but it invalidates a stored result only when that block’s own code changes — not when a function or dataset it depends on is edited elsewhere in the file. A stale cache therefore produces output that looks current and is not, and one such cache did hide a real correction to this project’s code. Every block here runs on every knit. The file takes longer to compile; what it prints is always what the code currently says.

# Load relevant libraries
library(gbm) # gradient boosting machines: supply the AP and TDE predictions
library(fixest) # fast fixed-effects estimation; supplies feols() below
# plyr is NOT attached. Its ddply() is called as plyr::ddply() below.
# Attaching plyr would mask dplyr's summarise(), mutate(), rename() and
# arrange() -- plyr has functions of the same names that ignore dplyr
# grouping -- and that masking would persist for any other chapter
# rendered afterwards in the same R session.
library(mice) # multiple imputation; pools results across the five datasets
library(miscTools) # assorted utilities; supplies colMedians()
library(kableExtra) # formats knitr tables for HTML output
library(tidyverse) # bundle of data-handling packages (dplyr, ggplot2, ...)
library(sjPlot) # model plots used in the interaction figure

Auxiliar functions

The next block defines the functions that produce every expected value and confidence interval in Figure 10.2. Nothing runs here — defining a function is like writing down a recipe, and the cooking happens later.

One recipe, two kitchens. Because the chapter works at two levels, this block defines two bootstrap functions with the same internal logic: one operating on country-level data, one on individual-level data. The name tells you which is which — .cty for country, and its individual-level counterpart further down.

The shared recipe: fix AP at a chosen value, leave every other characteristic as observed, ask the fitted model what campaign focus to expect, then repeat on 500 resampled datasets to see how much that answer moves. Calling it across a range of AP values traces the lines in Figure 10.2.

# Function to run bootstrap, district level
# Adapted from https://www.r-bloggers.com/2013/01/the-cluster-bootstrap/

# Function to run bootstrap, country level
# Arguments: myvalue is the level of AP to impose; reps is the number of
# bootstrap resamples; seed fixes the random draws so results reproduce exactly.
boot.predict.se.cty <- function(myvalue = 0.5, reps = 500, seed = 123354) {
  require(fixest)

  # Set Seed
  set.seed(seed)

  # Run Model
  temp_list <- list()
  temp_list[[1]] <- feols(repCont2 ~ pers.hat1 + totalEff.hat1 | year, se = "hetero", data = tempData)
  temp_list[[2]] <- feols(repCont2 ~ pers.hat2 + totalEff.hat2 | year, se = "hetero", data = tempData)
  temp_list[[3]] <- feols(repCont2 ~ pers.hat3 + totalEff.hat3 | year, se = "hetero", data = tempData)
  temp_list[[4]] <- feols(repCont2 ~ pers.hat4 + totalEff.hat4 | year, se = "hetero", data = tempData)
  temp_list[[5]] <- feols(repCont2 ~ pers.hat5 + totalEff.hat5 | year, se = "hetero", data = tempData)

  # Point Estimate
  newData <- data.frame(
    pers.hat1 = myvalue,
    pers.hat2 = myvalue,
    pers.hat3 = myvalue,
    pers.hat4 = myvalue,
    pers.hat5 = myvalue,
    totalEff.hat1 = median(tempData$totalEff.hat1),
    totalEff.hat2 = median(tempData$totalEff.hat2),
    totalEff.hat3 = median(tempData$totalEff.hat3),
    totalEff.hat4 = median(tempData$totalEff.hat4),
    totalEff.hat5 = median(tempData$totalEff.hat5),
    year = as.integer(median(tempData$year))
  ) # copy data
  point.estimate1 <- predict(temp_list[[1]], newdata = newData)
  point.estimate2 <- predict(temp_list[[2]], newdata = newData)
  point.estimate3 <- predict(temp_list[[3]], newdata = newData)
  point.estimate4 <- predict(temp_list[[4]], newdata = newData)
  point.estimate5 <- predict(temp_list[[5]], newdata = newData)

  # Avg
  point.estimate <- (point.estimate1 + point.estimate2 + point.estimate3 + point.estimate4 + point.estimate5) / 5


  # Calculate SE
  sterrs <- matrix(NA, nrow = reps, ncol = 5)
  for (i in 1:reps) {
    # Create Data
    index <- sample(1:nrow(tempData), nrow(tempData), replace = TRUE) # sample clusters
    bootdat <- tempData[index, ]
    # Run Model
    temp_list_boot <- list()
    temp_list_boot[[1]] <- feols(repCont2 ~ pers.hat1 + totalEff.hat1 | year, se = "hetero", data = bootdat)
    temp_list_boot[[2]] <- feols(repCont2 ~ pers.hat2 + totalEff.hat2 | year, se = "hetero", data = bootdat)
    temp_list_boot[[3]] <- feols(repCont2 ~ pers.hat3 + totalEff.hat3 | year, se = "hetero", data = bootdat)
    temp_list_boot[[4]] <- feols(repCont2 ~ pers.hat4 + totalEff.hat4 | year, se = "hetero", data = bootdat)
    temp_list_boot[[5]] <- feols(repCont2 ~ pers.hat5 + totalEff.hat5 | year, se = "hetero", data = bootdat)
    # Calculate predict value
    newDataBootData <- data.frame(
      pers.hat1 = myvalue,
      pers.hat2 = myvalue,
      pers.hat3 = myvalue,
      pers.hat4 = myvalue,
      pers.hat5 = myvalue,
      totalEff.hat1 = median(bootdat$totalEff.hat1),
      totalEff.hat2 = median(bootdat$totalEff.hat2),
      totalEff.hat3 = median(bootdat$totalEff.hat3),
      totalEff.hat4 = median(bootdat$totalEff.hat4),
      totalEff.hat5 = median(bootdat$totalEff.hat5),
      year = as.integer(median(bootdat$year))
    ) # copy data
    sterrs[i, 1] <- predict(temp_list_boot[[1]], newdata = newDataBootData)
    sterrs[i, 2] <- predict(temp_list_boot[[2]], newdata = newDataBootData)
    sterrs[i, 3] <- predict(temp_list_boot[[3]], newdata = newDataBootData)
    sterrs[i, 4] <- predict(temp_list_boot[[4]], newdata = newDataBootData)
    sterrs[i, 5] <- predict(temp_list_boot[[5]], newdata = newDataBootData)
  }
  # prepare to return results
  sd_pool <- mean(apply(sterrs, 2, var, na.rm = TRUE), na.rm = TRUE) + (5 + 1) / (5 * (5 - 1)) * (sum((colMeans(sterrs, na.rm = TRUE) - mean(sterrs, na.rm = TRUE))^2))
  # CI
  lb95 <- point.estimate + sd_pool * qnorm(0.025)
  ub95 <- point.estimate + sd_pool * qnorm(0.975)
  lb90 <- point.estimate + sd_pool * qnorm(0.05)
  ub90 <- point.estimate + sd_pool * qnorm(0.95)
  val <- c(point.estimate, sd_pool, lb95, ub95, lb90, ub90)
  names(val) <- c("Point Estimate", "Std. Error", "lb95", "ub95", "lb90", "ub90")
  return(val)
}


# Function to run bootstrap, Individual Level
boot.predict.se.ind <- function(myvalue = 0.5, reps = 500, seed = 123354) {
  require(fixest)
  require(miscTools)

  # Set Seed
  set.seed(seed)

  # Run Model
  temp_list <- list()
  temp_list[[1]] <- feols(repCont2 ~ pers.hat1 + totalEff.hat1 + winning + ideolDistance + livesConst + pastMP + partyHier | year, se = "hetero", data = tempData_ind)
  temp_list[[2]] <- feols(repCont2 ~ pers.hat2 + totalEff.hat2 + winning + ideolDistance + livesConst + pastMP + partyHier | year, se = "hetero", data = tempData_ind)
  temp_list[[3]] <- feols(repCont2 ~ pers.hat3 + totalEff.hat3 + winning + ideolDistance + livesConst + pastMP + partyHier | year, se = "hetero", data = tempData_ind)
  temp_list[[4]] <- feols(repCont2 ~ pers.hat4 + totalEff.hat4 + winning + ideolDistance + livesConst + pastMP + partyHier | year, se = "hetero", data = tempData_ind)
  temp_list[[5]] <- feols(repCont2 ~ pers.hat5 + totalEff.hat5 + winning + ideolDistance + livesConst + pastMP + partyHier | year, se = "hetero", data = tempData_ind)

  # Point Estimate
  newData <- as.data.frame(t(miscTools::colMedians(tempData_ind))) # copy data
  newData$year <- as.integer(newData$year)
  newData[, 3:7] <- myvalue # Replace education value
  point.estimate1 <- predict(temp_list[[1]], newdata = newData)
  point.estimate2 <- predict(temp_list[[2]], newdata = newData)
  point.estimate3 <- predict(temp_list[[3]], newdata = newData)
  point.estimate4 <- predict(temp_list[[4]], newdata = newData)
  point.estimate5 <- predict(temp_list[[5]], newdata = newData)

  # Avg
  point.estimate <- (point.estimate1 + point.estimate2 + point.estimate3 + point.estimate4 + point.estimate5) / 5


  # Calculate SE
  sterrs <- matrix(NA, nrow = reps, ncol = 5)
  index <- c()
  bootdat <- c()
  temp_list_boot <- list()
  for (i in 1:reps) {
    # Create Data
    index <- sample(1:nrow(tempData_ind), nrow(tempData_ind), replace = TRUE) # sample clusters
    bootdat <- tempData_ind[index, ]
    # Run Model
    temp_list_boot[[1]] <- feols(repCont2 ~ pers.hat1 + totalEff.hat1 + winning + ideolDistance + livesConst + pastMP + partyHier | year, se = "hetero", data = bootdat)
    temp_list_boot[[2]] <- feols(repCont2 ~ pers.hat2 + totalEff.hat2 + winning + ideolDistance + livesConst + pastMP + partyHier | year, se = "hetero", data = bootdat)
    temp_list_boot[[3]] <- feols(repCont2 ~ pers.hat3 + totalEff.hat3 + winning + ideolDistance + livesConst + pastMP + partyHier | year, se = "hetero", data = bootdat)
    temp_list_boot[[4]] <- feols(repCont2 ~ pers.hat4 + totalEff.hat4 + winning + ideolDistance + livesConst + pastMP + partyHier | year, se = "hetero", data = bootdat)
    temp_list_boot[[5]] <- feols(repCont2 ~ pers.hat5 + totalEff.hat5 + winning + ideolDistance + livesConst + pastMP + partyHier | year, se = "hetero", data = bootdat)
    # Calculate predict value
    newDataBootData <- as.data.frame(t(miscTools::colMedians(bootdat))) # copy data
    newDataBootData$year <- as.integer(newDataBootData$year)
    newDataBootData[, 3:7] <- myvalue # Replace education value
    sterrs[i, 1] <- predict(temp_list_boot[[1]], newdata = newDataBootData)
    sterrs[i, 2] <- predict(temp_list_boot[[2]], newdata = newDataBootData)
    sterrs[i, 3] <- predict(temp_list_boot[[3]], newdata = newDataBootData)
    sterrs[i, 4] <- predict(temp_list_boot[[4]], newdata = newDataBootData)
    sterrs[i, 5] <- predict(temp_list_boot[[5]], newdata = newDataBootData)
  }
  # prepare to return results
  sd_pool <- mean(apply(sterrs, 2, var, na.rm = TRUE), na.rm = TRUE) + (5 + 1) / (5 * (5 - 1)) * (sum((colMeans(sterrs, na.rm = TRUE) - mean(sterrs, na.rm = TRUE))^2))
  # CI
  lb95 <- point.estimate + sd_pool * qnorm(0.025)
  ub95 <- point.estimate + sd_pool * qnorm(0.975)
  lb90 <- point.estimate + sd_pool * qnorm(0.05)
  ub90 <- point.estimate + sd_pool * qnorm(0.95)
  val <- c(point.estimate, sd_pool, lb95, ub95, lb90, ub90)
  names(val) <- c("Point Estimate", "Std. Error", "lb95", "ub95", "lb90", "ub90")
  return(val)
}
# Function to extract estimates for table 7.4
extract_Estimates <- function(model_name) {
  out <- ifelse(model_name$p.value < 0.01, paste0(round(model_name$estimate, 3), "***", " (", round(model_name$std.error, 3), ")"),
    ifelse(model_name$p.value < 0.05, paste0(round(model_name$estimate, 3), "**", " (", round(model_name$std.error, 3), ")"),
      ifelse(model_name$p.value < 0.1, paste0(round(model_name$estimate, 3), "*", " (", round(model_name$std.error, 3), ")"),
        paste0(round(model_name$estimate, 3), " (", round(model_name$std.error, 3), ")")
      )
    )
  )
  names(out) <- model_name$term
  out <- data.frame(out)
  out$variable <- row.names(out)
  return(out)
}

Introduction

Chapter 10 focuses on campaigns for office, a crucial stage in the electoral process where candidates and parties communicate with voters to secure support. Building on the concept of Average Personalism (AP) introduced earlier in the book, this chapter examines how electoral system incentives shape the degree to which candidates emphasize their personal attributes versus their party’s collective identity during campaigns.

The chapter reviews existing literature on campaign intensity, complexity, and spending, highlighting how personal vote-seeking incentives vary across different electoral systems. Using data from the Comparative Candidate Survey (CCS), we empirically test whether candidates in systems with higher AP scores report greater emphasis on personal campaigning. The analysis shows a clear association between intraparty incentives and candidates’ campaign focus.

This file contains calls to datasets used in Chapter 10 (Campaigns for Office), as well as the code necessary to produce all graphs in the chapter.

Data processing

This chunk loads and prepares the datasets and machine learning objects used in Chapter 10, Campaigns for Office. It sets the working directory, imports both individual- and country-level campaign data, and retrieves the results of previously estimated GBM models used to calculate personalism and Duvergerian effects across electoral districts and countries.

Paths in this replication package. Every data file is addressed relative to the folder holding this document, as data/shared/... for files several chapters use, or data/ch10/... and so on for files specific to one chapter.

Nothing needs editing. knitr runs each code block with the working directory set to the document’s own folder, which is the root of this repository, so the paths resolve wherever the repository is cloned. There are no setwd() calls and no absolute paths anywhere in these files.

# Set the path to Chapter 10 materials
# the short file names below. This is the one line to edit for your machine.


# Note about threshold for Austria 2008:
# Austria uses a intra-provitial threshold. We, however, don't have
# the information on district ID for Austria. As a result, we
# decided to use the second and third tiers threshold (0.04)
# (A coding decision recorded here for transparency: where a rule could not be
# measured as defined, the nearest available equivalent was substituted.)

# load ccs's data
# The Comparative Candidate Survey, in two forms: one row per country-election,
# and one row per individual candidate. The two levels of the chapter.
load(file = "data/ch10/RData/ccs_country_final.RData")
load(file = "data/ch10/RData/ccs_individual_final.RData")

# Load district-level GBM objects
# Fitted models from Chapter 6. Nothing is estimated here -- they are only
# asked to predict AP and TDE for the electoral systems in the CCS.
load(file = "data/shared/AP_district_objects_t285_d13.RData")
load(file = "data/shared/TDE_district_objects_t485_d9.RData")

# create objects with optimal GBM
# $optimalGBM holds the best-performing fitted model from each of five training
# runs, so each of these is a list of five models.
optimalGBMIntra <- totalAP.objects$optimalGBM
optimalGBMInter <- totalEffENP.objects$optimalGBM

# load country-level objects
# IMPORTANT: these load under the SAME names as the district-level objects
# above, overwriting them. That is safe only because the district versions were
# already copied into optimalGBMIntra / optimalGBMInter on the two lines above.
# "nofamily" records a modelling choice: these models do not use electoral
# system family as a predictor, only the component rules.
load(file = "data/shared/AP_country_nofamily_objects.RData")
load(file = "data/shared/TDE_country_nofamily_objects.RData")

### Get the objects from list
# The country-level equivalents, named with a Cty suffix.
optimalGBMIntraCty <- totalAP.objects$optimalGBM
optimalGBMInterCty <- totalEffENP.objects$optimalGBM

# remove GBM objects
# Delete the bulky originals now that both sets have been extracted.
rm(totalAP.objects)
rm(totalEffENP.objects)

# load country-level scores
# Pre-computed TDE and AP for real systems. Note the vintage in the filename:
# chapters use different versions of this file and they are not interchangeable.
load(file = "data/shared/RealSystems_Scores_GBM_Aug_2024.RData")

Predicted values of Average Personalism (AP) and Total Duvergerian Effect (TDE)

This section estimates predicted values of Average Personalism (AP) and Total Duvergerian Effect (TDE) at both the individual and country levels by applying gradient boosting models (GBM) across five imputations of the data. It then examines how these predicted electoral incentives correlate with a key dependent variable, a proxy for candidates’ self-reported campaign focus (i.e., personal- vs. party-centered campaigns). This analysis provides an empirical test of the expectation that personal vote-seeking incentives are associated with more candidate-centered campaigning.

### Run predictions of TDE and AP five times (one per imputation)

# Keep only candidate-level observations with complete electoral rule data
indLevelComplete <- subset(indLevel, !is.na(M) & !is.na(ballot_type) & !is.na(pool_level) & !is.na(new.nvotes) & !is.na(formula) & !is.na(threshold))

# Exclude specific country-election cases due to missing or problematic district info (see reasons below)
indLevelComplete <- subset(indLevelComplete, !(ctyyear %in% c("Germany-2005", "Germany-2009", "Hungary-2010", "New Zealand-2011")))
# Germany-2005: no district or M
# Germany-2009: no district or M for PR (?) candidates
# New Zealand-2011:: no district or M
# Hungary-2010: seems to be only PR candidates


# Predict Interparty and Intraparty

indLevelData <- list()
for (i in 1:5) {
  # Interparty
  indLevelComplete$totalEff.hat <- predict(optimalGBMInter[[i]], indLevelComplete, n.trees = optimalGBMInter[[i]]$n.trees)
  # Intraparty
  indLevelComplete$pers.hat <- predict(optimalGBMIntra[[i]], indLevelComplete, n.trees = optimalGBMIntra[[i]]$n.trees)
  # Put in a list
  indLevelData[[i]] <- indLevelComplete
}

# Get TDE and AP, then average the values across datasets
all_TDE_AP_ind <- do.call("rbind", sapply(1:5, function(i) indLevelData[[i]][, c("ctyyear", "id", "totalEff.hat", "pers.hat", "repCont2")], simplify = FALSE))

avg_TDE_AP_ind <- plyr::ddply(
  .data = all_TDE_AP_ind, .variables = c("ctyyear", "id"), .fun = plyr::summarise,
  totalEff.hat = mean(totalEff.hat),
  pers.hat = mean(pers.hat),
  repCont2 = mean(repCont2)
)

#############################################
# Correlate AP and campaign personalization  (repCont2) (individual-level)
cor.test(avg_TDE_AP_ind$repCont2, avg_TDE_AP_ind$pers.hat)
## 
##  Pearson's product-moment correlation
## 
## data:  avg_TDE_AP_ind$repCont2 and avg_TDE_AP_ind$pers.hat
## t = 9, df = 11742, p-value <2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  0.0659 0.1018
## sample estimates:
##    cor 
## 0.0838
#############################################

# Keep cases with complete information about electoral rules
# We also remove the following countries (see reasons below)
ctyLevelComplete <- subset(ctyLevel, !(ctyyear %in% c("Germany-2005", "Germany-2009", "Hungary-2010", "New Zealand-2011")))
# Germany-2005: no district or M
# Germany-2009: no district or M for PR (?) candidates
# New Zealand-2011:: no district or M
# Hungary-2010: seems to be only PR candidates

# Keep first election in the year
data2export <- subset(data2export, election_count == 1)

# We need to estimate ap and tde for a few countries not included in our dataset
toEstimate <- subset(ctyLevelComplete, country %in% c("Malta", "Romania", "Chile", "Montenegro") & year != 2012)
ctyLevelComplete <- subset(ctyLevelComplete, !(country %in% c("Malta", "Romania", "Chile", "Montenegro") & year != 2012))

# Remove Romania 2012
ctyLevelComplete <- subset(ctyLevelComplete, !(country %in% c("Romania") & year == 2012))

ctyLevelData <- list()
for (i in 1:5) {
  temp <- merge(ctyLevelComplete, data2export[, c("country", "year", paste0("totalEff.hat", i), paste0("pers.hat", i))], by = c("country", "year"), all.x = TRUE)
  names(temp)[12:13] <- c("totalEff.hat", "pers.hat")
  # Interparty
  toEstimate$totalEff.hat <- predict(optimalGBMInter[[i]], toEstimate, n.trees = optimalGBMInter[[i]]$n.trees)
  # Intraparty
  toEstimate$pers.hat <- predict(optimalGBMIntra[[i]], toEstimate, n.trees = optimalGBMIntra[[i]]$n.trees)
  # Add info
  temp <- rbind(temp, toEstimate)
  # Put in a list
  ctyLevelData[[i]] <- temp
}


# Get TDE and AP, then average the values across datasets at the country level
all_TDE_AP_cty <- do.call("rbind", sapply(1:5, function(i) ctyLevelData[[i]][, c("country", "year", "totalEff.hat", "pers.hat", "repCont2")], simplify = FALSE))
avg_TDE_AP_cty <- plyr::ddply(
  .data = all_TDE_AP_cty, .variables = c("country", "year"), .fun = plyr::summarise,
  totalEff.hat = mean(totalEff.hat),
  pers.hat = mean(pers.hat),
  repCont2 = mean(repCont2)
)

#############################################

# Correlation between ap and repCont2
cor.test(all_TDE_AP_cty$repCont2, all_TDE_AP_cty$pers.hat)
## 
##  Pearson's product-moment correlation
## 
## data:  all_TDE_AP_cty$repCont2 and all_TDE_AP_cty$pers.hat
## t = 12, df = 208, p-value <2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  0.568 0.724
## sample estimates:
##   cor 
## 0.653

The correlation results reveal two key findings:

At the individual level, the correlation between predicted personal vote-seeking incentives (pers.hat, or AP) and self-reported focus on personal reputation (repCont2) is positive but weak (r = 0.084), though highly statistically significant. This small but consistent relationship suggests that even controlling for variation across candidates and systems, personalism in the electoral rules modestly shapes how candidates frame their campaigns. At the country level, the correlation is much stronger (r = 0.653), indicating that in aggregate, systems with higher average AP scores are substantially more likely to foster campaigns that focus on candidates rather than parties.

These results support the theoretical expectations laid out in Chapter 10. Specifically, they affirm that candidates competing in systems with strong personal vote incentives (high AP) report focusing their campaigns more on their personal reputations than their party labels. The stark contrast between the individual-level and country-level results highlights how systemic features of electoral design exert powerful contextual effects on campaign strategy, beyond individual traits or perceptions. As discussed in the chapter, this validates the broader claim that electoral systems shape the personalism of campaigns through institutional incentives, not just through candidate agency.

Figure 10.1: Attention to Candidate

This section replicates Figure 10.1 from Chapter 10, which visualizes the distribution of candidates’ self-reported campaign focus—whether they aimed to attract attention to themselves or to their party—across countries. Using data from the Comparative Candidate Survey (CCS), the variable repCont2 captures this self-placement, with higher values indicating a more personalistic campaign strategy. Countries are grouped and shaded according to their ballot type (open, flexible, closed), allowing for a visual comparison between electoral systems and their personal vote-seeking incentives.

How to read Figure 10.1. Descriptive, before any modelling: one row per country showing how its candidates placed themselves on the self-versus-party scale, shaded by ballot type.

The expectation is an ordering — open-ballot systems toward the personalistic end, closed-list systems toward the party end, flexible lists between. Look for whether the shading sorts cleanly from one side to the other, and note the exceptions, since those are the cases where the rest of the chapter’s more careful measure (AP) should do better than ballot type alone as a summary.

## Figure 10.1 — Replication of Attention to Candidate by Country

# Select only relevant columns: campaign focus, country, and ballot type
new_indLevel <- indLevel[, c("repCont2", "country", "ballot_type")]

# Calculate the mean attention-to-candidate score by country and ballot type
ind_colors <- with(new_indLevel, aggregate(repCont2, list(country, ballot_type), mean, na.rm = T))

# Rename columns for clarity
names(ind_colors) <- c("country", "ballot_type", "avg")

# Assign fill colors: darker greys for more open ballots
ind_colors$col[ind_colors$ballot_type == "closed"] <- "grey85"
ind_colors$col[ind_colors$ballot_type == "flexible"] <- "grey55"
ind_colors$col[ind_colors$ballot_type == "open"] <- "grey30"

# Sort countries by ballot type and mean campaign focus score
ind_colors <- ind_colors[order(ind_colors$ballot_type, ind_colors$avg), ]

# Assign a plotting order
ind_colors$orderN <- 1:nrow(ind_colors)

# Merge order into the original data
new_indLevel <- merge(new_indLevel, ind_colors[, c("country", "orderN")], by = "country")
# pdf('~/Downloads/Plots/dv_campaign.pdf', 12)

# Set margins and plot the boxplot
par(mar = c(12, 6, 2, 10))
boxplot(new_indLevel$repCont2 ~ new_indLevel$orderN,
  las = 2, # Rotate country labels
  col = ind_colors$col, # Use assigned fill colors
  names = ind_colors$country, # Label x-axis with country names
  xlab = "", # No x-axis label
  pch = 19, # Solid points for outliers
  frame = FALSE, # Clean plot frame
  cex.axis = 1.75, # Scale axis text
  cex.lab = 1.75, # Scale y-axis label
  ylab = "Attention to Candidate"
) # Y-axis label

# Add legend for ballot types
par(mar = c(12, 2, 2, 0), new = TRUE)
legend(
  x = 27, y = 6, legend = c("Open", "Flexible", "Closed"),
  col = c("grey30", "grey55", "grey85"), fill = c("grey30", "grey55", "grey85"),
  title = "Ballot Type:", bty = "n", cex = 1.75
)

# dev.off()

The figure confirms the book’s theoretical expectation that ballot structure shapes campaign strategy. Candidates in open-list systems (dark grey) report substantially more candidate-centered campaigns, with countries like Ireland and Estonia showing median values around 6 or above. In contrast, closed-list systems (light grey), such as Albania, Portugal, and Australia, are strongly party-centered, clustering below the midpoint of the scale. The case of flexible ballot systems (medium grey) highlights how institutional details matter. Countries like Sweden and Iceland exhibit lower personalization—similar to closed-list countries—likely due to restrictive thresholds for list reordering. Others, such as Malta and Denmark, allow more candidate influence and show correspondingly higher scores.

Overall, Figure 10.1 supports the argument that personal vote-seeking incentives—proxied by electoral system rules—are visibly associated with how candidates portray their campaigns.

Models

This section estimates five linear models to evaluate the relationship between electoral system incentives—measured through predicted Average Personalism (AP, pers.hat) and Total Duvergerian Effect (TDE, totalEff.hat)—and candidates’ campaign focus (repCont2).

Five models across the two levels. Models 1 and 2 use the country data; models 3, 4 and 5 use the individual data. Within each level the specification builds up the same way — AP alone, then AP plus TDE, then (individual level only) a set of candidate characteristics.

The outcome throughout is repCont2: the candidate’s self-placement on whether the campaign sought attention for themselves or for the party, with higher values meaning more personalistic. | year adds year fixed effects, so comparisons are made within election years rather than across them.

Model 5 is the demanding one. Its controls ask whether the AP effect is really about electoral incentives or merely reflects who the candidate is: winning (did they win), ideolDistance (distance from their party), livesConst (do they live in the constituency), pastMP (incumbency) and partyHier (position in the party hierarchy). If AP survives those, the incentive story holds.

# Initialize lists to store five versions of each model (one per imputation)
# Five containers, each of which will hold five fitted models -- one per set of
# GBM predictions.
m1_l <- list()
m2_l <- list()
m3_l <- list()
m4_l <- list()
m5_l <- list()

# Loop through all 5 imputations and estimate each model
# One pass fits all five specifications to prediction set i. Note which data
# object each uses: ctyLevelData for models 1-2, indLevelData for models 3-5.
for (i in 1:5) {
  # Model 1: Country-level regression, AP as sole predictor, with year fixed effects
  m1_l[[i]] <- feols(repCont2 ~ pers.hat | year, se = "hetero", data = ctyLevelData[[i]])
  # Model 2: Country-level regression with both AP and TDE
  # Adding TDE checks that AP is not standing in for the interparty dimension.
  m2_l[[i]] <- feols(repCont2 ~ pers.hat + totalEff.hat | year, se = "hetero", data = ctyLevelData[[i]])
  # Model 3: Individual-level regression, AP as sole predictor
  # The same question asked of individual candidates rather than country means.
  m3_l[[i]] <- feols(repCont2 ~ pers.hat | year, se = "hetero", data = indLevelData[[i]])
  # Model 4: Individual-level regression with both AP and TDE
  m4_l[[i]] <- feols(repCont2 ~ pers.hat + totalEff.hat | year, se = "hetero", data = indLevelData[[i]])
  # Model 5: Individual-level regression with AP, TDE, and five controls
  # The strictest specification; see the note above on what the controls do.
  m5_l[[i]] <- feols(repCont2 ~ pers.hat + totalEff.hat
    + winning + ideolDistance + livesConst + pastMP + partyHier | year, se = "hetero", data = indLevelData[[i]])
}

# Use mice::pool to combine results
# pool() applies Rubin's rules to the five fitted versions of each model,
# returning one combined set of coefficients and standard errors; summary()
# presents it as the tidy table extract_Estimates() expects.
out1 <- summary(mice::pool(m1_l))
out2 <- summary(mice::pool(m2_l))
out3 <- summary(mice::pool(m3_l))
out4 <- summary(mice::pool(m4_l))
out5 <- summary(mice::pool(m5_l))

How to read Table 10.1. Five columns matching the five models: (1) and (2) are country-level, (3), (4) and (5) individual-level.

Read the AP row across. A positive coefficient means stronger personal vote-seeking incentives go with more self-focused campaigns, which is the chapter’s claim. The interesting comparisons are (2) versus (1) — does AP survive the addition of TDE? — and (5) versus (4) — does it survive the candidate’s own characteristics?

Then check the TDE row as a placebo. The argument is about intraparty incentives, so the interparty measure should not carry the effect.

Table 10.1: Association between AP and Campaign Focus

This code constructs Table 10.1, which summarizes five linear regression models estimating the association between electoral system incentives—especially Average Personalism (AP)—and candidates’ self-reported campaign focus on themselves rather than their party. The table pools estimates across five imputations and includes both country- and individual-level models, with and without controls. It also computes pseudo R² and adjusted R² values to assess model fit and adds key statistics to the output.

# We need to build a table for the book

# Combine pooled model summaries into a list
model_list <- list(out1, out2, out3, out4, out5)

# Extract cleaned estimates (coef and SE) from each model
out_estimates <- sapply(model_list, extract_Estimates, simplify = FALSE)

# Create table
# Merge all extracted estimates into one table, using the 'variable' name as the key
mytable <- merge(out_estimates[[1]], out_estimates[[2]], by = "variable", all = TRUE)
for (i in c(3:5)) mytable <- merge(mytable, out_estimates[[i]], by = "variable", all = TRUE)

# Reorder variables
mytable <- mytable[c(5, 6, 7, 1, 2, 4, 3), ]

# Rename rows
row.names(mytable) <- mytable$variable
mytable$variable <- NULL

# Calculate TSS
TSS_cty <- sum((ctyLevelData[[1]]$repCont2 - mean(ctyLevelData[[1]]$repCont2))^2)
TSS_ind <- sum((indLevelData[[1]]$repCont2 - mean(indLevelData[[1]]$repCont2))^2)
TSS_ind2 <- sum((na.omit(indLevelData[[1]][, c("repCont2", "pers.hat", "totalEff.hat", "winning", "ideolDistance", "livesConst", "pastMP", "partyHier")])$repCont2 - mean(na.omit(indLevelData[[1]][, c("repCont2", "pers.hat", "totalEff.hat", "winning", "ideolDistance", "livesConst", "pastMP", "partyHier")])$repCont2))^2)

# Calculate R2
r2_m1 <- round(Reduce("+", lapply(m1_l, function(x) (1 - (sum(x$residuals^2) / TSS_cty)))) / length(m1_l), 3)
r2_m2 <- round(Reduce("+", lapply(m2_l, function(x) (1 - (sum(x$residuals^2) / TSS_cty)))) / length(m2_l), 3)
r2_m3 <- round(Reduce("+", lapply(m3_l, function(x) (1 - (sum(x$residuals^2) / TSS_ind)))) / length(m3_l), 3)
r2_m4 <- round(Reduce("+", lapply(m4_l, function(x) (1 - (sum(x$residuals^2) / TSS_ind)))) / length(m4_l), 3)
r2_m5 <- round(Reduce("+", lapply(m5_l, function(x) (1 - (sum(x$residuals^2) / TSS_ind2)))) / length(m5_l), 3)

# Calculate Adj. Pseudo R2
adjr2_m1 <- round(Reduce("+", lapply(m1_l, function(x) (1 - (((sum(x$residuals^2) / TSS_cty) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(m1_l), 3)
adjr2_m2 <- round(Reduce("+", lapply(m2_l, function(x) (1 - (((sum(x$residuals^2) / TSS_cty) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(m2_l), 3)
adjr2_m3 <- round(Reduce("+", lapply(m3_l, function(x) (1 - (((sum(x$residuals^2) / TSS_ind) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(m3_l), 3)
adjr2_m4 <- round(Reduce("+", lapply(m4_l, function(x) (1 - (((sum(x$residuals^2) / TSS_ind) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(m4_l), 3)
adjr2_m5 <- round(Reduce("+", lapply(m5_l, function(x) (1 - (((sum(x$residuals^2) / TSS_ind2) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(m5_l), 3)

# Add R2, adj.R2, and N
mytable <- rbind(mytable, "Fixed Effects by Year" = c("Yes", "Yes", "Yes", "Yes", "Yes"))
mytable <- rbind(mytable, "R2" = c(r2_m1, r2_m2, r2_m3, r2_m4, r2_m5))
mytable <- rbind(mytable, "Adj R2" = c(adjr2_m1, adjr2_m2, adjr2_m3, adjr2_m4, adjr2_m5))
mytable <- rbind(mytable, "Observations" = c(
  m1_l[[1]]$nobs,
  m2_l[[1]]$nobs,
  m3_l[[1]]$nobs,
  m4_l[[1]]$nobs,
  m5_l[[1]]$nobs
))

# Rename rows
row.names(mytable) <- c(
  "AP",
  "TDE",
  "Likelihood of Success",
  "Ideological Distance",
  "Constituency",
  "Past MP",
  "Party Hierarchy",
  "Fixed Effects by Election",
  "R2",
  "Adj. R2",
  "Observations"
)

# Replace NA values with empty strings
mytable[is.na(mytable)] <- ""

# Create table
mytable %>%
  kbl(
    caption = ":  Association between ap and Campaign Focus",
    col.names = c("(1)", "(2)", "(3)", "(4)", "(5)"),
    format = "html",
    linesep = "",
  )
: Association between ap and Campaign Focus
(2) (3) (4) (5)
AP 10.769*** (2.931) 9.962*** (3.143) 8.316*** (1.617) 8.515*** (1.66) 7.548*** (1.384)
TDE 0.69 (0.513) 0.126** (0.056) 0.531*** (0.077)
Likelihood of Success 0.599*** (0.036)
Ideological Distance 0.242*** (0.039)
Constituency 0.42*** (0.121)
Past MP -0.252* (0.129)
Party Hierarchy 0.03 (0.083)
Fixed Effects by Election Yes Yes Yes Yes Yes
R2 0.6 0.653 0.099 0.1 0.165
Adj. R2 0.443 0.498 0.098 0.099 0.163
Observations 40 40 11744 11744 4999

Model interpretation

This code provides an interpretation of the substantive size of the effect of Average Personalism (AP) on campaign focus by calculating the marginal effect of AP in standard deviation units. It uses the coefficient estimates from Model 2 (country-level) and Model 5 (individual-level with controls) from Table 10.1 to estimate how much a one-standard-deviation increase in AP affects a candidate’s tendency to run a personal campaign. The code also retrieves countries (or candidates) at the minimum and maximum AP scores to illustrate the substantive contrast implied by the models.

Turning a coefficient into a sentence. A regression coefficient is expressed in the units of the variables, which rarely means anything to a reader. The standard remedy, used here, is a two-step conversion:

  1. Multiply the coefficient by the standard deviation of AP. This answers “how much does campaign focus change for a typical-sized difference in personalism?” — rather than for a one-unit change, which may be far larger than anything observed.
  2. Divide that by the standard deviation of the outcome. The result is an effect size in standard-deviation units, comparable across models and studies.

The final line then sorts countries by AP so the text can name two real cases sitting at the ends of the range, making the abstract quantity concrete.

# Model 2
# Standard Deviation of AP * estimated effect
# out2[1, 2] is the pooled estimate for AP: row 1 (the first predictor),
# column 2 (the estimate column of the summary table).
sd(avg_TDE_AP_cty$pers.hat) * out2[1, 2]
## [1] 0.614
# (Standard Deviation of AP * estimated effect) divided by Standard Deviation of Attention to Candidate
# Expressed in standard deviations of the outcome -- the effect size.
(sd(avg_TDE_AP_cty$pers.hat) * out2[1, 2]) / sd(avg_TDE_AP_cty$repCont2)
## [1] 0.51
# Find countries to compare
# Sort country-years from lowest to highest AP. order() returns row numbers in
# sorted sequence; using them as a row index rearranges the table. Printed so
# specific cases can be quoted in the chapter text; nothing is stored.
avg_TDE_AP_cty[order(avg_TDE_AP_cty$pers.hat), c("pers.hat", "country", "year")]
##    pers.hat        country year
## 22    0.357        Hungary 2014
## 28    0.360     Montenegro 2012
## 30    0.361    Netherlands 2006
## 29    0.370     Montenegro 2016
## 26    0.381          Italy 2013
## 17    0.392        Germany 2013
## 36    0.403        Romania 2016
## 1     0.403        Albania 2013
## 33    0.416       Portugal 2009
## 34    0.416       Portugal 2011
## 35    0.416       Portugal 2015
## 10    0.459 Czech Republic 2006
## 11    0.462        Denmark 2011
## 5     0.465        Belgium 2007
## 6     0.465        Belgium 2010
## 7     0.465        Belgium 2014
## 23    0.467        Iceland 2009
## 24    0.467        Iceland 2013
## 37    0.469         Sweden 2010
## 38    0.469         Sweden 2014
## 31    0.472         Norway 2009
## 32    0.472         Norway 2013
## 8     0.473         Canada 2015
## 42    0.473 United Kingdom 2010
## 2     0.480      Australia 2007
## 3     0.480      Australia 2013
## 4     0.489        Austria 2008
## 39    0.497    Switzerland 2007
## 40    0.497    Switzerland 2011
## 41    0.497    Switzerland 2015
## 12    0.540        Estonia 2011
## 13    0.540        Estonia 2015
## 16    0.543        Finland 2015
## 14    0.545        Finland 2007
## 15    0.545        Finland 2011
## 27    0.548          Malta 2013
## 9     0.549          Chile 2017
## 18    0.553         Greece 2007
## 19    0.553         Greece 2009
## 20    0.553         Greece 2012
## 21    0.553         Greece 2015
## 25    0.561        Ireland 2007

This code provides an interpretation of the substantive size of the effect of Average Personalism (AP) on campaign focus by calculating the marginal effect of AP in standard deviation units. It uses the coefficient estimates from Model 2 (country-level) and Model 5 (individual-level with controls) from Table 10.1 to estimate how much a one-standard-deviation increase in AP affects a candidate’s tendency to run a personal campaign. The code also retrieves countries (or candidates) at the minimum and maximum AP scores to illustrate the substantive contrast implied by the models.

# Model 5
# get countries used in the model
inSample <- na.omit(indLevelData[[1]][, c("repCont2", "pers.hat", "totalEff.hat", "winning", "ideolDistance", "livesConst", "pastMP", "partyHier", "ctyyear", "id")])$id
# Standard Deviation of AP * estimated effect
sd(avg_TDE_AP_ind[avg_TDE_AP_ind$id %in% inSample, ]$pers.hat) * out5[1, 2]
## [1] 0.35
# (Standard Deviation of AP * estimated effect) divided by Standard Deviation of Attention to Candidate
(sd(avg_TDE_AP_ind[avg_TDE_AP_ind$id %in% inSample, ]$pers.hat) * out5[1, 2]) / sd(avg_TDE_AP_ind[avg_TDE_AP_ind$id %in% inSample, ]$repCont2)
## [1] 0.12
# Find max AP and min AP
subset(avg_TDE_AP_ind, avg_TDE_AP_ind$id %in% inSample)[which.max(avg_TDE_AP_ind$pers.hat[avg_TDE_AP_ind$id %in% inSample]), ]
##          ctyyear   id totalEff.hat pers.hat repCont2
## 4804 Greece-2007 9759         1.31    0.556        3
subset(avg_TDE_AP_ind, avg_TDE_AP_ind$id %in% inSample)[which.min(avg_TDE_AP_ind$pers.hat[avg_TDE_AP_ind$id %in% inSample]), ]
##              ctyyear    id totalEff.hat pers.hat repCont2
## 6197 Montenegro-2012 13031        0.343     0.37        2

The individual-level model with controls (Model 5) finds a smaller but still meaningful effect: a one-standard-deviation increase in AP leads to a 0.35-point increase in candidate-centered campaigning, equivalent to 12% of a standard deviation of the outcome variable. This aligns with Figure 10.2b. The most extreme values of AP among individuals in the sample range from 0.370 (Montenegro 2012) to 0.556 (Greece 2007), representing the realistic bounds of variation across electoral contexts.

Together, these findings reinforce the book’s core argument: electoral systems that strengthen personal vote incentives also shape how candidates frame their campaigns, and this effect holds even after accounting for interparty competition and individual-level candidate characteristics

Country-Level Expected Values (Model 2)

This code simulates expected values of Attention to Candidate across the observed range of Average Personalism (AP) using Model 2 (country-level, with TDE as a control). It combines predictions across five imputations, using a bootstrapped function to estimate predicted values and their associated uncertainty. The output is used to generate Figure 10.2a in the book.

# Rearrange data to run predict() and calculate SEs

# Extract relevant variables for Model 2 from the first imputation
tempData <- ctyLevelData[[1]][, c("repCont2", "totalEff.hat", "pers.hat", "year")]
# Rename for consistency
names(tempData)[c(2, 3)] <- c("totalEff.hat1", "pers.hat1")
# Append totalEff and pers.hat from remaining imputations (2–5)
tempData <- cbind(
  tempData, ctyLevelData[[2]][, c("totalEff.hat", "pers.hat")],
  ctyLevelData[[3]][, c("totalEff.hat", "pers.hat")],
  ctyLevelData[[4]][, c("totalEff.hat", "pers.hat")],
  ctyLevelData[[5]][, c("totalEff.hat", "pers.hat")]
)
# Rename the newly added columns
names(tempData)[5:12] <- c(
  "totalEff.hat2", "pers.hat2", "totalEff.hat3", "pers.hat3",
  "totalEff.hat4", "pers.hat4", "totalEff.hat5", "pers.hat5"
)
# Reorder variables
tempData <- tempData[, c(
  "repCont2", "year",
  "pers.hat1", "pers.hat2", "pers.hat3", "pers.hat4", "pers.hat5",
  "totalEff.hat1", "totalEff.hat2", "totalEff.hat3", "totalEff.hat4", "totalEff.hat5"
)]

# Expected value at the country level
# Generate 15 equally spaced AP values over the observed range
apValues <- seq(min(avg_TDE_AP_cty$pers.hat), max(avg_TDE_AP_cty$pers.hat), length.out = 15)

# Initialize list to store simulated predictions and standard errors
outCountry <- list()
for (i in 1:length(apValues)) {
  # Run bootstrapped prediction at each AP level
  outCountry[[i]] <- boot.predict.se.cty(myvalue = apValues[i], reps = 500, seed = 123354)
}


# Combine results into a single data frame
outCountry <- do.call("rbind", outCountry)

Individual-Level Expected Values (Model 5)

This chunk mirrors the logic above but for Model 5, which is estimated at the individual level and includes additional covariates. It simulates expected values for Attention to Candidate across the AP range, controlling for TDE and candidate-level variables. These simulations are used to produce Figure 10.2b.

# Extract relevant variables from the first imputation
# Rearange data to run predict() and calculate SEs
tempData_ind <- indLevelData[[1]][, c("repCont2", "totalEff.hat", "pers.hat", "year", "winning", "ideolDistance", "livesConst", "pastMP", "partyHier")]
# Rename to prepare for merging
names(tempData_ind)[c(2, 3)] <- c("totalEff.hat1", "pers.hat1")
# Append predictions from imputations 2–5
tempData_ind <- cbind(
  tempData_ind, indLevelData[[2]][, c("totalEff.hat", "pers.hat")],
  indLevelData[[3]][, c("totalEff.hat", "pers.hat")],
  indLevelData[[4]][, c("totalEff.hat", "pers.hat")],
  indLevelData[[5]][, c("totalEff.hat", "pers.hat")]
)
# Rename new columns
names(tempData_ind)[10:17] <- c(
  "totalEff.hat2", "pers.hat2", "totalEff.hat3", "pers.hat3",
  "totalEff.hat4", "pers.hat4", "totalEff.hat5", "pers.hat5"
)
# Reorder and remove rows with missing data
tempData_ind <- na.omit(tempData_ind[, c(
  "repCont2", "year",
  "pers.hat1", "pers.hat2", "pers.hat3", "pers.hat4", "pers.hat5",
  "totalEff.hat1", "totalEff.hat2", "totalEff.hat3", "totalEff.hat4", "totalEff.hat5",
  "winning", "ideolDistance", "livesConst", "pastMP", "partyHier"
)])

# Define 15 AP values to simulate
# Expected value at the individual level
apValuesInd <- seq(min(avg_TDE_AP_ind$pers.hat), max(avg_TDE_AP_ind$pers.hat), length.out = 15)

# Initialize list to store bootstrapped expected values
outInd <- list()
for (i in 1:length(apValuesInd)) {
  outInd[[i]] <- boot.predict.se.ind(myvalue = apValuesInd[i], reps = 500, seed = 123354)
}

# Combine results
outInd <- do.call("rbind", outInd)

How to read Figure 10.2. Two panels showing the same relationship at the two levels: expected campaign focus across the range of AP, with a confidence band from the bootstrap.

Panel (a) is built from the country-level model, panel (b) from the individual-level one. Judge each by whether the band at the low-AP end clears the band at the high-AP end — that, rather than the visual steepness of the line, is what distinguishes the relationship from noise.

The two panels together are the stronger claim. Agreement across levels means the result is not an artefact of aggregating candidates into country averages, nor of treating individuals as independent when they share a system.

Figure 10.2: Expected Attention to Candidate

Figure 10.2 panel (a): Expected Attention to Candidate at the Country Level

Figure 10.2a presents the predicted effect of Average Personalism (AP) on candidates’ campaign focus, based on Model 2 from Table 10.1. This is a country-level model where self-reported attention to candidate is regressed on AP and TDE, with year fixed effects. The plot uses bootstrapped simulations to display the expected values of repCont2 across a realistic range of AP scores, along with 95% confidence intervals. This visualization allows readers to grasp the substantive impact of AP on campaign strategies.

# pdf('~/Downloads/Plots/ap_cand_cty.pdf')

layout(matrix(c(1), ncol = 1))

# Set plot margins
par(mar = c(6, 6, 2, 2))

# Country Level

# Create empty plot canvas
plot(
  x = apValues,
  y = outCountry[, 1], # expected values of repCont2
  type = "n", # don't draw anything yet
  ylim = c(1, 6), # y-axis range (based on simulation output)
  xlim = c(0.36, 0.56), # x-axis range (min–max AP)
  axes = FALSE, # manually add axes for styling
  xlab = "", ylab = "",
  cex.lab = 2
)
# Add axis labels
mtext("Expected Attention to Candidate", side = 2, line = 3, cex = 2)
mtext("Average Personalism", side = 1, line = 4, cex = 2)
# Draw custom axes
axis(1, cex.axis = 2, at = c(0.36, 0.41, .46, .51, .56))
axis(2, cex.axis = 2, las = 2)
# Add shaded confidence interval using polygon (upper and lower bounds)
polygon(
  x = c(apValues, rev(apValues)),
  y = c(outCountry[, "ub95"], rev(outCountry[, "lb95"])), col = adjustcolor("grey", alpha.f = 0.95), border = NA
)
# Add expected value line on top of shaded region
lines(x = apValues, y = outCountry[, 1], lty = 1, col = "white", lwd = 1)

# dev.off()

Figure 10.2a demonstrates that as Average Personalism increases—from 0.36 (Hungary) to 0.56 (Ireland)—the expected level of candidate-centered campaigning rises by over 2 points on the 0–10 scale of repCont2 (campaign focus on the candidate). This is a substantial effect, reinforcing the claim that electoral systems with strong personal vote-seeking incentives lead candidates to focus more on cultivating their own reputation than promoting their party’s brand.

Figure 10.2 panel (b): Individual-Level Simulation of Expected Values

# pdf('~/Downloads/Plots/ap_cand_ind.pdf')

layout(matrix(c(1), ncol = 1))

# Set plot margins
par(mar = c(6, 6, 2, 2))

# Individual Level
# Create empty plotting window
plot(
  x = apValuesInd,
  y = outInd[, 1], # Predicted mean values
  type = "n", # Do not draw points yet
  ylim = c(1, 6), # Y-axis limits
  xlim = c(0.36, 0.56), # X-axis limits reflect observed AP range
  axes = FALSE,
  xlab = "", ylab = "",
  cex.lab = 2
)

# Axis and label customization
mtext("Expected Attention to Candidate", side = 2, line = 4, cex = 2)
mtext("Average Personalism", side = 1, line = 4, cex = 2)
axis(1, cex.axis = 2, at = c(0.36, 0.41, .46, .51, .56))
axis(2, cex.axis = 2, las = 2)
# Add 95% confidence ribbon
polygon(
  x = c(apValuesInd, rev(apValuesInd)),
  y = c(outInd[, "ub95"], rev(outInd[, "lb95"])), col = adjustcolor("grey", alpha.f = 0.95), border = NA
)
# Overlay predicted line
lines(x = apValuesInd, y = outInd[, 1], lty = 1, col = "white", lwd = 1)

# dev.off()

Figure 10.2b confirms the effect observed at the country level in Figure 10.2a, but within a model that accounts for individual-level controls. As Average Personalism (AP) increases from about 0.36 to 0.56, the expected value of repCont2 (campaign focus on the candidate) increases from approximately 3.1 to 4.5 on a 0–10 scale. While the effect is more modest than in the country-level plot, it remains statistically significant and substantively meaningful—even when controlling for variables such as candidate incumbency, expectations of winning, and party hierarchy. The confidence band is narrow, suggesting a high degree of certainty in the predicted relationship.

Figure 10.3: Expected Attention to Candidate, AP, and District Magnitude.

The plot compares open-list PR (OLPR) and closed-list PR (CLPR) systems while holding other electoral design components constant. As district magnitude increases, the plot shows how both the institutional incentive (AP) and the predicted campaign behavior evolve. The results, based on simulated systems and predictions from Model 5, demonstrate that increasing district magnitude reduces both personal vote incentives and campaign personalization.

A two-stage prediction. This figure chains the book’s machinery together rather than reading anything off the data directly. First it invents electoral systems — open and closed list, at a range of district magnitudes, everything else held fixed — and feeds them to the GBM models to obtain predicted AP. Then it feeds those predicted AP values into Model 5 to obtain predicted campaign focus.

Because both stages are model output, the figure shows what the theory implies for systems that may never have existed, which is precisely its purpose: it isolates the effect of magnitude and ballot type from everything else that varies across real countries.

# Simulate electoral systems under CLPR and OLPR rules for six values of M
xAPclosed <- data.frame(
  formula = "hare",
  ballot_type = c("closed"),
  new.nvotes = "One",
  pool_level = "party",
  threshold = 0,
  M = c(2, 10, 15, 25, 50, 100)
)

xAPopen <- data.frame(
  formula = "hare",
  ballot_type = c("open"),
  new.nvotes = "One",
  pool_level = "party",
  threshold = 0,
  M = c(2, 10, 15, 25, 50, 100)
)

# Set variables as factors
xAPclosed$formula <- as.factor(xAPclosed$formula)
xAPclosed$ballot_type <- as.factor(xAPclosed$ballot_type)
xAPclosed$new.nvotes <- as.factor(xAPclosed$new.nvotes)
xAPclosed$pool_level <- as.factor(xAPclosed$pool_level)
xAPopen$formula <- as.factor(xAPopen$formula)
xAPopen$ballot_type <- as.factor(xAPopen$ballot_type)
xAPopen$new.nvotes <- as.factor(xAPopen$new.nvotes)
xAPopen$pool_level <- as.factor(xAPopen$pool_level)

# Combine both systems into one dataframe
xAP <- rbind(xAPclosed, xAPopen)

# Predict Average Personalism (AP) scores for each system using GBM models
ap_hat1 <- predict(optimalGBMIntra[[1]], xAP, n.trees = optimalGBMIntra[[1]]$n.trees)
ap_hat2 <- predict(optimalGBMIntra[[2]], xAP, n.trees = optimalGBMIntra[[2]]$n.trees)
ap_hat3 <- predict(optimalGBMIntra[[3]], xAP, n.trees = optimalGBMIntra[[3]]$n.trees)
ap_hat4 <- predict(optimalGBMIntra[[4]], xAP, n.trees = optimalGBMIntra[[4]]$n.trees)
ap_hat5 <- predict(optimalGBMIntra[[5]], xAP, n.trees = optimalGBMIntra[[5]]$n.trees)

# Average predicted AP values across imputations
xAP$ap <- colMeans(rbind(ap_hat1, ap_hat2, ap_hat3, ap_hat4, ap_hat5))

# Predict expected campaign personalization for each AP value using Model 5
outOpen <- list()
outClosed <- list()
for (i in 1:6) {
  outOpen[[i]] <- boot.predict.se.ind(myvalue = xAP$ap[xAP$ballot_type == "open"][i], reps = 500, seed = 123354)
  outClosed[[i]] <- boot.predict.se.ind(myvalue = xAP$ap[xAP$ballot_type == "closed"][i], reps = 500, seed = 123354)
}

# Unlist results into data frames
outOpen <- do.call("rbind", outOpen)
outClosed <- do.call("rbind", outClosed)


# Create the plot

# pdf('~/Downloads/Plots/ap_m_cand.pdf', width = 10)
par(mar = c(6, 6, 2, 6))

# Plot AP curves (left y-axis)
plot(
  x = c(2, 10, 15, 25, 50, 100),
  y = xAP$ap[xAP$ballot_type == "open"], type = "l",
  xlim = c(2, 100), ylim = c(0.35, .65), axes = FALSE,
  ylab = "", xlab = "", cex.lab = 2, lwd = 3
)
lines(x = c(2, 10, 15, 25, 50, 100), y = xAP$ap[xAP$ballot_type == "closed"], lty = 2, lwd = 3)
mtext("Average Personalism", side = 2, line = 4, cex = 2)
axis(1, cex.axis = 2)
axis(2, cex.axis = 1.5, las = 2)

# Add predicted campaign focus (repCont2) curves on right y-axis
par(new = TRUE)
plot(
  x = c(2, 10, 15, 25, 50, 100),
  y = outOpen[, 1], type = "l", axes = FALSE, ylim = c(3, 5),
  xlab = "", ylab = "", cex.lab = 2, col = "grey", lwd = 3
)
lines(x = c(2, 10, 15, 25, 50, 100), y = outClosed[, 1], lty = 2, lwd = 3, col = "grey")
mtext("District Magnitude", side = 1, line = 4, cex = 2)
axis(4, cex.axis = 1.5, las = 2)

# Add right-side label manually
corners <- par("usr") # Gets the four corners of plot area (x1, x2, y1, y2)
par(xpd = TRUE) # Draw outside plot area
text(x = corners[2] + 12, y = mean(corners[3:4]), "Attention to Candidate", srt = 270, cex = 2)

# Add legend
legend("topright",
  legend = c("OLPR: AP", "OLPR: Attention", "CLPR: AP", "CLPR: Attention"),
  col = c("black", "grey", "black", "grey"),
  lty = c(1, 1, 2, 2),
  lwd = c(3, 3, 3, 3),
  bty = "n",
  cex = 2
)

# dev.off()

Figure 10.3 shows that as district magnitude (M) increases, Average Personalism (AP) decreases under both closed- and open-list PR systems—indicating that larger districts weaken intraparty incentives for personalistic behavior. This pattern is consistent across both ballot types, though OLPR systems consistently score higher on AP, given that voters can choose candidates directly. Importantly, the decline in predicted attention to the candidate closely follows the decline in AP, illustrating the behavioral consequence of institutional incentives. Under both OLPR and CLPR, the expected personalization of campaigns decreases as M increases—supporting the book’s theoretical argument that district magnitude disciplines personalization, even in systems where ballots allow for candidate choice.

How to read Figure 10.3. Two lines, open list and closed list, across district magnitude.

The vertical gap between them is the effect of ballot type; how that gap changes from left to right is whether magnitude conditions it. The chapter’s reading is that both lines decline — larger districts dilute personal vote-seeking even under open lists — which qualifies the common expectation that magnitude and open ballots reinforce each other without limit.

Moving Forward

Chapter 10 has demonstrated how intraparty incentives, captured by Average Personalism (AP), shape campaign strategies across diverse electoral systems. Using both existing literature and data from the Comparative Candidate Survey (CCS), the chapter confirmed that candidates facing stronger personal vote-seeking incentives tend to focus more on their individual reputations during campaigns, rather than emphasizing party collective identities. This finding highlights the powerful influence of electoral rules—such as district magnitude, ballot type, and vote pooling—on campaign behavior and candidate-voter interactions.

By linking AP scores to observable campaign focus, this chapter bridges the gap between institutional incentives and real-world political behavior, complementing earlier analyses of party system size, ideological distribution, and congruence. However, campaigns are just one stage in the political process, and understanding how electoral incentives affect elected representatives’ behavior once in office is equally critical.

The next chapter shifts focus from campaigns to governance, exploring how the incentives shaped by electoral systems influence legislative behavior and coalition dynamics. By moving from pre-election competition to post-election collaboration, Chapter 11 completes the picture of how electoral institutions structure political incentives throughout the electoral cycle.

Session Information

Everything above was produced by a single run of this file. The two tables below record the environment that run took place in: the version of R, the machine and operating system, and the version of every package this chapter loads. They are generated while the page is being built, so they always describe the page you are reading rather than some earlier run.

This matters more than it may appear. R packages change: default arguments get revised, estimation routines are rewritten, and a number computed under one version of a modelling package is not guaranteed to reappear under another. Recording the versions is what allows a reader who gets a different answer to tell that apart from using a different version.

The R environment that produced this page
Setting Value
R version R version 4.5.1 (2025-06-13)
Platform aarch64-apple-darwin20
Operating system macOS Tahoe 26.5.2
Collation en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
Time zone America/Chicago
Pandoc 3.10.1
Page built on 30 July 2026
Packages this chapter attaches, with the version used here
Package Version
dplyr 1.1.4
fixest 0.14.1
forcats 1.0.0
gbm 2.2.2
ggplot2 3.5.2
kableExtra 1.4.1
lubridate 1.9.4
mice 3.19.0
miscTools 0.6-30
purrr 1.2.2
readr 2.1.5
sjPlot 2.9.0
stringr 1.5.1
tibble 3.3.0
tidyr 1.3.1
tidyverse 2.0.0

The environment used for the published book

The record for this chapter survives in the originally rendered page: R 4.3.1 (2023-06-16) on macOS 15.4.1 (aarch64, darwin20), knitted on 18 May 2025 with pandoc 3.1.1.

Where a record does survive, it is reproduced in full in RECOVERED-SessionInformation.md, alongside these files.