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 7, together with an explanation of what each line does. The code is exactly the code we ran for the book, with one correction described immediately below; nothing has been simplified.

Why these figures differ from the ones printed in the book

If you compare Figures 7.2, 7.3 and 7.4 on this page with the versions printed in the book, they will not match. The difference is unfortunate: a coding error that produces the graphs in the book has been corrected here. Nothing about the data, the models or the argument has changed. This note explains what you are seeing and why.

What looks different

Two things, one obvious and one subtle.

The grey confidence envelopes are much thinner. In some places they are a fraction of their former width — at the right-hand edge of Figure 7.2 the width of the band is roughly one twentieth of the width that appears in the book.

The lines are also slightly steeper, though you have to look for it. In Figure 7.2 the expected number of parties now climbs a little further as constituency diversity rises than it did before.

What that means

Both changes point the same way: the relationships reported in this chapter are stronger and more precisely estimated than the printed figures in the book suggest.

The grey band represents uncertainty — the range of values consistent with the data. A narrower band means we can be more confident about where the true relationship lies. Where the printed figures leave room for doubt about whether electoral rules really condition the effect of social diversity, the corrected figures leave considerably less.

The steeper slope means the estimated effect itself is somewhat larger. The gap between how permissive and constraining systems translate voter diversity into parties is wider than the book reports.

So the correction does not overturn any conclusions in the chapter. It strengthens each of them. Readers working from the printed figures are seeing a conservative version of the result.

Why the error happened

The chapter’s simulations rest on a simple move: take the real data, hold everything constant except the one feature of interest, change that feature to a chosen value, and ask the model what to expect. Because our measure of electoral-system incentives is itself estimated, it comes in five slightly different versions rather than one, and every calculation is run five times and averaged — a standard way of carrying forward uncertainty in a measured quantity.

The five versions are stored side by side in five adjacent columns of a table. The original code selected those columns by counting positions across the table rather than by calling them by name — and the count was off by one. It began one column too far to the right, so it replaced only four of the five versions and left the first one holding its original, real-world values.

The consequence: for four of the five runs the model was asked the intended question — what if electoral incentives took this value? — while the fifth was quietly answering a different one, using the values actually observed. The five answers were then averaged as though they addressed the same question.

That did two things. It pulled the averaged prediction toward the observed data, flattening the lines. And it made the five answers disagree with one another for reasons that had nothing to do with genuine uncertainty. Because the procedure treats disagreement among the five as evidence of imprecision, that manufactured disagreement inflated the confidence bands. Most of the width of the envelopes in the printed figures in the book results from this error rather than from real uncertainty.

The code now selects those columns by name rather than by position, which is why the error cannot recur: a misspelled name stops the calculation, whereas a mistaken number simply operates on the wrong variable without complaint.

Chapter 7 tests whether the Total Duvergerian Effect (TDE) explains the size of party systems. It discusses the most widely used measure of party system size — the effective number of parties (N) — and looks not only at the simple effects of electoral rules on party system size, but also at how those rules operate across constituencies where voters are more or less diverse. Increasing TDE is associated with smaller party systems. What is more, as voters grow more diverse, high-TDE electoral systems discourage them from supporting as many parties as equally diverse constituencies do under low-TDE systems.

The central claim is an interaction. The chapter is not simply arguing that constraining systems produce fewer parties. It argues that the effect of social diversity depends on the electoral system: diversity should translate into more parties where rules are permissive, and much less so where they are constraining. That is why the models below all contain a term of the form TDE * diversity, and why the key figures plot expected N across a range of diversity, one line per level of TDE, rather than reporting a single number.

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. A few conventions used throughout:

  • <- is R’s assignment arrow; x <- 5 stores 5 under the name x.
  • library(name) loads an add-on package of extra functions.
  • 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 TDE and AP 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(tidyverse) # bundle of data-handling packages (dplyr, ggplot2, ...)
library(mice) # multiple imputation; pools results across the five datasets
library(miscTools) # assorted utilities
library(kableExtra) # formats knitr tables for HTML output
library(tidyverse) # (loaded a second time; harmless, R simply ignores it)

Why five of everything. As elsewhere in the book, the GBM stage produced five sets of predicted TDE and AP scores rather than one, reflecting uncertainty in the underlying imputation. Every model below is therefore fitted five times, once per set of predictions, and the results are pooled at the end. This is why so much of the code loops over 1:5, and why you will see columns named totalEff.hat1 through totalEff.hat5.

The first block defines the function that produces every expected value and confidence interval in Figures 7.2 through 7.4. Nothing runs here — defining a function is like writing down a recipe, and the cooking happens later.

The recipe in one sentence. Fix TDE at a chosen level and diversity at a chosen level, leave every other feature of the data as it is, ask the fitted model how many parties to expect, then repeat the whole exercise on 500 resampled datasets to see how much that answer moves.

Calling it repeatedly across a range of diversity values, once for each level of TDE, is what traces out the lines in the figures.

The arguments are the dials:

  • tdeValue — the level of the Total Duvergerian Effect to impose
  • tot_divValue — the level of social diversity to impose
  • reps — how many bootstrap resamples to draw (500 by default)
  • seed — fixes the random draws so results reproduce exactly
  • allCountries / countryData — whether to average over every district in the data, or over the districts of one specified system. Figures 7.2 and 7.3 use the first; Figure 7.4 uses the second to profile selected countries.
