How to read this file

This is a replication file. It contains every line of R code needed to reproduce the figures of Chapter 5, 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 5 evaluates how each component rule of an electoral system relates to the book’s two quantities of interest. Chapters 2 and 3 speculated that the Total Duvergerian Effect (TDE) — the indicator of interparty incentives — would be mainly a function of district magnitude, seat allocation formula, and electoral threshold, and that Average Personalism (AP) — the indicator of intraparty incentives — would be mainly a function of ballot type, number and level of votes, vote pooling, and district magnitude. Chapter 4 presented the simulation framework that lets us observe those incentives in a controlled setting, free of the confounding factors that plague observational data. Here we tease out the influence of the individual rules that make up each system.

If you have never used R. Click the grey Code button above any block to show or hide the code. Inside a block, every line beginning with # is a comment: R ignores it, so comments exist purely for you. A few conventions:

  • <- is R’s assignment arrow. x <- 5 stores the number 5 under the name x. Inside a function call, = names an argument instead.
  • library(name) loads an add-on package — a collection of extra functions.
  • %>% is the “pipe”. a %>% f() is another way of writing f(a), which lets a sequence of operations read top to bottom instead of inside out.
  • function(...) defines a reusable recipe. Nothing happens when a function is defined; it runs only when it is later called by name.
  • df$col pulls the column col out of the data frame df.

Setup

The first block configures the document. It is run but not shown (include = FALSE), because it sets up the report rather than performing analysis.

This chapter needs only one package. If R reports there is no package called ..., install it once with install.packages("tidyverse").

Why dplyr:: is spelled out below. Two of the functions this chapter relies on have namesakes elsewhere: select() also exists in MASS, and filter() in base R’s stats. Whichever package was attached most recently wins, so if either of those is already loaded when the chapter runs, a bare select() calls the wrong function and fails with a puzzling message about an unused argument.

library(tidyverse) does not protect against this. Loading a package that is already attached does nothing at all — it will not move dplyr back to the front of the search path. Writing dplyr::select() names the function unambiguously and works regardless of what else is loaded.

# Load relevant libraries

# The tidyverse is a bundle of packages that share a common design. This
# chapter draws on three of them: dplyr for reshaping data (mutate, filter,
# select), ggplot2 for every figure, and tidyr for handling missing values.
library(tidyverse)

Introduction

In the previous chapters, we introduced a simulation framework that allowed us to explore how different electoral systems create distinct incentives for political actors. These simulations helped us position electoral systems within a two-dimensional space defined by interparty incentives—how electoral rules shape competition among parties—and intraparty incentives—how they shape competition within parties.

Chapter 5 dives deeper by examining how each of the core component rules that make up electoral systems influence these two dimensions. We focus on two key quantities from our simulations: the Total Duvergerian Effect (TDE), which captures how electoral systems constrain or expand party competition, and Average Personalism (AP), which measures the degree to which electoral systems encourage politicians to cultivate personal reputations.

While electoral systems are complex combinations of rules, understanding the individual impact of district magnitude, seat allocation formulas, electoral thresholds, ballot types, vote pooling, and voting levels is essential. This chapter explores the marginal effects of these components on TDE and AP, highlighting which rules matter most and how their interactions shape political incentives.

Though our simulation algorithms are intricate and the relationships between rules and incentives are not always straightforward, the evidence presented here shows clear patterns consistent with established theory. This chapter sets the stage for the next step: applying these insights to real-world electoral systems, which we undertake in Chapter 6.

This file contains calls to datasets used in Chapter 5 (Rules and Incentives), as well as the code necessary to produce all graphs in the chapter.

Data processing

The chapter draws on four saved datasets produced by the Chapter 4 simulations: AP and TDE scores, each summarised at two levels of aggregation.

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/ch05/... 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.

# so the short file names below resolve correctly. This is the one line you
# must edit to run the chapter on your own machine.


# readRDS() reads a single saved R object back into memory. Each of these four
# files holds a LIST of imputed datasets -- not one table but several, which is
# why the plotting functions below all begin by stacking a list together.
#   AP_dist   Average Personalism, district level
#   TDE_dist  Total Duvergerian Effect, district level
#   AP_cty    Average Personalism, country level
#   TDE_cty   Total Duvergerian Effect, country level
# "dist" and "cty" matter: a country-level score averages over the districts
# within a system, so the two levels answer slightly different questions. Most
# figures in this chapter use the district-level data.
AP_dist <- readRDS("data/ch05/rds/district_AP_imputed.rds")
TDE_dist <- readRDS("data/ch05/rds/district_TDE_imputed.rds")
AP_cty <- readRDS("data/ch05/rds/country_AP_imputed.rds")
TDE_cty <- readRDS("data/ch05/rds/country_TDE_imputed.rds")