# Function to run bootstrap, district level
boot.predict.se.enp <- function(tdeValue = 0.5, tot_divValue, reps = 500, seed = 123354, allCountries = TRUE, countryData) {
  # require() loads a package from inside a function, so it still works if
  # copied into a fresh session.
  require(fixest)
  require(miscTools)

  # Set Seed
  # Bootstrapping is random; fixing the seed makes the numbers reproducible.
  set.seed(seed)

  # Name the five TDE columns once, and refer to them by name from here on.
  # paste0("totalEff.hat", 1:5) builds the character vector
  #   "totalEff.hat1" "totalEff.hat2" ... "totalEff.hat5"
  # Both places below need to overwrite all five at once. Selecting them by
  # name rather than by position means the code keeps working if the column
  # order of the data ever changes -- and, unlike a numeric range, a wrong name
  # is visible on the page rather than silently shifting which variable is set.
  tde_cols <- paste0("totalEff.hat", 1:5)

  # Run Model
  # The same model fitted to each of the five sets of GBM predictions.
  # Reading the formula:
  #   enp                     effective number of parties -- the outcome
  #   totalEff.hat1 * tot_div TDE interacted with diversity. The * expands to
  #                           three terms: TDE alone, diversity alone, and their
  #                           product. The product is the chapter's argument.
  #   pers.hat1               Average Personalism, held constant as a control
  #   | ctyyear               country-year fixed effects: a separate intercept
  #                           for every country-year, so the estimate rests on
  #                           comparisons BETWEEN districts within the same
  #                           election, not across countries or over time
  #   se = 'hetero'           heteroskedasticity-robust standard errors
  # Note the index on the predictor names: model 1 uses totalEff.hat1 and
  # pers.hat1, model 2 uses ...hat2, and so on through the five predictions.
  temp_list <- list()
  temp_list[[1]] <- feols(enp ~ totalEff.hat1 * tot_div + pers.hat1 | ctyyear, se = "hetero", data = tempData)
  temp_list[[2]] <- feols(enp ~ totalEff.hat2 * tot_div + pers.hat2 | ctyyear, se = "hetero", data = tempData)
  temp_list[[3]] <- feols(enp ~ totalEff.hat3 * tot_div + pers.hat3 | ctyyear, se = "hetero", data = tempData)
  temp_list[[4]] <- feols(enp ~ totalEff.hat4 * tot_div + pers.hat4 | ctyyear, se = "hetero", data = tempData)
  temp_list[[5]] <- feols(enp ~ totalEff.hat5 * tot_div + pers.hat5 | ctyyear, se = "hetero", data = tempData)

  # Point Estimate
  # Choose the population to average over: every district in the data, or only
  # the districts of the system passed in through countryData.
  if (allCountries) {
    newData <- tempData # copy data
  } else {
    newData <- countryData # copy data
  }
  # Overwrite the two variables of interest, leaving everything else untouched.
  # This is the standard "hold all else constant" manoeuvre: whatever difference
  # appears between two calls is attributable to the values imposed here.
  # tde_cols, defined at the top of the function, names all five TDE prediction
  # columns. They are set at once so that each of the five fitted models sees
  # the imposed value rather than the observed one.
  newData[, tde_cols] <- tdeValue # Replace tde value
  newData[, "tot_div"] <- tot_divValue # Replace tot_div value
  # predict() gives one fitted value per district; mean() averages them into a
  # single expected N for this combination of TDE and diversity.
  point.estimate1 <- mean(predict(temp_list[[1]], newdata = newData))
  point.estimate2 <- mean(predict(temp_list[[2]], newdata = newData))
  point.estimate3 <- mean(predict(temp_list[[3]], newdata = newData))
  point.estimate4 <- mean(predict(temp_list[[4]], newdata = newData))
  point.estimate5 <- mean(predict(temp_list[[5]], newdata = newData))

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

  # Calculate SE
  # ------------------------------------------------------------------------
  # THE BOOTSTRAP
  # A point estimate says nothing about precision. The bootstrap answers "how
  # much would this number move if we had drawn a different sample?" by
  # repeatedly resampling the data we have, with replacement, and re-running
  # the whole calculation on each resample. The spread of the results estimates
  # the standard error.
  # One row per resample, one column per set of GBM predictions.
  # ------------------------------------------------------------------------
  sterrs <- matrix(NA, nrow = reps, ncol = 5)
  for (i in 1:reps) {
    # Create Data
    # Draw row numbers at random, as many as the data has rows, with
    # replacement -- so some rows appear twice and others not at all.
    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(enp ~ totalEff.hat1 * tot_div + pers.hat1 | ctyyear, se = "hetero", data = bootdat)
    temp_list_boot[[2]] <- feols(enp ~ totalEff.hat2 * tot_div + pers.hat2 | ctyyear, se = "hetero", data = bootdat)
    temp_list_boot[[3]] <- feols(enp ~ totalEff.hat3 * tot_div + pers.hat3 | ctyyear, se = "hetero", data = bootdat)
    temp_list_boot[[4]] <- feols(enp ~ totalEff.hat4 * tot_div + pers.hat4 | ctyyear, se = "hetero", data = bootdat)
    temp_list_boot[[5]] <- feols(enp ~ totalEff.hat5 * tot_div + pers.hat5 | ctyyear, se = "hetero", data = bootdat)
    # Calculate predict value
    # Point Estimate
    if (allCountries) {
      newDataBootData <- bootdat # copy data
    } else {
      newDataBootData <- countryData # copy data
    }
    # Same substitution as above, on the resampled data. Selected by name via
    # tde_cols so both places cannot drift apart.
    newDataBootData[, tde_cols] <- tdeValue # Replace TDE value
    newDataBootData[, "tot_div"] <- tot_divValue # Replace tot_div value
    sterrs[i, 1] <- mean(predict(temp_list_boot[[1]], newdata = newDataBootData))
    sterrs[i, 2] <- mean(predict(temp_list_boot[[2]], newdata = newDataBootData))
    sterrs[i, 3] <- mean(predict(temp_list_boot[[3]], newdata = newDataBootData))
    sterrs[i, 4] <- mean(predict(temp_list_boot[[4]], newdata = newDataBootData))
    sterrs[i, 5] <- mean(predict(temp_list_boot[[5]], newdata = newDataBootData))
  }
  # prepare to return results
  # ------------------------------------------------------------------------
  # RUBIN'S RULES
  # Total uncertainty has two sources, added together here:
  #   WITHIN  -- mean(apply(sterrs, 2, var)) takes the variance down each of the
  #              five columns and averages them: ordinary sampling uncertainty.
  #   BETWEEN -- the sum-of-squares term measures how far the five column means
  #              sit from the overall mean: the extra uncertainty from not
  #              knowing the true TDE and AP values. The multiplier
  #              (5+1)/(5*(5-1)) is Rubin's correction for having five sets.
  # apply(X, 2, var) means "apply var() to each column" (2 = columns, 1 = rows).
  # ------------------------------------------------------------------------
  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))

  # CIs
  # qnorm() returns the cut-points of a normal distribution: qnorm(0.025) is
  # -1.96 and qnorm(0.975) is +1.96, which bracket the middle 95%. The 90%
  # interval uses 0.05 and 0.95, giving the narrower inner band in the figures.
  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)
  # Bundle the six numbers into one named vector and hand it back to the caller.
  val <- c(point.estimate, sd_pool, lb95, ub95, lb90, ub90)
  names(val) <- c("Point Estimate", "Std. Error", "lb95", "ub95", "lb90", "ub90")
  return(val)
}

A second, purely cosmetic helper. It turns estimated coefficients into the strings that appear in the regression table, of the form 0.123*** (0.045).