Every figure in this chapter is a variation on the same idea: take a component rule, put it on the horizontal axis, put TDE or AP on the vertical axis, and split the picture by electoral system family. Rather than repeat that recipe eight times, the next block defines four functions that do the work, and each figure below is then a single line calling one of them.

Four functions, one job each.

  • data_stack() — the workhorse. Takes a list of imputed datasets, tidies the category labels, keeps only the variables asked for, and glues everything into one long table.
  • plot_district() — plots one rule against one outcome, district level only. Used by Figures 5.1 through 5.6.
  • plot_interact() — plots two rules at once, so the effect of one can be seen to depend on the other. Used by Figure 5.7.
  • plot_one() — like plot_district() but overlays country and district levels. Defined here but not called by any figure in this chapter.

Nothing runs when this block is knitted. Defining a function is like writing down a recipe; the cooking happens later, in the figure blocks.

# FUNCTIONS

# Stack and Recode Electoral Data

# This function processes a list of datasets, applies factor recoding, selects relevant variables, and classifies electoral systems into families.

# List of parameters:
# data: a list of data frames containing electoral information.
# x: a character vector specifying the independent variable(s) to retain.
# y: a character vector specifying the dependent variable(s) to retain.
# level: a string specifying the level of aggregation

# The function returns a single stacked data frame with recoded categorical variables and a new variable classifying electoral systems into families.


data_stack <- function(data, x, y, level) {
  # ------------------------------------------------------------------------
  # The pattern here is worth learning, because it recurs throughout the book:
  #
  #   lapply(data, FUN = ...)   applies the same function to every element of
  #                             a list, returning a list of results.
  #   do.call(rbind, <list>)    then stacks those results into one data frame,
  #                             as if you had typed rbind(res1, res2, res3, ...).
  #
  # Together they mean "clean each imputed dataset the same way, then pile them
  # up". Reading the code inside out -- innermost parentheses first -- is the
  # only way to make sense of it.
  #
  # \(dta) { ... } is shorthand for function(dta) { ... }, available from
  # R 4.1 onward. It defines a small unnamed function on the spot; `dta` is
  # whichever dataset lapply() is currently handing it.
  # ------------------------------------------------------------------------
  do.call(
    rbind, # Combines the results of lapply into a single data frame
    lapply(data, # Applies function to each element of the list
      FUN = \(dta) {
        # If 'y' is "pers" and the level is "Country", use "pers_avg"
        # The country-level files store personalism already averaged
        # across districts, under a different column name. This
        # swaps in the right name so the caller does not have to.
        if (y == "pers" & level == "Country") {
          y <- "pers_avg"
        }

        # What follows is one long pipeline. Each %>% passes the
        # result of the previous step into the next, so the data are
        # transformed in sequence: recode labels, fill in missing
        # values, keep the needed columns, classify into families.
        dta %>%
          # Recode electoral function names for better readability
          # recode_factor(old = "New", ...) renames category labels.
          # Note that two inputs, stv and droop, both map to "Droop":
          # recoding can deliberately merge categories, not just
          # rename them.
          mutate(elec_fun_name = recode_factor(elec_fun_name,
            hare = "Hare",
            droop = "Droop",
            stv = "Droop",
            hagenbachbischoff = "Hagenbach-Bischoff",
            saintelague = "Sainte-Lague",
            modsaintlague = "Mod. Sainte-Lague",
            imperiali = "Imperiali",
            dhondt = "D'Hondt",
            fortified_pr = "Fortified PR",
            a_v = "Abs. Majority",
            lim_nom = "Plurality",
            plurality = "Plurality"
          )) %>%
          # Recode 'new.nvotes' variable to more interpretable labels
          mutate(new.nvotes = recode_factor(new.nvotes,
            One = "One",
            LessSeats = "Total Seats - 1",
            TotalSeats = "Total Seats",
            TotalCandidates = "Total Candidates"
          )) %>%
          # Replace NA values in 'new.nvotes' with "Total Seats - 1"
          # NA is R's marker for a missing value. replace_na()
          # substitutes a chosen value wherever one appears. Here the
          # substitutions encode a modelling decision: a system that
          # does not record this rule is treated as the stated
          # default, rather than being dropped from the figure.
          mutate(new.nvotes = replace_na(
            new.nvotes,
            "Total Seats - 1"
          )) %>%
          # Recode 'ballot_type' to reflect different voting systems
          mutate(ballot_type = recode_factor(ballot_type,
            open = "Open",
            flexible = "Flexible",
            closed = "Closed"
          )) %>%
          # Replace NA values in 'ballot_type' with "Open"
          mutate(ballot_type = replace_na(
            ballot_type,
            "Open"
          )) %>%
          # Recode 'pool_level' for different levels of vote aggregation
          mutate(pool_level = recode_factor(pool_level,
            candidate = "Candidate",
            party_list = "Sub-party List",
            party = "Party List"
          )) %>%
          # Replace NA values in 'pool_level' with "Sub-party List"
          mutate(pool_level = replace_na(
            pool_level,
            "Sub-party List"
          )) %>%
          # Recode electoral thresholds
          # The backticks around `0.01` are needed because a name
          # cannot begin with a digit in R; backticks let you use one
          # anyway. Turning the numbers into ordered labels means the
          # axis reads "0, 1%, 3%..." rather than as a continuous
          # scale.
          mutate(threshold = recode_factor(threshold,
            `0` = "0",
            `0.01` = "1%",
            `0.03` = "3%",
            `0.05` = "5%",
            `0.1` = "10%"
          )) %>%
          # Select only the relevant variables specified by the user
          # x and y are whatever the caller asked for, so this line
          # is what makes one function serve every figure.
          # all_of() insists that the named columns exist, turning a
          # silent mismatch into an immediate, informative error.
          dplyr::select(all_of(c(x, y, "system_name"))) %>%
          # Classify electoral systems into broad categories
          # case_when() is a multi-way if/else. Each line reads
          # "condition ~ value", checked from top to bottom; the
          # first match wins. %in% asks whether a value appears
          # anywhere in the list that follows it.
          # .default catches everything not matched above, so any
          # system that is neither PR nor mixed becomes Majoritarian.
          mutate(
            family = case_when(
              system_name %in% c(
                "FortifiedPR",
                "CLPR",
                "FrLPR",
                "OLPR",
                "STV",
                "FLPR"
              ) ~ "PR", # PR Systems
              system_name %in% c("MMC", "MMI") ~ "Mixed", # Mixed systems
              .default = "Majoritarian"
            ), # Majoritarian Systems
            # Record which level of aggregation this table came
            # from, so the two can be told apart once stacked.
            level = level
          ) %>%
          # Drop 'system_name' as it's no longer needed after classification
          # A minus sign inside select() removes a column instead of
          # keeping it.
          dplyr::select(-system_name)
      }
    )
  )
}


# Generate Electoral System Comparison Plots
# This function creates a comparative visualization of electoral system features and their effects on a specified outcome.

# List of parameters:
# data_cty: a list of country-level datasets to be processed and stacked.
# data_dist: a list of district-level datasets to be processed and stacked.
# rule: a string specifying the independent variable
# outcome: a string specifying the dependent variable to be plotted
# xlab: a string for the x-axis label.
# ylab: a string for the y-axis label.
# print: Logical; if TRUE (default), the plot is printed.
# file: for saving the plot).
# type: a string indicating whether the independent variable is "ordered" (categorical) or "continuous".

# The function returns a ggplot object visualizing the relationship between the rule and outcome across electoral system families.


plot_one <- function(data_cty,
                     data_dist,
                     rule,
                     outcome,
                     xlab, ylab,
                     print = TRUE,
                     file,
                     type = c("ordered", "continuous")) {
  # Stack country-level and district-level datasets
  all_data_cty <- data_stack(data_cty, rule, outcome, "Country")
  all_data_dist <- data_stack(data_dist, rule, outcome, "District")

  # Ensure consistent column names across both datasets
  names(all_data_cty) <- names(all_data_dist)

  # Combine both datasets into a single data frame
  all_data <- rbind(all_data_cty, all_data_dist)


  # Apply data filtering conditions based on the selected rule
  if (rule == "M") {
    all_data <- all_data %>%
      dplyr::filter(!((family == "Majoritarian") & (M > 43))) # Exclude majoritarian systems with M > 43
  }
  if (rule == "new.nvotes") {
    all_data <- all_data %>%
      dplyr::filter(!(family == "Mixed")) # Exclude mixed systems for this rule
  }
  if (rule == "ballot_type") {
    all_data <- all_data %>%
      dplyr::filter(!(family == "Mixed")) # Exclude mixed systems for this rule
  }
  if (rule == "threshold") {
    all_data <- all_data %>%
      dplyr::filter(!(family == "Majoritarian")) # Exclude majoritarian systems for this rule
  }


  # Initialize ggplot object with rule on x-axis and outcome on y-axis
  # -----------------------------------------------------------------------
  # HOW ggplot2 WORKS
  # A ggplot is assembled by adding layers with +, much as base R graphics
  # are built up by successive commands. The pieces here:
  #   ggplot(aes(...))  declares the "aesthetic mapping": which column drives
  #                     the x position, which the y, which the colour.
  #   .data[[rule]]     is the pronoun trick that makes these functions
  #                     general. `rule` holds a column NAME as text, e.g.
  #                     "M"; .data[[rule]] looks up that column at run time.
  #                     Writing plain `rule` would plot the literal word.
  #   scale_color_grey  greyscale, since the book prints in black and white.
  #   facet_wrap(...)   splits the plot into one panel per electoral family.
  #                     scales = "free_x" lets each panel set its own x-axis,
  #                     which matters because majoritarian and PR systems do
  #                     not share the same range of magnitudes or formulas.
  #   labs / theme_bw   axis labels and a plain black-and-white theme.
  # Note that the object is only BUILT here, not drawn; it is stored in `plt`
  # and drawn further down by print().
  # -----------------------------------------------------------------------
  plt <- all_data %>%
    ggplot(aes(x = .data[[rule]], y = .data[[outcome]], color = level)) +
    scale_color_grey(start = 0.7, end = 0) + # Use grayscale colors
    facet_wrap(vars(family), scales = "free_x") + # Create facet grid by electoral family
    labs(x = xlab, y = ylab, color = "Level") + # Set axis labels and legend title
    theme_bw()

  # Add visualization type based on 'type' argument
  # The right display depends on the kind of rule being plotted: a trend line
  # for a numeric rule such as district magnitude, a box plot for a categorical
  # one such as ballot type. Adding a layer to an existing plot object is done
  # with plt <- plt + <layer>.
  if (type == "continuous") {
    plt <- plt +
      geom_smooth(se = TRUE) # Add a smoothed line with confidence intervals for continuous variables
  } else {
    plt <- plt +
      # scale_x_discrete(guide = guide_axis(n.dodge = 3)) +
      theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + # Rotate x-axis labels for readability
      geom_boxplot() # Use box plots for categorical independent variables
  }
  # A ggplot object must be printed to appear. Inside a function this has to be
  # explicit, because only the value of the LAST line is auto-printed.
  if (print) {
    print(plt) # Print the plot if 'print' argument is TRUE
  }
}