# Function to extract estimates for table 7.4
extract_Estimates <- function(model_name) {
  # Three nested ifelse() calls walk down the significance thresholds, and the
  # first match wins: p < 0.01 gets three stars, otherwise p < 0.05 gets two,
  # otherwise p < 0.10 gets one, otherwise none. paste0() glues the pieces into
  # a single string with the standard error in parentheses.
  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 7 revisits a central question in electoral politics: How do the incentives embedded in electoral systems shape the size of party systems? Building on the conceptual framework and simulation-based measures introduced earlier in the book—specifically the Total Duvergerian Effect (TDE) that captures the strength of interparty incentives—this chapter investigates the relationship between electoral rules and the effective number of parties in a system. Using both simulation results and empirical data at the district level, the chapter tests the hypothesis that electoral systems with stronger constraining incentives (high TDE) produce smaller party systems, while weaker, more permissive systems (low TDE) allow for a larger number of viable parties.

Furthermore, the chapter explores how social heterogeneity within electoral constituencies interacts with electoral system incentives to influence party system size. We show that in more diverse constituencies, permissive systems enable the expression of a broader range of political preferences through multiple parties, whereas constraining systems suppress party system size regardless of diversity.

The chapter sets the stage for the next part of the book, which examines not only how many parties exist but where those parties position themselves ideologically. Understanding the determinants of party system size is fundamental to grasping the dynamics of electoral competition and democratic representation.

This file contains calls to datasets used in Chapter 7 (The Size of the Party System), as well as the code necessary to produce all graphs in the chapter.

Data processing

The following code blocks prepare the data necessary for analyzing how electoral system incentives shape the size of party systems.

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/ch07/... 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 07 materials

# the short file names below. This is the one line to edit for your machine.


# load Potter's data
# District-level election returns, from Potter's dataset. Supplies the object
# `potter`, whose key columns are enp (effective number of parties), tot_div
# (social diversity), M (district magnitude) and the country-year identifiers.
load(file = "data/ch07/RData/potter_expanded.RData")

# Load district-level GBM objects
# The fitted gradient boosting models from Chapter 6. They are not re-estimated
# here; they are only asked to predict TDE and AP for these real districts.
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 the five
# training runs, so each of these is a list of five models.
optimalGBMIntra <- totalAP.objects$optimalGBM
optimalGBMInter <- totalEffENP.objects$optimalGBM

# Load country level scores
# Country-level TDE and AP, merged in further down for the national-level
# comparison in Figure 7.1. 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")

This block uses these GBM models to generate predicted values of TDE and AP for each district across five imputations, aggregates the results, and computes average estimates. It then merges these predictions with country-level data and checks the correlation between TDE and the effective number of electoral parties (ENP), laying the groundwork for the regression and visualization analyses that follow.

### We need to run predictions five times
# Predict Interparty and Intraparty

potterData <- list() # Initialize a list to store predicted data

# One pass per fitted model: predict TDE and AP for every real district, then
# store that version of the dataset. Each pass overwrites the same two columns
# of `potter` before saving a copy, so the five stored datasets differ only in
# their predicted scores.
for (i in 1:5) {
  # Interparty incentive prediction: Total Duvergerian Effect (TDE)
  # n.trees tells the boosting model how many of its trees to use -- here all
  # of those selected as optimal during training.
  potter$totalEff.hat <- predict(optimalGBMInter[[i]], potter, n.trees = optimalGBMInter[[i]]$n.trees)
  # Intraparty incentive prediction: Average Personalism (AP)
  potter$pers.hat <- predict(optimalGBMIntra[[i]], potter, n.trees = optimalGBMIntra[[i]]$n.trees)
  # Store updated dataset in the list
  potterData[[i]] <- potter
}

# Combine all five versions into a single dataset for aggregation
# Get TDE and AP, then average the values across datasets
# sapply(1:5, function(i) ...) pulls the same columns out of each of the five
# datasets; simplify = FALSE keeps them as a list, and do.call('rbind', ...)
# stacks them into one tall table five times the original height.
all_TDE_AP_dist <- do.call("rbind", sapply(1:5, function(i) potterData[[i]][, c("cnty", "distname", "id", "year", "ctyyear", "distname", "M", "totalEff.hat", "pers.hat", "enp")], simplify = FALSE))

# Average predictions across imputations for each district-year
# ddply() splits the stacked table into groups, applies a function to each, and
# recombines. The grouping variables identify a single district in a single
# election, so this collapses the five stacked copies back into one row per
# district carrying the averaged TDE and AP. M and enp are identical across the
# five copies; taking their mean simply carries them through unchanged.
avg_TDE_AP_dist <- plyr::ddply(
  .data = all_TDE_AP_dist, .variables = c("cnty", "year", "ctyyear", "distname", "id"), .fun = plyr::summarise,
  M = mean(M),
  totalEff.hat = mean(totalEff.hat),
  pers.hat = mean(pers.hat),
  enp = mean(enp)
)


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

# # Correlation between predicted TDE and observed party system size (enp)
# A first look before any modelling: cor.test() reports the correlation between
# predicted TDE and the observed number of parties, with a p-value. The
# chapter's argument predicts a NEGATIVE correlation -- more constraining
# systems, fewer parties. This is a raw association with nothing held constant,
# so it is a sanity check rather than evidence.
cor.test(all_TDE_AP_dist$enp, all_TDE_AP_dist$totalEff.hat)
## 
##  Pearson's product-moment correlation
## 
## data:  all_TDE_AP_dist$enp and all_TDE_AP_dist$totalEff.hat
## t = -49, df = 7608, p-value <2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  -0.510 -0.476
## sample estimates:
##    cor 
## -0.493
#############################################

# Match country-level TDE data to Potter dataset by country-year
## Keep TDE (Country level) for countries and years in potter
data2export$ctyyear <- paste0(data2export$country, "-", data2export$year)
tdeCountryLevel <- subset(data2export, ctyyear %in% unique(potter$ctyyear))
# Keep only the first election per country-year
tdeCountryLevel <- subset(tdeCountryLevel, election_count == 1)
# Merge with district-level IDs for consistent identification
tdeCountryLevel <- merge(tdeCountryLevel, unique(avg_TDE_AP_dist[, c("ctyyear", "id")]), by = "ctyyear")

Models

This block estimates a series of linear regression models to test whether electoral system incentives—captured by the Total Duvergerian Effect (TDE)—and other factors help explain variation in the size of party systems, measured as the effective number of electoral parties (ENP). Using five imputed datasets, it estimates four nested models: a baseline model with TDE alone, then progressively adds average personalism (AP), constituency diversity, and finally an interaction between TDE and diversity. Each specification is estimated both with and without country-election fixed effects. The results from the five imputations are then pooled using Rubin’s rules to produce final coefficient estimates and standard errors for interpretation.

Eight models, in a 4 × 2 grid. Four specifications, each estimated twice — hence the naming convention: m1 to m4 are the specifications, and the trailing b marks the fixed-effects version.

Adds Without FE With FE
1 TDE alone m1_l m1_lb
2 + AP m2_l m2_lb
3 + diversity m3_l m3_lb
4 + TDE × diversity m4_l m4_lb

The | ctyyear in the b versions adds country-year fixed effects: a separate intercept for every election. That absorbs everything constant within an election — national political culture, the party system’s history, the state of the economy — so the estimate rests only on comparisons between districts of the same election. Model 4b is the chapter’s preferred specification.

# Run Models

# Initialize lists to store regression models across the 5 imputations
# Eight empty containers, one per model in the grid above. Each will end up
# holding five fitted models -- one per set of GBM predictions.
m1_l <- list()
m1_lb <- list()
m2_l <- list()
m2_lb <- list()
m3_l <- list()
m3_lb <- list()
m4_l <- list()
m4_lb <- list()

# Model Estimation Loop
# One pass fits all eight models to prediction set i.
for (i in 1:5) {
  # Very basic model.
  # Raw association: does TDE alone track the number of parties?
  m1_l[[i]] <- feols(enp ~ totalEff.hat, se = "hetero", data = potterData[[i]])
  m1_lb[[i]] <- feols(enp ~ totalEff.hat | ctyyear, se = "hetero", data = potterData[[i]])
  # add AP
  # Check that TDE is not standing in for the intraparty dimension.
  m2_l[[i]] <- feols(enp ~ totalEff.hat + pers.hat, se = "hetero", data = potterData[[i]])
  m2_lb[[i]] <- feols(enp ~ totalEff.hat + pers.hat | ctyyear, se = "hetero", data = potterData[[i]])
  # Add total (constituency) diversity
  # Diversity enters as a separate cause: more heterogeneous electorates may
  # support more parties whatever the rules.
  m3_l[[i]] <- feols(enp ~ totalEff.hat + tot_div + pers.hat, se = "hetero", data = potterData[[i]])
  m3_lb[[i]] <- feols(enp ~ totalEff.hat + tot_div + pers.hat | ctyyear, se = "hetero", data = potterData[[i]])
  # With Interaction (between TDE and diversity)
  # The chapter's actual hypothesis. The * expands to three terms -- TDE,
  # diversity, and their product -- and it is the product that says the effect
  # of diversity DEPENDS ON how constraining the system is.
  m4_l[[i]] <- feols(enp ~ totalEff.hat * tot_div + pers.hat, se = "hetero", data = potterData[[i]])
  m4_lb[[i]] <- feols(enp ~ totalEff.hat * tot_div + pers.hat | ctyyear, se = "hetero", data = potterData[[i]])
}

# Combine Estimates Across Imputations
# 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()
# then presents it as the tidy table extract_Estimates() expects.
out1 <- summary(pool(m1_l))
out1b <- summary(pool(m1_lb))
out2 <- summary(pool(m2_l))
out2b <- summary(pool(m2_lb))
out3 <- summary(pool(m3_l))
out3b <- summary(pool(m3_lb))
out4 <- summary(pool(m4_l))
out4b <- summary(pool(m4_lb))

Table 7.4: Association Between TDE, AP, and the Effective Number of Electoral Parties

This block generates Table 7.4 by organizing, merging, and formatting the results from eight pooled linear regression models estimated earlier in the chapter. These models examine the relationship between the Total Duvergerian Effect (TDE), Average Personalism (AP), constituency diversity, and their interaction effects on the effective number of electoral parties (N). The table reports coefficient estimates, robust standard errors, and model fit statistics, offering a comprehensive view of how electoral incentives and social heterogeneity shape party system size.

# We need to build a table for the book
# Gather model outputs
# The eight pooled summaries, in the column order they will appear.
model_list <- list(out1, out1b, out2, out2b, out3, out3b, out4, out4b)
out_estimates <- sapply(model_list, extract_Estimates, simplify = FALSE)

# Create table
# Merge coefficients across models by variable name
# all = TRUE keeps rows present in only some models -- essential here, because
# the simpler specifications have no diversity or interaction terms and those
# cells must come out blank rather than dropping the row entirely.
mytable <- merge(out_estimates[[1]], out_estimates[[2]], by = "variable", all = TRUE)
for (i in c(3:8)) mytable <- merge(mytable, out_estimates[[i]], by = "variable", all = TRUE)

# Reorder rows and rename them
# Reorder variables
mytable <- mytable[c(4, 2, 3, 5, 1), ]
# Rename rows
row.names(mytable) <- mytable$variable
mytable$variable <- NULL

# Calculate TSS: Total sum of squares for ENP (based on first imputation)
TSS <- sum((potterData[[1]]$enp - mean(potterData[[1]]$enp))^2)

# Compute R2 and Adj. R2 for all models
r2_m1 <- round(Reduce("+", lapply(m1_l, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(m1_l), 3)
r2_m1b <- round(Reduce("+", lapply(m1_lb, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(m1_lb), 3)
r2_m2 <- round(Reduce("+", lapply(m2_l, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(m2_l), 3)
r2_m2b <- round(Reduce("+", lapply(m2_lb, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(m2_lb), 3)
r2_m3 <- round(Reduce("+", lapply(m3_l, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(m3_l), 3)
r2_m3b <- round(Reduce("+", lapply(m3_lb, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(m3_lb), 3)
r2_m4 <- round(Reduce("+", lapply(m4_l, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(m4_l), 3)
r2_m4b <- round(Reduce("+", lapply(m4_lb, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(m4_lb), 3)

# Calculate Adj. Pseudo R2
adjr2_m1 <- round(Reduce("+", lapply(m1_l, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(m1_l), 3)
adjr2_m1b <- round(Reduce("+", lapply(m1_lb, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(m1_lb), 3)
adjr2_m2 <- round(Reduce("+", lapply(m2_l, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(m2_l), 3)
adjr2_m2b <- round(Reduce("+", lapply(m2_lb, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(m2_lb), 3)
adjr2_m3 <- round(Reduce("+", lapply(m3_l, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(m3_l), 3)
adjr2_m3b <- round(Reduce("+", lapply(m3_lb, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(m3_lb), 3)
adjr2_m4 <- round(Reduce("+", lapply(m4_l, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(m4_l), 3)
adjr2_m4b <- round(Reduce("+", lapply(m4_lb, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(m4_lb), 3)

# Add R2, adj.R2, and N rows to the regression table
mytable <- rbind(mytable, "Fixed Effects by Country-Year" = c("No", "Yes", "No", "Yes", "No", "Yes", "No", "Yes"))
mytable <- rbind(mytable, "R2" = c(r2_m1, r2_m1b, r2_m2, r2_m2b, r2_m3, r2_m3b, r2_m4, r2_m4b))
mytable <- rbind(mytable, "Adj R2" = c(adjr2_m1, adjr2_m1b, adjr2_m2, adjr2_m2b, adjr2_m3, adjr2_m3b, adjr2_m4, adjr2_m4b))
mytable <- rbind(mytable, "Observations" = c(
  m1_l[[1]]$nobs, m1_lb[[1]]$nobs,
  m2_l[[1]]$nobs, m2_lb[[1]]$nobs,
  m3_l[[1]]$nobs, m3_lb[[1]]$nobs,
  m4_l[[1]]$nobs, m4_lb[[1]]$nobs
))

# Rename rows
row.names(mytable) <- c(
  "TDE",
  "AP",
  "Constituency Diversity",
  "TDE x Constituency Diversity",
  "Intercept",
  "Fixed Effects by Country-Election",
  "R2",
  "Adj. R2",
  "Observations"
)

# Replace NA for '' (empty strings) and print table
mytable[is.na(mytable)] <- ""

# Create table
mytable %>%
  kbl(
    caption = ": Association Between tde, ap, and the Effective Number of Electoral Parties",
    col.names = c("(1)", "(2)", "(3)", "(4)", "(5)", "(6)", "(7)", "(8)"),
    format = "html",
    linesep = "",
  )
: Association Between tde, ap, and the Effective Number of Electoral Parties
(2) (3) (4) (5) (6) (7) (8)
TDE -0.604*** (0.057) -1.232*** (0.134) -0.571*** (0.04) -1.241*** (0.14) -0.62*** (0.046) -1.208*** (0.14) -0.313** (0.13) -0.564*** (0.192)
AP 8.581*** (0.829) -0.772 (4.179) 8.575*** (0.839) -0.753 (4.142) 8.567*** (0.816) -0.581 (3.997)
Constituency Diversity 0.745*** (0.183) 0.547*** (0.156) 1.759*** (0.49) 2.956*** (0.565)
TDE x Constituency Diversity -0.471** (0.183) -0.976*** (0.198)
Intercept 4.68*** (0.134) 0.615 (0.456) 0.229 (0.445) -0.401 (0.532)
Fixed Effects by Country-Election No Yes No Yes No Yes No Yes
R2 0.245 0.663 0.38 0.663 0.387 0.666 0.39 0.672
Adj. R2 0.244 0.655 0.379 0.655 0.386 0.657 0.388 0.663
Observations 1522 1522 1522 1522 1522 1522 1522 1522

Table 7.4 provides robust empirical support for the chapter’s main argument: electoral systems with stronger constraining incentives, measured by higher TDE values, are associated with smaller effective party systems. Across all model specifications, the TDE coefficient is consistently negative and statistically significant, confirming that more constraining electoral rules discourage party fragmentation. While personalism (AP) shows a positive association with party system size in models without fixed effects, this relationship disappears when controlling for country-election fixed effects. Constituency diversity has a positive and significant effect on party system size, indicating that more heterogeneous electorates support more parties. Crucially, the negative and significant interaction between TDE and constituency diversity reveals that high TDE systems suppress the effect of diversity on party system size. This finding confirms that electoral rules condition how social heterogeneity translates into political fragmentation.

Expectations

This block prepares simulated quantities of interest to visualize how electoral system incentives and constituency diversity jointly shape party system size. It constructs a dataset that consolidates predicted TDE and AP values across all imputations, and then uses bootstrapped simulations to calculate the expected effective number of parties (ENP). Specifically, it simulates how ENP changes when holding TDE constant and varying diversity, and vice versa.

Why the data are rebuilt here. boot.predict.se.enp() expects one wide table holding all five sets of predictions side by side, as columns totalEff.hat1totalEff.hat5 and pers.hat1pers.hat5. The predictions currently live in five separate datasets, so this block flattens them into that shape. The function then selects the TDE columns by name, so what matters is that all five are present and correctly named — not where they sit.

# Create unified dataset for prediction

# Rearange data to run predict() and calculate SEs
# Start from the first prediction set, keeping the outcome, the two predictors,
# the control and the fixed-effect identifier.
tempData <- potterData[[1]][, c("enp", "totalEff.hat", "tot_div", "pers.hat", "ctyyear")]
# Rename columns 2 and 4 so they carry the "1" suffix the function expects.
names(tempData)[c(2, 4)] <- c("totalEff.hat1", "pers.hat1")
# Adds TDE and AP predictions from the other four imputations.
tempData <- cbind(
  tempData, potterData[[2]][, c("totalEff.hat", "pers.hat")],
  potterData[[3]][, c("totalEff.hat", "pers.hat")],
  potterData[[4]][, c("totalEff.hat", "pers.hat")],
  potterData[[5]][, c("totalEff.hat", "pers.hat")]
)
names(tempData)[6:13] <- c(
  "totalEff.hat2", "pers.hat2", "totalEff.hat3", "pers.hat3",
  "totalEff.hat4", "pers.hat4", "totalEff.hat5", "pers.hat5"
)
# Reorder variables for clarity
tempData <- tempData[, c(
  "enp", "tot_div", "ctyyear",
  "pers.hat1", "pers.hat2", "pers.hat3", "pers.hat4", "pers.hat5",
  "totalEff.hat1", "totalEff.hat2", "totalEff.hat3", "totalEff.hat4", "totalEff.hat5"
)]

# Fix tde at minimum, median, and max, then vary tot_div (Constituency Diversity) from min to max
# seq(from, to, length.out = 15) builds a ladder of 15 evenly spaced diversity
# values spanning the observed range. Walking up that ladder twice -- once with
# TDE pinned at its minimum, once at its maximum -- traces the two lines of
# Figure 7.2. If the interaction is real, the two lines will not be parallel.
tot_divValues <- seq(min(tempData$tot_div), max(tempData$tot_div), length.out = 15)
outTDEMin <- list()
outTDEMax <- list()

# 15 rungs x 2 lines x 500 bootstrap resamples x 5 models: this loop is the
# slowest part of the chapter.
for (i in 1:length(tot_divValues)) {
  outTDEMin[[i]] <- boot.predict.se.enp(tdeValue = min(all_TDE_AP_dist$totalEff.hat), tot_divValue = tot_divValues[i], reps = 500, seed = 123354)
  outTDEMax[[i]] <- boot.predict.se.enp(tdeValue = max(all_TDE_AP_dist$totalEff.hat), tot_divValue = tot_divValues[i], reps = 500, seed = 123354)
}

# Fix tot_div at minimum, median, and max, then vary TDE from min to max
# The mirror image of the block above: now TDE varies along the ladder and
# diversity is pinned at its extremes. Same interaction viewed from the other
# side, which is a useful check that the finding is not an artefact of which
# variable is placed on the horizontal axis.
tdeValues <- seq(min(all_TDE_AP_dist$totalEff.hat), max(all_TDE_AP_dist$totalEff.hat), length.out = 15)
outTotDivMin <- list()
outTotDivMax <- list()
for (i in 1:length(tdeValues)) {
  outTotDivMin[[i]] <- boot.predict.se.enp(tdeValue = tdeValues[i], tot_divValue = min(tempData$tot_div), reps = 500, seed = 123354)
  outTotDivMax[[i]] <- boot.predict.se.enp(tdeValue = tdeValues[i], tot_divValue = max(tempData$tot_div), reps = 500, seed = 123354)
}

# Unlist results
# Combine all predictions into data frames
# Each list element is one six-number vector; rbind() stacks them into a table
# with 15 rows and columns for the estimate, its error, and the four CI bounds.
outTDEMin <- do.call("rbind", outTDEMin)
outTDEMax <- do.call("rbind", outTDEMax)
outTotDivMin <- do.call("rbind", outTotDivMin)
outTotDivMax <- do.call("rbind", outTotDivMax)

Figure 7.1: Estimated TDE at the District and National Levels

This block generates Figure 7.1, which visualizes the Total Duvergerian Effect (TDE) at both the district and national levels for each country-year in the dataset. District-level TDE estimates are plotted as gray dots, while black triangles represent national-level averages. This visualization allows a direct comparison of within-country variation alongside between-country differences in electoral system strength.

# pdf('~/Downloads/Plots/tde_district_national.pdf', width = 14)
# The commented pdf() and dev.off() lines would write the figure to a file
# instead of displaying it. Both are disabled so it appears inline.
# Margins: bottom 10, to leave room for the rotated country-year labels.
par(mar = c(10, 5, 2, 2))

# Order the data to match plotting order
# Both datasets must be sorted the same way, because the country-level points
# and the axis labels are positioned by the shared `id` column.
all_TDE_AP_dist <- all_TDE_AP_dist[order(all_TDE_AP_dist$ctyyear), ]
tdeCountryLevel <- tdeCountryLevel[order(tdeCountryLevel$ctyyear), ]

# Plot district-level TDE scores
# One grey dot per district. pch = 20 is a small filled circle, cex = 2 doubles
# its size, and axes = FALSE suppresses the default axes so custom ones can be
# drawn below.
plot(
  x = (all_TDE_AP_dist$id), (all_TDE_AP_dist$totalEff.hat), pch = 20, # filled circles
  ylab = "", xlab = "", axes = FALSE, cex = 2, col = "grey", ylim = c(0, 4.1)
)
# Add country-level TDE scores as black triangles
points(x = tdeCountryLevel$id, y = tdeCountryLevel$totalEff.hat, pch = 17)
# Draw x-axis with country-year labels
axis(1, at = tdeCountryLevel$id, labels = tdeCountryLevel$ctyyear, las = 2, cex.axis = 1.25)
# Draw y-axis for TDE
axis(2, las = 2, cex.axis = 1.25, at = seq(0, 4, 0.5))
mtext("TDE", 2, line = 3.5, cex = 1.25)

# dev.off()

How to read Figure 7.1. Each vertical slice is one election, labelled along the bottom. The grey dots in that slice are its individual districts; the black triangle is the single national-level score for the same election.

The point of the figure is the vertical spread of grey dots. A tall column means districts within that one election face very different interparty incentives, and that the national figure — the triangle — is an average concealing wide internal variation. Where the triangle sits low while the dots range high, the national score would badly mislead anyone treating the country as having one electoral system. That is the argument for analysing this chapter at the district level.

The accompanying code also retrieves summary statistics for Switzerland 2007—one of the most illustrative cases of within-country heterogeneity—used in the interpretation of the figure. This visualization sets the stage for the chapter’s district-level analytical focus.

Duplicate chunk label. knitr takes a chunk’s label from the first unnamed option, so this block and the one above may both resolve to figure-7.1. If knitting stops with Duplicate chunk label 'figure-7.1', renaming this one — to figure-7.1-interpretation, say — fixes it. Chunk labels do not affect results.

# For text that goes with Figure 7.1
# Get summary stats for Switzerland (used in text)
# The square brackets subset the column to rows matching one country-year, and
# summary() reports min, quartiles, median, mean and max of what remains.
# The first line describes the spread ACROSS Swiss districts; the second gives
# the single national score. The gap between them is the figures quoted in the
# chapter text. Nothing is stored -- these print and are read off.
summary(avg_TDE_AP_dist$totalEff.hat[avg_TDE_AP_dist$ctyyear == "Switzerland-2007"])
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   0.588   1.273   1.309   1.644   1.762   3.109
summary(tdeCountryLevel$totalEff.hat[tdeCountryLevel$ctyyear == "Switzerland-2007"])
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   0.488   0.488   0.488   0.488   0.488   0.488

Figure 7.1 offers a clear picture of how TDE varies both within and across countries for 36 elections in 14 nations. Notably, the figure reveals substantial within-country variation in electoral system strength, particularly in proportional representation systems. For example, Switzerland exhibits a wide range of district-level TDE scores (0.588 to 3.109) around a comparatively low national average of 0.488. This spread reflects how mechanical and psychological incentives produced by electoral rules differ substantially across districts. The figure thus underscores the relevance of conducting analyses at the district level, where seat allocation rules operate most directly, and supports the chapter’s empirical approach that uses district-level TDE to explain party system size.

Figure 7.2: Expected Effective Number of Parties (N) Conditional on Diversity

This block generates Figure 7.2, which visualizes how the interaction between electoral system permissiveness (Total Duvergerian Effect, TDE) and constituency diversity influences the expected effective number of parties (N). Using model-based predictions and bootstrapped 95% confidence intervals, the figure plots expected party system size across varying levels of diversity under two scenarios: one with low TDE (permissive electoral rules) and one with high TDE (constraining rules).

This is the chapter’s key figure. Everything else builds to it. Two lines, one for a permissive system (TDE at its minimum) and one for a constraining system (TDE at its maximum), each tracing expected N as diversity rises. The shaded bands are the bootstrapped confidence intervals computed above.

The hypothesis is not about either line on its own — it is about the difference between their slopes.

# pdf('~/Downloads/Plots/enp_hat.pdf', width = 8)

# Sets the layout to a single panel and adjusts plot margins for axis labels and clarity.
# layout() divides the drawing device into panels; a 1x1 matrix means one panel.
layout(matrix(c(1), ncol = 1))
par(mar = c(6, 6, 0, 1))

# Panel (a): Low TDE (Permissive System)
# Minimun
plot(
  x = tot_divValues,
  y = outTDEMin[, 1], type = "n", ylim = c(0, 10), axes = FALSE, xlim = c(0, 1.05),
  xlab = "", ylab = "", cex.lab = 2
)
# Adds axis labels for the Y and X axes, respectively.
mtext("Expected Effective Number of Parties", side = 2, line = 3, cex = 2)
mtext("Constituency Diversity", side = 1, line = 4, cex = 2)
# Draws the X and Y axes with enlarged text and rotated Y-axis labels.
axis(1, cex.axis = 2.5, at = round(seq(min(tempData$tot_div), max(tempData$tot_div), length.out = 5), 3))
axis(2, cex.axis = 2.5, las = 2)
# Fills in the 95% confidence interval around the predicted ENP curve
polygon(
  x = c(tot_divValues, rev(tot_divValues)),
  y = c(outTDEMin[, "ub95"], rev(outTDEMin[, "lb95"])), col = adjustcolor("grey", alpha.f = 0.95), border = NA
)
# Overlays the predicted ENP line in white for visual contrast over the shaded area.
lines(x = tot_divValues, y = outTDEMin[, 1], lty = 1, col = "white", lwd = 1)

# Panel (b): High TDE (Constraining System)
# Maximum

plot(
  x = tot_divValues,
  y = outTDEMax[, 1], type = "n", ylim = c(0, 10), axes = FALSE, xlim = c(0, 1.05),
  xlab = "", ylab = "", cex.lab = 2
)
# Repeats axis labeling.
mtext("Expected Effective Number of Parties", side = 2, line = 3, cex = 2)
mtext("Constituency Diversity", side = 1, line = 4, cex = 2)
# Draws axes with consistent formatting.
axis(1, cex.axis = 2.5, at = round(seq(min(tempData$tot_div), max(tempData$tot_div), length.out = 5), 3))
axis(2, cex.axis = 2.5, las = 2)
# Fills the confidence interval ribbon for the high-TDE prediction line.
polygon(
  x = c(tot_divValues, rev(tot_divValues)),
  y = c(outTDEMax[, "ub95"], rev(outTDEMax[, "lb95"])), col = adjustcolor("grey", alpha.f = 0.95), border = NA
)
# Adds the predicted ENP line for the high-TDE case.
lines(x = tot_divValues, y = outTDEMax[, 1], lty = 1, col = "white", lwd = 1)

# dev.off()

Figure 7.2 offers an intuitive demonstration of how permissive electoral systems (low TDE) enable constituency diversity to translate into larger party systems. Panel (a) shows that when TDE is at its minimum, increasing constituency diversity leads to a substantial rise in the expected number of parties—an increase of about 2.25 across the observed range. Panel (b) reveals that when TDE is high (constraining systems), diversity has little to no effect on party system size; the slope is flat and statistically insignificant. These findings support the theoretical expectation that constraining electoral incentives limit the ability of social heterogeneity to produce party system fragmentation, while permissive systems allow diversity to manifest in more parties.

Figure 7.3: Expected N Conditional on Diversity—District Level

How to read Figures 7.2 and 7.3. They show the same interaction from two directions, and the pair is more persuasive than either alone.

In 7.2, diversity runs along the horizontal axis and each line is a level of TDE. The claim is that the permissive line climbs steeply — diversity turns into more parties when the rules allow it — while the constraining line stays comparatively flat. Converging or diverging lines are the interaction; two parallel lines would mean the rules do not condition the effect of diversity.

In 7.3, the axes swap: TDE runs along the horizontal axis and each line is a level of diversity. Here the claim is that both lines slope downward, but the high-diversity line falls more steeply, because constraining rules have more to suppress where the electorate is heterogeneous.

In both, check whether the confidence bands overlap. Where they separate, the difference between the two scenarios is distinguishable from noise; where they overlap, it is not, however suggestive the gap between the lines looks.

This block produces Figure 7.3, which complements the previous figure by reversing the simulation design: rather than fixing TDE and varying constituency diversity, it fixes diversity at low and high levels and varies TDE across its observed range to observe its effect on party system size.

# pdf('~/Downloads/Plots/enp_hat_varyingTDE.pdf', width = 8)

# Sets a one-panel layout and defines margins for plot clarity.
layout(matrix(c(1), ncol = 1))
par(mar = c(6, 6, 0, 1))

# Panel (a): Low Diversity – Varying TDE
# Minimum
plot(
  x = tdeValues,
  y = outTotDivMin[, 1], type = "n", ylim = c(0, 10), axes = FALSE, xlim = c(min(all_TDE_AP_dist$totalEff.hat), max(all_TDE_AP_dist$totalEff.hat)),
  xlab = "", ylab = "", cex.lab = 2
)
# Adds axis labels
mtext("Expected Effective Number of Parties", side = 2, line = 3, cex = 2)
mtext("TDE", side = 1, line = 4, cex = 2)
# Draws x- and y-axes with large tick labels
axis(1, cex.axis = 2.5, at = round(seq(min(all_TDE_AP_dist$totalEff.hat), max(all_TDE_AP_dist$totalEff.hat), length.out = 5), 3))
axis(2, cex.axis = 2.5, las = 2)
# Fills the 95% confidence interval ribbon for low diversity
polygon(
  x = c(tdeValues, rev(tdeValues)),
  y = c(outTotDivMin[, "ub95"], rev(outTotDivMin[, "lb95"])), col = adjustcolor("grey", alpha.f = 0.95), border = NA
)
# Draws the predicted ENP line (white) over the shaded area for visual clarity
lines(x = tdeValues, y = outTotDivMin[, 1], lty = 1, col = "white", lwd = 1)

# Panel (b): High Diversity – Varying TDE
# Maximum
plot(
  x = tdeValues,
  y = outTotDivMax[, 1], type = "n", ylim = c(0, 10), axes = FALSE, xlim = c(min(all_TDE_AP_dist$totalEff.hat), max(all_TDE_AP_dist$totalEff.hat)),
  xlab = "", ylab = "", cex.lab = 2
)
# Labels the axes again
mtext("Expected Effective Number of Parties", side = 2, line = 3, cex = 2)
mtext("TDE", side = 1, line = 4, cex = 2)
# Draws tick marks
axis(1, cex.axis = 2.5, at = round(seq(min(all_TDE_AP_dist$totalEff.hat), max(all_TDE_AP_dist$totalEff.hat), length.out = 5), 3))
axis(2, cex.axis = 2.5, las = 2)
# Plots confidence interval and the prediction line for high diversity
polygon(
  x = c(tdeValues, rev(tdeValues)),
  y = c(outTotDivMax[, "ub95"], rev(outTotDivMax[, "lb95"])), col = adjustcolor("grey", alpha.f = 0.95), border = NA
)
lines(x = tdeValues, y = outTotDivMax[, 1], lty = 1, col = "white", lwd = 1)

# dev.off()

Figure 7.3 provides an alternative visualization of the interplay between electoral system incentives and constituency diversity in shaping the effective number of parties. Panel (a) fixes constituency diversity at its minimum and shows a gradual decline in expected party system size as TDE increases, indicating stronger constraints reduce fragmentation even in homogeneous districts. Panel (b), with diversity fixed at its maximum, demonstrates a much sharper decline in party system size with increasing TDE, highlighting that constraining electoral incentives are especially effective at limiting party fragmentation in diverse constituencies. These patterns reinforce the idea that electoral rules not only impact party system size directly but also moderate the influence of social heterogeneity on political fragmentation.

Scenarios

These simulation results are used in the textual interpretation of Figure 7.3 and help anchor the interaction effects in a concrete, real-world context (Switzerland 2007), where both institutional and social heterogeneity are pronounced.

From lines to numbers. The figures show the shape of the interaction; the chapter text needs specific quantities to quote. This block calls the same bootstrap function at chosen combinations of TDE and diversity, using Switzerland 2007 — the case with the widest district-level spread in Figure 7.1 — to make the effect concrete.

This is the slowest block in the chapter: 500 bootstrap replications for every scenario. Nothing is stored between knits, so the quantities quoted below are always computed from the code exactly as it currently stands.

# Name the two blocks of prediction columns, and use these names everywhere
# below instead of column numbers. Each block holds five values -- one per
# imputation -- so rowSums(...)/5 averages across them.
#   ap_cols   pers.hat1     ... pers.hat5      (Average Personalism)
#   tde_cols  totalEff.hat1 ... totalEff.hat5  (Total Duvergerian Effect)
# Selecting by name rather than by position means the code cannot quietly
# operate on the wrong variables if the column order of the data ever changes.
ap_cols <- paste0("pers.hat", 1:5)
tde_cols <- paste0("totalEff.hat", 1:5)

# Find the range in Diversity and TDE

# Calculates the range of Constituency Diversity and TDE for each country-year using district-level data
div <- by(tempData$tot_div, tempData$ctyyear, function(x) max(x) - min(x), simplify = TRUE)
tde <- by(rowSums(tempData[, tde_cols]) / 5, tempData$ctyyear, function(x) max(x) - min(x), simplify = TRUE)
div <- array2DF(div, simplify = TRUE)
# The TDE value is averaged across the five imputations before computing the range.
tde <- array2DF(tde, simplify = TRUE)
# Converts the summary statistics into data frames and merges them for joint inspection.
div_tde <- merge(div, tde, by = "tempData$ctyyear")

# plot to find the observation at the top of the right-top corner (most illustrative cases)
plot(div_tde$Value.x, div_tde$Value.y, type = "n")
text(x = div_tde$Value.x, y = div_tde$Value.y, labels = div_tde$"tempData$ctyyear")

### We will use Switzerland-2007

# Vary diversity and fix TDE

# Filters the data to only include districts from Switzerland 2007.
swissData <- tempData[tempData$ctyyear == "Switzerland-2007", ]

# Identifies the minimum and maximum diversity values.
swissDataMin <- min(swissData$tot_div) # Min diversity
swissDataMax <- max(swissData$tot_div) # Max diversity

# Sets Average Personalism (AP) to its mean value for prediction consistency.
swissData[1, ap_cols] <- mean(rowSums(swissData[, ap_cols]) / 5) # Replace AP with its mean value

# Calculates the average TDE across imputations.
avgTDESwitzerland <- mean(rowSums(swissData[, tde_cols]) / 5) # Avg TDE for Switzerland

# Computes expected ENP for the least and most diverse Swiss districts, holding TDE and AP constant.
switzerlandDivMin <- boot.predict.se.enp(
  tdeValue = avgTDESwitzerland, tot_divValue =
    swissDataMin, reps = 500, seed = 123354,
  allCountries = FALSE, countryData = swissData[1, ]
)
switzerlandDivMax <- boot.predict.se.enp(
  tdeValue = avgTDESwitzerland, tot_divValue =
    swissDataMax, reps = 500, seed = 123354,
  allCountries = FALSE, countryData = swissData[1, ]
)
switzerlandDivMin
## Point Estimate     Std. Error           lb95           ub95           lb90 
##         3.9800         0.0527         3.8767         4.0832         3.8933 
##           ub90 
##         4.0666
switzerlandDivMax
## Point Estimate     Std. Error           lb95           ub95           lb90 
##         4.2841         0.0565         4.1734         4.3949         4.1912 
##           ub90 
##         4.3771
# Varying TDE with Fixed Diversity
swissData <- tempData[tempData$ctyyear == "Switzerland-2007", ]
# Identifies minimum and maximum TDE across Swiss districts.
swissDataMin <- min(rowSums(swissData[, tde_cols]) / 5) # Min TDE
swissDataMax <- max(rowSums(swissData[, tde_cols]) / 5) # Max TDE

# Fixes AP again at the mean.
swissData[1, ap_cols] <- mean(rowSums(swissData[, ap_cols]) / 5) # Replace AP with its mean value
# Fixes diversity at its average value across districts.
avgDivSwiss <- mean(swissData$tot_div[swissData$ctyyear == "Switzerland-2007"]) # Avg Diversity for Switzerland

# Computes expected ENP for districts with weakest and strongest electoral incentives, holding diversity and AP constant.
swissDivMin <- boot.predict.se.enp(
  tdeValue = swissDataMin, tot_divValue = avgDivSwiss,
  reps = 500, seed = 123354,
  allCountries = FALSE, countryData = swissData[1, ]
)
swissDivMax <- boot.predict.se.enp(
  tdeValue = swissDataMax, tot_divValue = avgDivSwiss,
  reps = 500, seed = 123354,
  allCountries = FALSE, countryData = swissData[1, ]
)

swissDivMin
## Point Estimate     Std. Error           lb95           ub95           lb90 
##         5.4360         0.0883         5.2628         5.6091         5.2907 
##           ub90 
##         5.5813
swissDivMax
## Point Estimate     Std. Error           lb95           ub95           lb90 
##         2.3815         0.0798         2.2252         2.5379         2.2503 
##           ub90 
##         2.5127

To further illustrate the interaction between electoral system incentives and social diversity, we calculate expected values for a real-world case—Switzerland in 2007, which exhibits the greatest within-election variation in both constituency diversity and TDE. Holding TDE and AP constant at their average observed values, the expected effective number of parties increases from 4.08 to 4.40 when moving from the least to the most diverse Swiss districts. Conversely, when holding diversity constant at its mean and varying TDE from its minimum to maximum observed values, the expected number of parties drops sharply from 5.29 to 2.85. These results reinforce the main finding of the chapter: permissive electoral systems allow social heterogeneity to translate into party system fragmentation, whereas constraining rules substantially limit the number of viable parties, even in diverse settings

Figure 7.4: Expected N Conditional on Diversity—Select Systems

This block generates Figure 7.4, which simulates the expected size of the party system across levels of constituency diversity for three variations of a closed-list proportional representation (PR) system near the institutional “sweet spot” described by Carey and Hix (2011).

Where allCountries = FALSE finally matters. Figures 7.2 and 7.3 averaged over every district in the data. Here the function is called with a specific system passed through countryData, so the expected values describe three concrete variants of closed-list PR rather than the world average.

This is the chapter’s engagement with Carey and Hix’s “sweet spot” — the claim that moderate district magnitudes deliver the best balance of representativeness and accountability. The three variants sit near that spot, and the figure asks whether they behave alike once constituency diversity is taken into account.

# Creates a data frame representing two versions of a “typical” closed-list PR system: one with low district magnitude (M = 3) and one with moderate magnitude (M = 8)
xTDE <- data.frame(
  formula = "dhondt",
  ballot_type = "closed",
  new.nvotes = "One",
  pool_level = "party",
  threshold = 0.025,
  M = c(3, 8)
)

# Ensure variables are treated as categorical
xTDE$formula <- as.factor(xTDE$formula)
xTDE$ballot_type <- as.factor(xTDE$ballot_type)
xTDE$new.nvotes <- as.factor(xTDE$new.nvotes)
xTDE$pool_level <- as.factor(xTDE$pool_level)

# # Get TDE predictions for each of the five imputed GBM models, these create lists
tde_hat1 <- predict(optimalGBMInter[[1]], xTDE, n.trees = optimalGBMInter[[1]]$n.trees)
tde_hat2 <- predict(optimalGBMInter[[2]], xTDE, n.trees = optimalGBMInter[[2]]$n.trees)
tde_hat3 <- predict(optimalGBMInter[[3]], xTDE, n.trees = optimalGBMInter[[3]]$n.trees)
tde_hat4 <- predict(optimalGBMInter[[4]], xTDE, n.trees = optimalGBMInter[[4]]$n.trees)
tde_hat5 <- predict(optimalGBMInter[[5]], xTDE, n.trees = optimalGBMInter[[5]]$n.trees)

# Take the average TDE across the five models
xTDE$tde <- colMeans(rbind(tde_hat1, tde_hat2, tde_hat3, tde_hat4, tde_hat5))

# Simulate ENP for average TDE, upper bound, and lower bound of “sweet spot”
# Calculate expected values
outAvg <- list()
outUB <- list() # outside the sweet spot, upper bound
outLB <- list() # outside the sweet spot, lower bound
for (i in 1:length(tot_divValues)) {
  # Average TDE from M = 3 & 8 systems
  outAvg[[i]] <- boot.predict.se.enp(tdeValue = mean(xTDE$tde), tot_divValue = tot_divValues[i], reps = 500, seed = 123354, allCountries = TRUE)
  # Upper bound TDE = 1.56 (M = 3, more constraining)
  outUB[[i]] <- boot.predict.se.enp(tdeValue = 1.56, tot_divValue = tot_divValues[i], reps = 500, seed = 123354, allCountries = TRUE)
  # Lower bound TDE = 1.21 (M = 8, more permissive)
  outLB[[i]] <- boot.predict.se.enp(tdeValue = 1.21, tot_divValue = tot_divValues[i], reps = 500, seed = 123354, allCountries = TRUE)
}

# Combine the results into data frames
outAvg <- do.call("rbind", outAvg)
outUB <- do.call("rbind", outUB)
outLB <- do.call("rbind", outLB)

# Start the plot
# pdf('~/Downloads//Plots/TDE_M3_8.pdf', width = 8)
layout(matrix(c(1), ncol = 1))
par(mar = c(6, 6, 0, 0))

# Creates an empty plot frame with the X-axis representing Constituency Diversity, and the Y-axis showing expected ENP (ranging from 2 to 6).
plot(
  x = tot_divValues,
  y = outAvg[, 1], type = "n", ylim = c(2, 6), axes = FALSE,
  xlab = "", ylab = "", cex.lab = 2
)
# Adds axis labels and tick marks.
mtext("Expected Effective Number of Parties", side = 2, line = 3, cex = 2)
mtext("Constituency Diversity", side = 1, line = 4, cex = 2)
axis(1, cex.axis = 2.5)
axis(2, cex.axis = 2.5, las = 2, at = c(2, 3, 4, 5, 6))
# Plots the expected ENP line for the “average” TDE case and shades the confidence interval ribbon.
polygon(
  x = c(tot_divValues, rev(tot_divValues)),
  y = c(outAvg[, 3], rev(outAvg[, 4])), col = adjustcolor("grey", alpha.f = 0.95), border = NA
)
lines(x = tot_divValues, y = outAvg[, 1], lty = 1, col = "white", lwd = 1)

# dev.off()

Figure 7.4 illustrates the expected effective number of parties across different levels of constituency diversity within systems characterized by low district magnitude (M = 3 or 8), closed-list PR, the D’Hondt seat allocation formula, and a 2.5% legal threshold—parameters that define the “sweet spot.” Based on model 8, predicted party system sizes range from about 3.1 to 4.6 parties, increasing as constituency diversity rises. This overlap in predicted party system sizes underscores that even with similar institutional designs, social heterogeneity critically shapes political representation. The figure refines the Carey and Hix argument by showing that achieving the balance between accountability and representation depends not only on institutional features but also on the diversity of the electorate.

How to read Figure 7.4. Three lines, one per variant of closed-list PR, each tracing expected N across constituency diversity. Unlike Figures 7.2 and 7.3, the lines here differ in their institutional details rather than in an imposed TDE value.

If systems sitting close together in the “sweet spot” nonetheless produce visibly different lines, the lesson is that a single summary of an electoral system — even a well-chosen one like district magnitude — is not enough to predict party system size once the social context is allowed to vary. That is the chapter’s qualification of Carey and Hix.

Moving Forward

In this chapter, we have revisited the foundational question of how electoral system incentives shape the size of party systems. Using the Total Duvergerian Effect (TDE), which integrates the combined influence of multiple electoral rules, we have shown that electoral systems vary in their constraining or permissive character. Systems with high TDE—characterized by low district magnitude, disproportional seat allocation formulas, and significant legal thresholds—tend to produce smaller party systems by discouraging excess party entry and encouraging strategic coordination among voters. Conversely, systems with low TDE allow for larger and more fragmented party systems, especially in constituencies with high social diversity.

Our analysis also highlights the critical interaction between electoral system design and the social heterogeneity of constituencies. The same electoral rules can yield different party system sizes depending on the degree of diversity among voters. This interaction nuances the argument by Carey and Hix (2011) about the “sweet spot” of electoral systems, revealing that representativeness and accountability depend not only on institutional design but also on the social context. These findings underscore the complexity of electoral incentives and caution against simplistic expectations about party system size based on single rules such as district magnitude alone. They encourage a more holistic view that incorporates multiple interacting electoral rules and constituency characteristics.

Looking ahead, the next chapter shifts focus from the quantity of parties to their qualitative positioning. Understanding how electoral systems influence the distribution of parties across the ideological spectrum is vital for grasping the nature of political competition and representation. By examining party system dispersion and the ideological locations of parties, we aim to deepen our understanding of how electoral incentives shape not only how many parties exist but also how they compete and appeal to voters.

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
stringr 1.5.1
tibble 3.3.0
tidyr 1.3.1
tidyverse 2.0.0

The environment used for the published book

The results printed in the book were computed earlier, on a different setup. 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.

Note that this page is not that run. The column-indexing correction described earlier in the chapter postdates it, so Figures 7.2 to 7.4 here differ from the ones that run produced, and from the ones in the printed book.

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