# Generate Interaction Plots for Electoral Systems

# This function creates a visualization to examine the interaction between two electoral system characteristics and their effects on a specified outcome.

# List of parameters:
# data_cty: a list of country-level datasets to be processed and stacked.
# data_dist: a list of district-level datasets to be processed and stacked.
# rule: a character vector of length 2 specifying the two independent variables for interaction.
# outcome: a string specifying the dependent variable to be plotted.
# xlab: a string for the x-axis label.
# ylab: a string for the y-axis label.
# llab: a string for the legend label.
# print: Logical; if TRUE (default), the plot is printed.
# file: (Unused in the current version, but could be implemented for saving the plot).
# type: a string indicating whether the independent variable is "ordered" (categorical) or "continuous".

# The function returns a ggplot object visualizing the interaction between two rules and the electoral outcome across different electoral system families.


plot_interact <- function(data_cty,
                          data_dist,
                          rule,
                          outcome,
                          xlab, ylab, llab,
                          print = TRUE,
                          file,
                          type = c("ordered", "continuous")) {
  # Stack country-level and district-level datasets
  all_data_cty <- data_stack(data_cty, rule, outcome, "Country")
  all_data_dist <- data_stack(data_dist, rule, outcome, "District")

  # Ensure column names match between both datasets
  names(all_data_cty) <- names(all_data_dist)

  # Combine both datasets into a single data frame
  all_data <- rbind(all_data_cty, all_data_dist)

  # Filter the data to include only relevant observations
  plt <- all_data %>%
    dplyr::filter(
      level == "District", # Consider only district-level data
      ballot_type %in% c("Closed", "Open"), # Include only these ballot types
      !((family == "Majoritarian") & (M > 43))
    ) %>% # Exclude majoritarian systems with M > 43
    ggplot(aes(x = .data[[rule[1]]], y = .data[[outcome]], color = .data[[rule[2]]])) +
    scale_color_grey(start = 0.7, end = 0) + # Use grayscale color
    facet_wrap(vars(family), scales = "free_x") + # Facet plot by electoral family
    labs(x = xlab, y = ylab, color = llab) + # Add axis labels and legend title
    theme_bw()

  # Add visualization type based on 'type' argument
  if (type == "continuous") {
    plt <- plt +
      geom_smooth(se = TRUE) # Add a smoothed trend line with confidence intervals when the variable is continuous
  } else {
    plt <- plt +
      # scale_x_discrete(guide = guide_axis(n.dodge = 3)) +
      theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + # Rotate x-axis labels for readability
      geom_boxplot() # Use box plots for categorical  variables
  }

  # Print the plot if 'print' argument is TRUE
  if (print) {
    print(plt)
  }
}


# Generate District-Level Electoral System Plots
# This function creates a visualization to examine the relationship between an electoral rule and an outcome at the district level.

# data_dist: a list of district-level datasets to be processed and stacked.
# rule: a string specifying the independent variable (e.g., "M", "new.nvotes", "ballot_type", "threshold").
# outcome: a string specifying the dependent variable to be plotted.
# xlab: a string for the x-axis label.
# ylab: a string for the y-axis label.
# print: Logical; if TRUE (default), the plot is printed.
# file: could be implemented for saving the plot.
# type: a string indicating whether the independent variable is "ordered" (categorical) or "continuous".

# The function returns a ggplot object visualizing the relationship between the rule and outcome across electoral system families at the district level.


# This is the function that produces Figures 5.1 through 5.6. It is the simplest
# of the four: one rule, one outcome, district level only.
plot_district <- function(data_dist,
                          rule,
                          outcome,
                          xlab, ylab,
                          print = TRUE,
                          file,
                          type = c("ordered", "continuous")) {
  # Stack district-level data
  all_data <- data_stack(data_dist, rule, outcome, "District")

  # Apply data filtering conditions based on the selected rule
  # Each rule needs a different exclusion, because some combinations are not
  # meaningful: a majoritarian system with 43+ seats per district is a
  # simulation artefact, and vote rules or ballot types are not well defined
  # for mixed systems. The ! operator negates a condition, so filter(!(...))
  # reads "keep everything EXCEPT those cases".
  if (rule == "M") {
    all_data <- all_data %>%
      dplyr::filter(!((family == "Majoritarian") & (M > 43))) # Exclude majoritarian systems with M > 43
  }
  if (rule == "new.nvotes") {
    all_data <- all_data %>%
      dplyr::filter(!(family == "Mixed")) # Exclude mixed systems for this rule
  }
  if (rule == "ballot_type") {
    all_data <- all_data %>%
      dplyr::filter(!(family == "Mixed")) # Exclude mixed systems for this rule
  }
  if (rule == "threshold") {
    all_data <- all_data %>%
      dplyr::filter(!(family == "Majoritarian")) # Exclude majoritarian systems for this rule
  }

  # Initialize ggplot object with rule on x-axis and outcome on y-axis
  plt <- all_data %>%
    ggplot(aes(x = .data[[rule]], y = .data[[outcome]])) +
    scale_color_grey(start = 0.7, end = 0) + # Use grayscale color scale
    facet_wrap(vars(family), scales = "free_x") + # Facet plot by electoral family
    labs(x = xlab, y = ylab) + # Add axis labels
    theme_bw()

  # Add visualization type based on 'type' argument
  if (type == "continuous") {
    plt <- plt +
      geom_smooth(se = TRUE, color = "black") # Add a smoothed trend line with confidence intervals for continuous  variables
  } else {
    plt <- plt +
      # scale_x_discrete(guide = guide_axis(n.dodge = 3)) +
      theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + # Rotate x-axis labels for readability
      geom_boxplot() # Use box plots for categorical variables
  }

  # Print the plot if 'print' argument is TRUE
  if (print) {
    print(plt)
  }
}


# Generate Interaction Plots for Electoral Systems
# This function creates a visualization to examine the interaction between two electoral system characteristics and their effects on a specified outcome.

# data_cty: a list of country-level datasets to be processed and stacked.
# data_dist: a list of district-level datasets to be processed and stacked.
# rule: a character vector of length 2 specifying the two independent variables for interaction.
# outcome: a string specifying the dependent variable to be plotted.
# xlab: a string for the x-axis label.
# ylab: a string for the y-axis label.
# llab: a string for the legend label.
# print: Logical; if TRUE (default), the plot is printed.
# file: could be implemented for saving the plot.
# type: a string indicating whether the independent variable is "ordered" (categorical) or "continuous".

# The function returns a ggplot object visualizing the interaction between two rules and the electoral outcome across different electoral system families.

# NOTE: this is a second definition of plot_interact(), identical to the one
# above. Re-defining a function simply overwrites the earlier version, so the
# behaviour of Figure 5.7 is unaffected -- the two are the same recipe. The
# duplicate is left in place because this file reproduces the code as run.
plot_interact <- function(data_cty,
                          data_dist,
                          rule,
                          outcome,
                          xlab, ylab, llab,
                          print = TRUE,
                          file,
                          type = c("ordered", "continuous")) {
  # Stack country-level and district-level datasets
  all_data_cty <- data_stack(data_cty, rule, outcome, "Country")
  all_data_dist <- data_stack(data_dist, rule, outcome, "District")

  # Ensure column names match between both datasets
  names(all_data_cty) <- names(all_data_dist)

  # Combine both datasets into a single data frame
  all_data <- rbind(all_data_cty, all_data_dist)

  # Filter the data to include only relevant observations
  plt <- all_data %>%
    dplyr::filter(
      level == "District", # Consider only district-level data
      ballot_type %in% c("Closed", "Open"), # Include only these ballot types
      !((family == "Majoritarian") & (M > 43))
    ) %>% # Exclude majoritarian systems with M > 43
    ggplot(aes(x = .data[[rule[1]]], y = .data[[outcome]], color = .data[[rule[2]]])) +
    scale_color_grey(start = 0.7, end = 0) + # Use grayscale color scale
    facet_wrap(vars(family), scales = "free_x") + # Facet plot by electoral family
    labs(x = xlab, y = ylab, color = llab) + # Add axis labels and legend title
    theme_bw()

  # Add visualization type based on 'type' argument
  if (type == "continuous") {
    plt <- plt +
      geom_smooth(se = TRUE) # Add a smoothed trend line with confidence intervals for continuous variables
  } else {
    plt <- plt +
      # scale_x_discrete(guide = guide_axis(n.dodge = 3)) +
      theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) + # Rotate x-axis labels for readability
      geom_boxplot() # Use box plots for categorical variables
  }

  # Print the plot if 'print' argument is TRUE
  if (print) {
    print(plt)
  }
}

Figure 5.1: District Magnitude and TDE

This plot shows how district magnitude (M)—the number of seats allocated in a district—affects the Total Duvergerian Effect (TDE), which measures how strongly an electoral system constrains party competition. Specifically, TDE compares the effective number of parties under sincere voting (voters choosing their true preference) versus strategic voting (voters adjusting choices based on electoral incentives). A high TDE indicates strong pressure for party consolidation, while a low TDE signals a more permissive system that allows more party fragmentation.

# Reading the call: use the district-level TDE data; put district magnitude (M)
# on the x-axis and the TDE score on the y-axis; label the axes; and because M
# is a number rather than a category, draw a smoothed trend line
# (type = "continuous") rather than box plots.
# `file` is accepted by the function but never used -- the figure is displayed
# inline rather than written to disk.
plot_district(TDE_dist, "M", "TDE_enp", "Magnitude", "Total Duvergerian Effect",
  file = "Figures/M_TDE_dist.pdf", type = "continuous"
)

How to read Figure 5.1. One panel per electoral family. Within each, the line is a smoothed average of TDE across district magnitude, and the shaded band is its confidence interval. Look for the slope: a line falling steeply as M grows means larger districts weaken the pressure toward a small number of parties. Compare panels rather than absolute heights — the x-axis ranges differ because scales = "free_x" lets each family use its own.

On the x-axis of Figure 5.1, we have district magnitude (M), and on the y-axis, the average TDE scores. The lines represent different electoral system families—majoritarian and proportional—highlighting how the impact of district magnitude varies by system type. This figure demonstrates that district magnitude is a key structural rule shaping the level of party competition and the strategic dynamics parties and voters face in elections.

Figure 5.2: Seat Allocation Formula and TDE

This figure examines how seat allocation formulas—the mathematical methods that convert votes into seats—shape the Total Duvergerian Effect (TDE), which measures how strongly electoral systems push parties toward consolidation.

On the x-axis, the different seat allocation formulas are grouped by electoral system family: majoritarian systems primarily use plurality and absolute majority rules, while proportional representation (PR) systems rely on quota and divisor methods such as Hare, Droop, D’Hondt, and Sainte-Laguë. The y-axis shows the distribution of TDE scores, reflecting the varying degree to which each formula constrains party fragmentation.

# Same function, different rule. Because the seat allocation formula is a
# category rather than a number, type = "ordered" switches the display from a
# trend line to box plots -- one box per formula.
plot_district(TDE_dist, "elec_fun_name", "TDE_enp", "Seat Allocation Formula", "Total Duvergerian Effect",
  file = "Figures/Formula_TDE_dist.pdf", type = "ordered"
)

Reading a box plot. The thick line is the median. The box spans the middle half of the observations, from the 25th to the 75th percentile, so its height shows how spread out the scores are. The whiskers reach out to the bulk of the remaining values, and individual points beyond them are outliers. Comparing medians answers “which formula gives higher TDE?”; comparing box heights answers “how consistently?”.

Figure 5.2 highlights that while some variation exists across formulas, the biggest difference lies between majoritarian and proportional systems. PR systems consistently produce lower TDE scores, indicating more permissive environments for multiple parties. Thus, the broad electoral family matters far more for party competition than the specific seat allocation formula within PR systems.

Figure 5.3: Electoral Threshold and TDE

This figure examines how the legal threshold—the minimum vote share a party must clear to receive any seats—shapes the Total Duvergerian Effect (TDE). Of the three rules expected to govern interparty incentives, the threshold is the most direct: it excludes small parties by statute rather than by arithmetic.

Only proportional systems impose thresholds, so the figure resolves into a single panel. The x-axis shows the five threshold levels used in the simulations—0, 1%, 3%, 5% and 10%—and the y-axis the distribution of TDE scores at each.

# Same function and the same display settings as Figures 5.2 and 5.4, so the
# three figures are directly comparable: district-level data, a categorical rule
# on the x-axis (type = "ordered", hence box plots), TDE on the y-axis.
#
# Two things happen automatically here, both set up much earlier in the file:
#   1. plot_district() carries a rule-specific filter. For "threshold" it drops
#      the Majoritarian family, since systems that award a single seat by
#      plurality have no threshold to speak of. That is why the figure resolves
#      into one panel rather than three.
#   2. data_stack() recoded the raw threshold values into ordered labels
#      (0, 1%, 3%, 5%, 10%). Because recode_factor() fixes the level order, the
#      boxes appear in ascending order along the axis rather than alphabetically.
plot_district(TDE_dist, "threshold", "TDE_enp", "Electoral Threshold", "Total Duvergerian Effect",
  file = "Figures/Threshold_TDE_dist.pdf", type = "ordered"
)

How to read Figure 5.3. Compare median lines across the five boxes, left to right. A rising sequence means that higher statutory thresholds push the system toward fewer, larger parties — the Duvergerian pressure the measure is designed to capture. Watch the box heights as well: if the spread narrows as the threshold rises, high thresholds are not merely raising TDE on average but making the outcome more predictable across systems.

Figure 5.4: Ballot Type and AP

This figure examines how ballot type influences Average Personalism (AP), which captures the extent to which electoral systems encourage candidates to build personal reputations rather than relying solely on party labels. The expectation is simple: the more open the ballot, the stronger the incentive for candidates to stand out from their copartisans, resulting in higher personalism.

The x-axis shows different ballot structures. Majoritarian systems feature open and closed ballots, while proportional representation (PR) systems add a third category—flexible ballots—where voters can express candidate preferences, but party leaders still hold some control over candidate ranking. The y-axis displays AP scores, indicating the strength of personal vote incentives.

# The outcome switches from TDE to AP here, so the data argument changes from
# TDE_dist to AP_dist and the outcome column from "TDE_enp" to "pers".
plot_district(AP_dist, "ballot_type", "pers", "Ballot Type", "Personalism",
  file = "Figures/Ballot_pers_dist.pdf", type = "ordered"
)

How to read Figure 5.4. Compare median lines across the boxes, left to right. The prediction is a clear ordering — closed lowest, flexible in between, open highest — because the more freedom voters have to choose among copartisans, the more a candidate gains by standing out. Note that “flexible” appears only in the PR panel; majoritarian systems have no such category, which is why the two panels have different numbers of boxes.

Figure 5.4 confirms a key insight from electoral studies: ballot structure is one of the most powerful institutional factors shaping personalism. Closed-list ballots promote party discipline and reduce candidate-centered competition, while open and flexible ballots encourage candidate accountability and personal reputation-building. This figure highlights how electoral design choices shape intraparty competition by balancing party control and candidate autonomy.

Figure 5.5: Number and Level of Votes and AP

This figure explores how the number of votes each voter can cast influences Average Personalism (AP)—the extent to which candidates focus on building personal reputations rather than relying solely on party labels. Intuitively, when voters can cast multiple votes, especially in proportional representation (PR) systems, candidates have stronger incentives to differentiate themselves and cultivate personal support.

On the x-axis, the figure presents different voting rules, reflecting how many votes voters have. The y-axis shows the distribution of AP scores for each voting rule, separately for majoritarian (left panel) and PR (right panel) systems.

# "new.nvotes" is the recoded number-of-votes rule, whose labels were set in
# data_stack() above: One, Total Seats - 1, Total Seats, Total Candidates.
plot_district(AP_dist, "new.nvotes", "pers", "Nr. of Votes", "Personalism",
  file = "Figures/NVotes_pers_dist.pdf", type = "ordered"
)

How to read Figure 5.5. Read left to right within each panel, from fewest votes to most. A rising sequence in the PR panel supports the argument that giving voters more votes sharpens intraparty competition. A flat sequence in the majoritarian panel is equally informative: those systems are already candidate-centred, so there is little room for the rule to matter.

Figure 5.5 reveals that in PR systems, allowing multiple votes encourages greater personalism, as candidates compete not only against other parties but also within their own party lists. In contrast, restricting voters to a single vote tends to reinforce party-centered competition. In majoritarian systems, however, the number of votes appears to have little effect on personalism, likely because these systems already emphasize candidate-centered competition by design.

This figure highlights an important point: electoral rules shape not only who gets elected, but also how politicians campaign and connect with voters.

Figure 5.6: Vote-Pooling Level and AP

This figure examines how vote pooling—the level at which votes are aggregated—affects Average Personalism (AP). Vote pooling determines whether votes cast for a candidate count solely toward that candidate’s election or also contribute to electing co-partisans at the sub-party or full party list level.

The x-axis displays different levels of vote pooling, while the y-axis shows AP scores, reflecting the degree of personalism. The box plots illustrate how AP varies across pooling levels in majoritarian systems (left panel) and proportional representation systems (right panel).

# "pool_level" was recoded in data_stack() to Candidate, Sub-party List and
# Party List -- ordered from the narrowest pool to the broadest.
plot_district(AP_dist, "pool_level", "pers", "Vote Pooling Level", "Personalism",
  file = "Figures/Pool_pers_dist.pdf", type = "ordered"
)

How to read Figure 5.6. The expected pattern runs downward: personalism should be highest when votes pool at the candidate level and lowest when they pool across the whole party list, since broad pooling means a vote for one candidate helps their copartisans too. The steepness of that decline is the quantity of interest.

Figure 5.6 highlights a key trade-off in electoral design: if the aim is to enhance candidate accountability, lower levels of vote pooling (candidate-level) create stronger incentives for personalism. Conversely, pooling votes at the party-list level reinforces party unity and reduces intraparty competition. This figure underscores a central theme of the chapter—electoral rules shape not only electoral outcomes but also how politicians compete and relate within parties.

Figure 5.7: District Magnitude, Ballot Type, and AP

This figure explores how district magnitude (M)—the number of seats in a district—and ballot type jointly influence Average Personalism (AP), a relationship that has long sparked debate in electoral studies.

The x-axis shows district magnitude, while the y-axis displays AP scores. The lines trace how AP changes with M for different ballot types: open-list systems in light gray and closed-list systems in black. The left panel presents results for majoritarian systems; the right panel focuses on proportional representation systems..

# This is the only figure using plot_interact(), and the only one that needs
# BOTH datasets, country and district. The key argument is c("M","ballot_type"):
# the first name goes on the x-axis, the second becomes the line colour. That is
# what turns one trend line into several -- one per ballot type -- so the two
# rules can be seen acting together.
plot_interact(AP_cty, AP_dist,
  c("M", "ballot_type"), "pers", "Magnitude", "Personalism", "Ballot type",
  file = "Figures/M_ballot_type_pers.pdf", type = "continuous"
)

How to read Figure 5.7. Two things to look at, in this order. First the vertical gap between the lines: open-list systems sitting above closed-list ones is the main effect of ballot type. Second, and more demanding, whether that gap widens as district magnitude increases. A widening gap would support the classic claim that magnitude amplifies the personalising effect of open ballots; roughly parallel lines would not.

Figure 5.7 challenges some classic expectations. Although open-list systems consistently produce higher AP scores than closed-list systems, the expected pattern—that personalism rises with increasing district magnitude in open-list systems—is not strongly supported here. Instead, these results highlight the complex and context-dependent dynamics of intraparty competition shaped by electoral rules.

Moving Forward

Chapter 5 unpacked how the individual components of electoral systems—district magnitude, seat allocation formulas, ballot types, vote pooling, and voting levels—shape the strategic incentives for interparty and intraparty competition. Using detailed simulations, we saw how these rules influence the Total Duvergerian Effect (TDE) and Average Personalism (AP), revealing nuanced patterns in how electoral design encourages party consolidation or candidate-centered competition. Building on this foundation, Chapter 6 applies these insights to the real world.

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
forcats 1.0.0
ggplot2 3.5.2
lubridate 1.9.4
purrr 1.2.2
readr 2.1.5
stringr 1.5.1
tibble 3.3.0
tidyr 1.3.1
tidyverse 2.0.0

The environment used for the published book

This chapter did not render its session-information block when the published results were produced, so no record of that original environment survives. The tables above describe this run only.

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