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 9, 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 9 examines whether electoral systems affect the degree of correspondence between citizens’ preferences — weighted by their number — and parties’ positions — weighted by their size. It asks whether a system’s Total Duvergerian Effect (TDE) is related to ideological congruence between citizens and politicians. Using data from Latin America on how citizens’ preferences and politicians’ preferences are distributed, we conclude that systems with a high TDE tend to be associated with lower levels of many-to-many congruence.

Two kinds of congruence, and why it matters which. The chapter distinguishes them, and Figure 9.1 is devoted to the difference.

  • One-to-one congruence compares a single summary of citizens — the median voter — with a single summary of politicians. It asks whether the centre of one matches the centre of the other.
  • Many-to-many congruence compares the whole distribution of citizen preferences with the whole distribution of party positions. Two systems can have identical medians while one reproduces the shape of public opinion far better than the other.

The chapter’s finding concerns many-to-many congruence, which is why Tables 9.1 and 9.2 report the two separately and why the expected-value figures are built from the many-to-many models.

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

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

Setup

Introduction

Chapter 9 explores the concept of ideological congruence—the degree to which elected representatives’ policy preferences align with those of the citizens they represent—and examines how different electoral systems influence this alignment. Building on the previous chapters that analyzed the size and ideological distribution of parties, this chapter focuses on whether electoral systems facilitate or hinder congruence between voters and politicians, measured at both the individual and aggregate levels.

The chapter investigates two distinct types of congruence: one-to-one congruence, comparing median citizen and median legislator preferences, and many-to-many congruence, assessing how the overall distribution of citizen preferences matches that of elected officials. Using survey data from Latin America and the project’s Total Duvergerian Effect (TDE) scores, the analysis tests how constraining versus permissive electoral systems affect these patterns of congruence. Results suggest that highly constraining systems tend to reduce many-to-many congruence, potentially limiting representativeness.

This file contains calls to a smaller dataset used in Chapter 9 (Congruence), as well as the code necessary to produce all graphs in the chapter.

Data processing

This code prepares the main dataset (MoodData) for use in Chapter 9 by aligning survey years with corresponding election years, which are essential for accurately merging institutional indicators such as the Total Duvergerian Effect (TDE). In some cases, surveys were conducted between elections, so the code manually adjusts the year_election variable to reference the most recent previous election. It then merges this harmonized dataset with external institutional data (data2export) using matched country-election year pairs.

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

# Function to extract estimates for table
extract_Estimates <- function(model_name) {
  out <- ifelse(model_name$p.value < 0.01, paste0(round(model_name$estimate, 3), "***", " (", round(model_name$std.error, 3), ")"),
    ifelse(model_name$p.value < 0.05, paste0(round(model_name$estimate, 3), "**", " (", round(model_name$std.error, 3), ")"),
      ifelse(model_name$p.value < 0.1, paste0(round(model_name$estimate, 3), "*", " (", round(model_name$std.error, 3), ")"),
        paste0(round(model_name$estimate, 3), " (", round(model_name$std.error, 3), ")")
      )
    )
  )
  names(out) <- model_name$term
  out <- data.frame(out)
  out$variable <- row.names(out)
  return(out)
}

Rubin’s rules, written out by hand. Other chapters call mice::pool() to combine results across the five imputed datasets. This chapter implements the same arithmetic explicitly, because it needs the pooled variance-covariance matrix — not just the standard errors — to simulate the confidence bands in Figure 9.5.

The logic is the familiar two-part decomposition. omegacl is the average uncertainty within imputations; uppsi measures how far the five sets of coefficients sit from their common average, i.e. the extra uncertainty between imputations. Adding them, with a small-sample correction, gives total uncertainty.

rubin_out <- function(model.list) {
  # Calculate Beta Bar (the coefficients)
  # lapply(model.list, coef) extracts the coefficient vector from each of the
  # five fitted models; Reduce("+", ...) adds those vectors together; dividing
  # by the count gives their average. This is Rubin's first rule.
  betabar <- Reduce("+", lapply(model.list, coef)) / length(model.list)
  # Calculate Uppsi. It is used in the calculations of SE's below.
  # BETWEEN-imputation variance. For each model, the deviation of its
  # coefficients from the average is multiplied by its own transpose --
  # v %*% t(v) turns a vector into a matrix -- and the results are averaged.
  # Dividing by (n-1) rather than n is the usual small-sample correction.
  uppsi <- Reduce("+", lapply(model.list, function(x) (coef(x) - betabar) %*% t(coef(x) - betabar))) / (length(model.list) - 1)
  # WITHIN-imputation variance: the average of the five models' own
  # variance-covariance matrices.
  omegacl <- Reduce("+", lapply(model.list, function(x) vcov(x))) / length(model.list)
  # Calculate the final variance
  # Total = within + between + a correction for having only five imputations.
  varR <- omegacl + as.numeric(uppsi) + as.numeric(uppsi) / length(model.list)
  # Return the results
  # A list with two components. Returning both is what lets Figure 9.5 draw
  # correlated simulations from the pooled distribution.
  return(list(betas = betabar, vcov = varR))
}

Figures 9.1 and 9.2 are illustrations, not results. They draw constructed examples to define the concepts before any data are analysed: 9.1 shows what one-to-one and many-to-many congruence each capture, and 9.2 shows the “bridging” problem — how citizen and politician surveys, asked on different scales, can be placed on a common one. The numbers in these blocks are chosen to make the point, not estimated.

Figure 9.1: Two Types of Congruence

This figure offers a stylized illustration of the two main concepts of ideological congruence discussed in Chapter 9. It contrasts many-to-many congruence, which concerns the similarity between the full distributions of ideological preferences of citizens and legislators, and one-to-one congruence, which refers to the alignment of the median citizen’s ideal point with that of the median legislator. The plot simplifies complex distributions to visually emphasize how one form of congruence can exist without the other.

# Set up plot margins and text rotation
par(mar = c(0, 0, 0, 0), srt = 0)

# Create an empty plot area without axes
plot(c(0, 6), c(0, 1), type = "n", axes = F, ylab = "", xlab = "")

# Draw horizontal base lines for legislators (top) and citizens (bottom)
segments(y0 = 0.53, y1 = 0.53, x0 = 0, x1 = 6, lwd = 5)
segments(y0 = 0.03, y1 = 0.03, x0 = 0, x1 = 6, lwd = 5)

# Create sequence of x-values for plotting densities
x <- seq(from = 0, to = 6, length.out = 100)

# Plot citizen distribution: normal centered at 3
points(xy.coords(x, dnorm(x, mean = 3, sd = 1) + 0.03), type = "l", lwd = 3)

# Plot legislator distribution: gamma skewed left
points(xy.coords(x, dgamma(x, shape = 2, rate = 1) + 0.53), type = "l", lwd = 3)

# Add labels for the two distributions
text(x = 4.7, y = 0.65, label = "Legislators", cex = 2, pos = 4)
text(x = 4.7, y = 0.15, label = "Citizens", cex = 2, pos = 4)

# Add black dot for median legislator
points(xy.coords(1.4, 0.53), pch = 19, col = "black", cex = 4)

# Add black dot for median citizen
points(xy.coords(3, 0.03), pch = 19, col = "black", cex = 4)

# Save the figure as a PDF
invisible(dev.copy(pdf, "congruence_explained.pdf", h = 7, w = 12))
invisible(dev.off())

Figure 9.1 illustrates the conceptual distinction between one-to-one and many-to-many ideological congruence. The lower panel represents citizens’ ideological preferences, normally distributed around the center, while the upper panel shows legislators’ preferences, skewed to the left. The median citizen and median legislator are highlighted with black dots. Despite their different overall distributions, the figure shows that one-to-one congruence may still be high (if median positions are aligned), even when many-to-many congruence is low (as the overall shapes of the distributions diverge). This visual underscores the chapter’s argument that different forms of congruence reflect different normative ideals—and that electoral systems may perform well on one dimension while failing on the other.

Figure 9.2: A Representation of “Bridging” Responses

This figure visually explains the bridging problem in measuring ideological congruence. Specifically, it shows how inferred ideological scores can appear aligned even when they are constructed from entirely separate sets of policy positions—an artifact that may distort measures of congruence if not addressed through proper scale-bridging.

par(mar = c(0, 0, 0, 0), srt = 0) # Remove all margins and set text rotation to 0

plot(c(0, 1), c(0, 1), type = "n", axes = F, ylab = "", xlab = "") # Create empty plot space

segments(y0 = 0.53, y1 = 0.53, x0 = 0, x1 = 1, lwd = 5) # Draw thick top axis line (citizen axis)
segments(y0 = 0.47, y1 = 0.47, x0 = 0, x1 = 1, lwd = 5) # Draw thick bottom axis line (politician axis)

text(x = 0.01, y = 0.42, label = "State", cex = 1.3) # Label left end of ideological spectrum
text(x = 0.99, y = 0.42, label = "Market", cex = 1.3) # Label right end of ideological spectrum

points(xy.coords(0.51, 0.53), pch = 19, col = "black", cex = 4) # Black dot for citizen's inferred ideal point
points(xy.coords(0.49, 0.47), pch = 19, col = "black", cex = 4) # Black dot for politician's inferred ideal point

points(xy.coords(c(0.46, 0.51, 0.56), rep(0.7, 3)), pch = 19, cex = 3) # Citizen answers on items A, B, C
points(xy.coords(c(0.39, 0.59), rep(0.3, 2)), pch = 19, cex = 3) # Politician answers on items X and Y

segments(x0 = c(0.46, 0.51, 0.56), x1 = rep(0.51, 3), y0 = rep(0.7, 3), y1 = rep(0.53, 3)) # Connect citizen answers to ideal point
segments(x0 = c(0.39, 0.59), x1 = rep(0.49, 3), y0 = rep(0.3, 3), y1 = rep(0.47, 3)) # Connect politician answers to ideal point

par(srt = 90) # Rotate text vertically for top labels
text(x = c(0.47, 0.52, 0.57), y = rep(0.84, 3), labels = c("Position on policy A", "Position on policy B", "Position on policy C"), pos = 3)

par(srt = 270) # Rotate text vertically (other direction) for bottom labels
text(x = c(0.40, 0.60), y = rep(0.16, 2), labels = c("Position on policy X", "Position on policy Y"), pos = 1)

par(srt = 0) # Reset text rotation
text(x = 0.62, y = 0.56, pos = 3, adj = 0, labels = "Citizen", cex = 1.4) # Label citizen
text(x = 0.6, y = 0.44, pos = 1, adj = 0, labels = "Politician", cex = 1.4) # Label politician

invisible(dev.copy(pdf, "exampleBridgeFail.pdf", h = 7, w = 12)) # Save figure to PDF
invisible(dev.off())

Figure 9.2a exposes a common measurement challenge in representation studies. It shows that both citizens and politicians can appear ideologically aligned—even when their positions are inferred from completely separate sets of policy questions. In this stylized case, a citizen’s ideal point is calculated from items A, B, and C, while a politician’s is derived from X and Y. The alignment of their scores is therefore illusory: without a shared policy anchor, the scales are not commensurable. This figure illustrates why bridging—using common items to place both groups on the same ideological scale—is essential for valid comparisons of congruence.

Next is a graph that shows how the bridging problem can be circumvented:

Figure 9.2 (Panel b): Solving the Bridging Problem

This figure builds on the previous one by illustrating how researchers can bridge distinct survey instruments to compare the inferred ideological positions of citizens and politicians. The addition of a common policy item (Policy K) allows us to anchor both sets of responses to a shared scale, turning previously incomparable estimates into valid measures of ideological congruence.

# Set blank margins and reset text rotation
par(mar = c(0, 0, 0, 0), srt = 0)

# Initialize a blank plot with x and y ranging from 0 to 1, no axes or labels
plot(c(0, 1), c(0, 1), type = "n", axes = FALSE, ylab = "", xlab = "")

# Draw a single horizontal ideological axis for both citizens and politicians
segments(y0 = 0.5, y1 = 0.5, x0 = 0, x1 = 1, lwd = 5)

# Add ideological endpoint labels
text(x = 0.01, y = 0.45, label = "State", cex = 1.3)
text(x = 0.99, y = 0.45, label = "Market", cex = 1.3)

# Plot the inferred ideological position of the citizen (at x=0.3) and the politician (at x=0.5)
points(xy.coords(c(0.3, 0.5), c(0.5, 0.5)), pch = 19, col = "black", cex = 4)

# Plot the policy items A, B, C, and K used to estimate the citizen's position
points(xy.coords(c(0.15, 0.25, 0.35, 0.45), rep(0.7, 4)), pch = 19, cex = 3)

# Plot the policy items X, Y, and K used to estimate the politician’s position
points(xy.coords(c(0.45, 0.5, 0.55), rep(0.3, 3)), pch = 19, cex = 3)

# Draw lines connecting each policy item to the citizen’s inferred ideal point
segments(
  x0 = c(0.15, 0.25, 0.35, 0.45), x1 = rep(0.3, 4),
  y0 = rep(0.7, 4), y1 = rep(0.5, 4)
)

# Draw lines connecting each policy item to the politician’s inferred ideal point
segments(
  x0 = c(0.45, 0.5, 0.55), x1 = rep(0.5, 3),
  y0 = rep(0.3, 3), y1 = rep(0.5, 3)
)

# Rotate text vertically and label policy items for the citizen
par(srt = 90)
text(
  x = c(0.16, 0.26, 0.36, 0.46), y = rep(0.82, 4),
  labels = c("Position on policy A", "Position on policy B", "Position on policy C", "Position on policy K"), pos = 3
)

# Draw a dashed line showing how Policy K bridges both actors
segments(x0 = 0.45, x1 = 0.45, y0 = 0.7, y1 = 0.3, lty = 2)

# Label policy items for the politician (rotated downward)
par(srt = 270)
text(
  x = c(0.455, 0.505, 0.555), y = rep(0.18, 3),
  labels = c("Position on policy K", "Position on policy X", "Position on policy Y"), pos = 1
)

# Reset rotation and label actor names
par(srt = 0)
text(x = 0.5, y = 0.53, pos = 3, adj = 0, labels = "Politician", cex = 1.4)
text(x = 0.3, y = 0.47, pos = 1, adj = 0, labels = "Citizen", cex = 1.4)

# Export the figure to PDF
invisible(dev.copy(pdf, "exampleBridge.pdf", h = 7, w = 12))
invisible(dev.off())

Figure 9.2b demonstrates how the inclusion of a shared survey item (Policy K) allows us to bridge separate sets of ideological responses and place both citizens and politicians on a common ideological continuum. In contrast to Panel A—where alignment was an artifact—this configuration ensures that the observed distance between citizen and politician ideal points reflects genuine differences in policy preferences. The dotted line connecting the two instances of Policy K symbolizes its role as a bridging anchor, solving the problem of scale incommensurability and enabling meaningful comparison of ideological congruence across groups.

Following are the plots that depict two types of congruence: one-to-one and many-to-many

How to read Figure 9.3. Each point is one country-year, with one-to-one congruence on one axis and many-to-many on the other. The question is how far the two measures agree.

If they tracked each other closely, the chapter could use either and the distinction of Figure 9.1 would be academic. Scatter away from a tight line means they are capturing different things — systems that match the median voter well while reproducing the shape of opinion poorly, and vice versa. That is the justification for reporting Tables 9.1 and 9.2 separately rather than choosing one measure.

Figure 9.3: Relationship Between One-to-One and Many-to-Many Congruence

Figure 9.3 explores the relationship between one-to-one and many-to-many congruence across country–congressional term observations.

The left panel displays this relationship in low-TDE electoral systems, while the right panel shows high-TDE systems. Each point corresponds to a case, labeled with a country–year identifier. The trend lines illustrate how congruence types co-vary and how this association may be moderated by the structure of electoral incentives.

# Goes to chapter 9 Congruence
p <- ggplot(MoodData, mapping = aes(one2one, many2many.simul)) +
  geom_text(aes(label = shortID)) +
  geom_smooth(method = "loess") +
  xlab("One-to-one congruence") +
  ylab("Many-to-many congruence") +
  theme(
    axis.title.x = element_text(size = 18), # Increase x-axis title size
    axis.title.y = element_text(size = 18), # Increase y-axis title size
    axis.text.x = element_text(size = 16), # Increase x-axis text size
    axis.text.y = element_text(size = 16) # Increase y-axis text size
  )

p

invisible(dev.copy(pdf, "congruences.pdf", h = 7, w = 12))
invisible(dev.off())

This figure shows the relationship between one-to-one congruence (x-axis) and many-to-many congruence (y-axis) across country-year observations in Latin America. Each point represents a case labeled by its short code, while the LOESS curve captures the smoothed association. The upward slope indicates a positive correlation: systems that better align the median positions of citizens and legislators also tend to reflect the full distribution of citizen preferences more accurately.

p1 <- ggplot(subset(MoodData, totalEff.hat <= median(MoodData$totalEff.hat)), mapping = aes(one2one, many2many.simul)) +
  geom_text(aes(label = shortID)) +
  geom_smooth(method = "loess") +
  xlab("One-to-one congruence") +
  ylab("Many-to-many congruence") +
  xlim(0.3, 1) +
  ylim(0.2, 1.2) +
  ggtitle("Low TDE") +
  theme(
    axis.title.x = element_text(size = 18), # Increase x-axis title size
    axis.title.y = element_text(size = 18), # Increase y-axis title size
    axis.text.x = element_text(size = 16), # Increase x-axis text size
    axis.text.y = element_text(size = 16) # Increase y-axis text size
  )


p2 <- ggplot(subset(MoodData, totalEff.hat > median(MoodData$totalEff.hat)), mapping = aes(one2one, many2many.simul)) +
  geom_text(aes(label = shortID)) +
  geom_smooth(method = "loess") +
  xlab("One-to-one congruence") +
  ylab("Many-to-many congruence") +
  xlim(0.3, 1) +
  ylim(0.2, 1.2) +
  ggtitle("High TDE") +
  theme(
    axis.title.x = element_text(size = 18), # Increase x-axis title size
    axis.title.y = element_text(size = 18), # Increase y-axis title size
    axis.text.x = element_text(size = 16), # Increase x-axis text size
    axis.text.y = element_text(size = 16) # Increase y-axis text size
  )


grid.arrange(p1, p2, ncol = 2)

invisible(dev.copy(pdf, "congruences_sidebyside.pdf", h = 7, w = 12))
invisible(dev.off())

Figure 9.3 shows that one-to-one and many-to-many congruence are positively associated, but the strength of this relationship varies with the restrictiveness of the electoral system. In low-TDE systems (left panel), the association is tighter and more consistent, suggesting that permissive rules not only align median preferences but also better capture the full ideological distribution of citizens. In contrast, high-TDE systems (right panel) exhibit greater dispersion: even when median congruence is high, many-to-many congruence remains uneven. This supports the idea that restrictive systems may deliver median alignment without truly reflecting the diversity of citizen preferences.

Models

The following code estimates a series of linear models to assess how the Total Duvergerian Effect (TDE) and Average Personalism (AP) influence different forms of ideological congruence. The models use five alternative operationalizations of both TDE and AP, stored in a list of datasets. Each block estimates three specifications: (1) including only TDE and economic controls (GDP growth and inflation), (2) adding year fixed effects, and (3) further including personalism. The first set of models uses contemporaneous values of congruence, while the second set uses lagged citizen preferences. All models are estimated with heteroskedasticity-robust standard errors using feols from the fixest package.

Create 5 small datasets

# Define variable names for five different versions of TDE and AP
varsTDE <- c("totalEff.hat1", "totalEff.hat2", "totalEff.hat3", "totalEff.hat4", "totalEff.hat5")
varsAP <- c("pers.hat1", "pers.hat2", "pers.hat3", "pers.hat4", "pers.hat5")

# Initialize an empty list to store each of the five reduced datasets
MoodDataList <- list()

# Loop over the five versions and create a simplified dataset for each
for (i in 1:5) {
  # Subset relevant variables: the i-th TDE and AP, five outcome variables, and covariates
  MoodDataList[[i]] <- subset(MoodData, select = c(
    varsTDE[i], varsAP[i],
    "one2one.const", "one2one.const.lag", "many2many.const",
    "many2many.const.lag", "change.gdppc", "inflation", "year"
  ))

  # Rename TDE and AP columns for consistency across datasets
  names(MoodDataList[[i]])[1:2] <- c("TDE", "AP")
}

We estimate three models for one-to-one convergence: using TDE plus change in GDP per capita and inflation, then adding year fixed effects, and lastly adding AP. These models do not have lagged terms:

# Initialize empty lists for the three model specifications
model1 <- model2 <- model3 <- list()

# Estimate three models for each dataset (five versions of TDE and AP)
for (i in 1:5) {
  # Model 1: TDE + GDP + inflation (no fixed effects)
  model1[[i]] <- feols(one2one.const ~ TDE + change.gdppc + inflation, se = "hetero", data = MoodDataList[[i]])

  # Model 2: Add year fixed effects
  model2[[i]] <- feols(one2one.const ~ TDE + change.gdppc + inflation | year, se = "hetero", data = MoodDataList[[i]])

  # Model 3: Add AP as predictor (with year fixed effects)
  model3[[i]] <- feols(one2one.const ~ TDE + AP + change.gdppc + inflation | year, se = "hetero", data = MoodDataList[[i]])
}

We now estimate these same models for one-to-one convergence, but using lagged terms:

# Initialize empty lists for the lagged model specifications
model1b <- model2b <- model3b <- list()

for (i in 1:5) {
  # Model 1b: Lagged congruence regressed on TDE + GDP + inflation
  model1b[[i]] <- feols(one2one.const.lag ~ TDE + change.gdppc + inflation, se = "hetero", data = MoodDataList[[i]])

  # Model 2b: Add year fixed effects
  model2b[[i]] <- feols(one2one.const.lag ~ TDE + change.gdppc + inflation | year, se = "hetero", data = MoodDataList[[i]])

  # Model 3b: Add AP as covariate (with year fixed effects)
  model3b[[i]] <- feols(one2one.const.lag ~ TDE + AP + change.gdppc + inflation | year, se = "hetero", data = MoodDataList[[i]])
}

We will estimate the same three specifications for many-to-many convergence: using TDE exclusively, adding economic controls and fixed effects, and then adding econ controls + fixed effects + AP. These ones do not have lagged terms:

# Initialize lists to store model outputs for the three specifications
model4 <- model5 <- model6 <- list()

# Loop over the five TDE/AP datasets
for (i in 1:5) {
  # Model 4: Regress many-to-many congruence on TDE, GDP growth, and inflation
  model4[[i]] <- feols(many2many.const ~ TDE + change.gdppc + inflation, se = "hetero", data = MoodDataList[[i]])

  # Model 5: Add year fixed effects to the model
  model5[[i]] <- feols(many2many.const ~ TDE + change.gdppc + inflation | year, se = "hetero", data = MoodDataList[[i]])

  # Model 6: Add Average Personalism (AP) as an additional covariate
  model6[[i]] <- feols(many2many.const ~ TDE + AP + change.gdppc + inflation | year, se = "hetero", data = MoodDataList[[i]])
}

Finally, the same three specifications for many-to-many convergence with lagged values of predictors:

# Initialize lists to store lagged model outputs
model4b <- model5b <- model6b <- list()

# Loop over the five TDE/AP datasets
for (i in 1:5) {
  # Model 4b: Lagged congruence ~ TDE + economic controls
  model4b[[i]] <- feols(many2many.const.lag ~ TDE + change.gdppc + inflation, se = "hetero", data = MoodDataList[[i]])

  # Model 5b: Add year fixed effects to the lagged model
  model5b[[i]] <- feols(many2many.const.lag ~ TDE + change.gdppc + inflation | year, se = "hetero", data = MoodDataList[[i]])

  # Model 6b: Add AP to the full lagged specification
  model6b[[i]] <- feols(many2many.const.lag ~ TDE + AP + change.gdppc + inflation | year, se = "hetero", data = MoodDataList[[i]])
}

The next code snippet brings together five versions of each model to present a single set of coefficients per model.

Four model families, then two tables. The four modelling blocks above pair up along two dimensions: which congruence measure is the outcome (one-to-one or many-to-many), and whether the specification is contemporaneous or lagged. The lagged versions guard against the possibility that congruence and the electoral system are being measured at the same moment for reasons of coincidence rather than cause.

Table 9.1 collects the one-to-one results, Table 9.2 the many-to-many ones. The chapter’s finding lives in Table 9.2.

Table 9.1: TDE as Predictor of One-to-One Congruence

This code block constructs Table 9.1, which summarizes the results of six linear models estimating the effect of electoral system restrictiveness—measured by the Total Duvergerian Effect (TDE)—on one-to-one ideological congruence. It pools estimates from five imputed datasets using mice::pool, extracts and merges coefficient tables, and it calculates total and adjusted \(R^2\) values manually for each specification.

# Apply mice
# Use mice::pool to combine results
out1 <- summary(mice::pool(model1))
out2 <- summary(mice::pool(model2))
out3 <- summary(mice::pool(model3))
out1b <- summary(mice::pool(model1b))
out2b <- summary(mice::pool(model2b))
out3b <- summary(mice::pool(model3b))

# Combine the six model outputs into a list
model_list_one <- list(out1, out2, out3, out1b, out2b, out3b)
# Extract coefficients, SEs, and p-values from each model
out_estimates_one <- sapply(model_list_one, extract_Estimates, simplify = FALSE)

# Merge coefficient tables into a single table across the six models
mytable <- merge(out_estimates_one[[1]], out_estimates_one[[2]], by = "variable", all = TRUE)
for (i in c(3:6)) mytable <- merge(mytable, out_estimates_one[[i]], by = "variable", all = TRUE)


# Reorder rows so variables are in desired order
mytable <- mytable[c(5, 2, 3, 4, 1), ]

# Move variable names to row names and remove the variable column
row.names(mytable) <- mytable$variable
mytable$variable <- NULL

# Calculate Total Sum of Squares (TSS) for outcome variable (contemporaneous)
TSS <- sum((MoodDataList[[1]]$one2one.const - mean(MoodDataList[[1]]$one2one.const))^2)
# TSS for lagged version
TSS_b <- sum((MoodDataList[[1]]$one2one.const.lag - mean(MoodDataList[[1]]$one2one.const.lag, na.rm = TRUE))^2, na.rm = TRUE)

# Calculate average R² across the 5 imputations for each model
r2_m1 <- round(Reduce("+", lapply(model1, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(model1), 3)
r2_m2 <- round(Reduce("+", lapply(model2, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(model2), 3)
r2_m3 <- round(Reduce("+", lapply(model3, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(model3), 3)
r2_m1b <- round(Reduce("+", lapply(model1b, function(x) (1 - (sum(x$residuals^2) / TSS_b)))) / length(model1b), 3)
r2_m2b <- round(Reduce("+", lapply(model2b, function(x) (1 - (sum(x$residuals^2) / TSS_b)))) / length(model2b), 3)
r2_m3b <- round(Reduce("+", lapply(model3b, function(x) (1 - (sum(x$residuals^2) / TSS_b)))) / length(model3b), 3)

# Calculate adjusted R² using formula that accounts for sample size and number of parameters
adjr2_m1 <- round(Reduce("+", lapply(model1, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(model1), 3)
adjr2_m2 <- round(Reduce("+", lapply(model2, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(model2), 3)
adjr2_m3 <- round(Reduce("+", lapply(model3, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(model3), 3)
adjr2_m1b <- round(Reduce("+", lapply(model1b, function(x) (1 - (((sum(x$residuals^2) / TSS_b) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(model1b), 3)
adjr2_m2b <- round(Reduce("+", lapply(model2b, function(x) (1 - (((sum(x$residuals^2) / TSS_b) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(model2b), 3)
adjr2_m3b <- round(Reduce("+", lapply(model3b, function(x) (1 - (((sum(x$residuals^2) / TSS_b) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(model3b), 3)

# Append fixed effect indicator and R² stats to the coefficient table
mytable <- rbind(mytable, "Fixed Effects by Year" = c("No", "Yes", "Yes", "No", "Yes", "Yes"))
mytable <- rbind(mytable, "R2" = c(r2_m1, r2_m2, r2_m3, r2_m1b, r2_m2b, r2_m3b))
mytable <- rbind(mytable, "Adj R2" = c(adjr2_m1, adjr2_m2, adjr2_m3, adjr2_m1b, adjr2_m2b, adjr2_m3b))
mytable <- rbind(mytable, "Observations" = c(
  model1[[1]]$nobs,
  model2[[1]]$nobs,
  model3[[1]]$nobs,
  model1b[[1]]$nobs,
  model2b[[1]]$nobs,
  model3b[[1]]$nobs
))

# Rename final table rows for clarity
row.names(mytable) <- c(
  "TDE",
  "AP",
  "$\\Delta$GDP per capita",
  "Inflation Rate",
  "Constant",
  "Fixed Effects by Year",
  "R2",
  "Adj. R2",
  "Observations"
)

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

# Create table
mytable %>%
  kbl(
    caption = ":  TDE as Predictor of One-to-One Congruence",
    col.names = c("(1)", "(2)", "(3)", "(4)", "(5)", "(6)"),
    format = "latex",
    linesep = "",
  )

Table 9.1 tests whether more constraining electoral systems (i.e., systems with higher TDE) reduce ideological alignment between the median citizen and the median legislator. Across all six models, the coefficient on TDE is small and statistically insignificant, suggesting no evidence that electoral system restrictiveness affects one-to-one congruence. This result is consistent with the theoretical expectation outlined in the chapter: that both high-TDE (majoritarian-style) and low-TDE (proportional-style) systems can produce decent alignment at the median because of either convergence to the center (high TDE) or availability of median-aligned parties (low TDE).

How to read Tables 9.1 and 9.2 together. Read the TDE row in each.

Table 9.2 is where the chapter expects to find something: a negative coefficient, meaning more constraining systems reproduce the distribution of citizen preferences less faithfully. Table 9.1 is the useful contrast — if TDE does little for one-to-one congruence, then electoral rules are not simply shifting the centre of the party system relative to the median voter; they are changing how well the spread of opinion is represented.

Compare the contemporaneous and lagged columns within each table. A result that holds in both is harder to attribute to coincident timing.

Table 9.2 : TDE as Predictor of Many-to-Many Congruence

This code constructs Table 9.2, summarizing results from six linear models that estimate the effects of the Total Duvergerian Effect (TDE) and Average Personalism (AP) on many-to-many ideological congruence—that is, how closely the full distribution of legislator preferences mirrors the distribution of citizen preferences. The code pools estimates from multiply imputed models, merges the outputs into a single table, and calculates R² and adjusted pseudo-R² values manually.

# Apply mice
# Use mice::pool to combine results

# Pool estimates from multiple imputations for each model specification
out4 <- summary(mice::pool(model4))
out5 <- summary(mice::pool(model5))
out6 <- summary(mice::pool(model6))
out4b <- summary(mice::pool(model4b))
out5b <- summary(mice::pool(model5b))
out6b <- summary(mice::pool(model6b))

# We need to build a table for the book

# Combine pooled summaries into a list
model_list_many <- list(out4, out5, out6, out4b, out5b, out6b)
# Extract coefficients, SEs, and p-values into structured format
out_estimates_many <- sapply(model_list_many, extract_Estimates, simplify = FALSE)

# Extract coefficients, SEs, and p-values into structured format
mytable <- merge(out_estimates_many[[1]], out_estimates_many[[2]], by = "variable", all = TRUE)
for (i in c(3:6)) mytable <- merge(mytable, out_estimates_many[[i]], by = "variable", all = TRUE)

# Reorder table rows for logical presentation
mytable <- mytable[c(5, 2, 3, 4, 1), ]

# Move variable names to row names and remove variable column
row.names(mytable) <- mytable$variable
mytable$variable <- NULL

# Compute total sum of squares for outcome variable
TSS <- sum((MoodDataList[[1]]$many2many.const - mean(MoodDataList[[1]]$many2many.const))^2)
TSS_b <- sum((MoodDataList[[1]]$many2many.const.lag - mean(MoodDataList[[1]]$many2many.const.lag, na.rm = TRUE))^2, na.rm = TRUE)

# Calculate mean R2 across the 5 imputations for each model
r2_m4 <- round(Reduce("+", lapply(model4, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(model4), 3)
r2_m5 <- round(Reduce("+", lapply(model5, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(model5), 3)
r2_m6 <- round(Reduce("+", lapply(model6, function(x) (1 - (sum(x$residuals^2) / TSS)))) / length(model6), 3)
r2_m4b <- round(Reduce("+", lapply(model4b, function(x) (1 - (sum(x$residuals^2) / TSS_b)))) / length(model4b), 3)
r2_m5b <- round(Reduce("+", lapply(model5b, function(x) (1 - (sum(x$residuals^2) / TSS_b)))) / length(model5b), 3)
r2_m6b <- round(Reduce("+", lapply(model6b, function(x) (1 - (sum(x$residuals^2) / TSS_b)))) / length(model6b), 3)

# Calculate adjusted pseudo-R² for each model
adjr2_m4 <- round(Reduce("+", lapply(model4, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(model4), 3)
adjr2_m5 <- round(Reduce("+", lapply(model5, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(model5), 3)
adjr2_m6 <- round(Reduce("+", lapply(model6, function(x) (1 - (((sum(x$residuals^2) / TSS) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(model6), 3)
adjr2_m4b <- round(Reduce("+", lapply(model4b, function(x) (1 - (((sum(x$residuals^2) / TSS_b) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(model4b), 3)
adjr2_m5b <- round(Reduce("+", lapply(model5b, function(x) (1 - (((sum(x$residuals^2) / TSS_b) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(model5b), 3)
adjr2_m6b <- round(Reduce("+", lapply(model6b, function(x) (1 - (((sum(x$residuals^2) / TSS_b) * (x$nobs - 1)) / (x$nobs - x$nparams - 1))))) / length(model6b), 3)

# # Append additional metadata to the table. Add R2, adj.R2, and N
mytable <- rbind(mytable, "Fixed Effects by Year" = c("No", "Yes", "Yes", "No", "Yes", "Yes"))
mytable <- rbind(mytable, "R2" = c(r2_m4, r2_m5, r2_m6, r2_m4b, r2_m5b, r2_m6b))
mytable <- rbind(mytable, "Adj R2" = c(adjr2_m4, adjr2_m5, adjr2_m6, adjr2_m4b, adjr2_m5b, adjr2_m6b))
mytable <- rbind(mytable, "Observations" = c(
  model4[[1]]$nobs,
  model5[[1]]$nobs,
  model6[[1]]$nobs,
  model4b[[1]]$nobs,
  model5b[[1]]$nobs,
  model6b[[1]]$nobs
))

# Rename rows for final display
row.names(mytable) <- c(
  "TDE",
  "AP",
  "$\\Delta$GDP per capita",
  "Inflation Rate",
  "Constant",
  "Fixed Effects by Year",
  "R2",
  "Adj. R2",
  "Observations"
)

# Clean empty cells
mytable[is.na(mytable)] <- ""

# Create table
mytable %>%
  kbl(
    caption = ":  TDE as Predictor of Many-to-Many Congruence",
    col.names = c("(1)", "(2)", "(3)", "(4)", "(5)", "(6)"),
    format = "latex",
    linesep = "",
  )

Table 9.2 evaluates the impact of electoral system restrictiveness on many-to-many congruence—how well legislatures reflect the entire distribution of ideological preferences among citizens. Unlike Table 9.1, the results here show a consistent and negative effect of TDE: in both the basic model and the fixed-effects model (columns 1 and 2, and their lagged counterparts 4 and 5), the TDE coefficient is negative and statistically significant. This supports the book’s theoretical claim that restrictive electoral systems suppress representational diversity, reducing the ability of citizens to elect legislators across the ideological spectrum.

Not in the printed chapter. Figure 9.5 is generated by this file but does not appear in the book. It is retained here because it presents the models of Table 9.2 in expected-value form, which is easier to read than coefficients.

Figure 9.5: Expected Values of Many-to-Many Congruence by TDE

Figure 9.5 panel (a): Expected Values of Many-to-Many Congruence by TDE (no lag)

Figure 9.5a visualizes the predicted relationship between the Total Duvergerian Effect (TDE) and many-to-many ideological congruence, based on the first model specification from Table 9.2. Using simulation techniques, the code generates expected values of congruence across the full range of observed TDE values while holding economic conditions constant at their median. The plot displays both the expected trend line and its 95% confidence interval, allowing for an intuitive representation of the substantive magnitude and uncertainty surrounding the effect of electoral system restrictiveness on distributional congruence between citizens and legislators.

No random seed is set in this block, and it uses randomness. rmvnorm() draws 100 random coefficient vectors, so the shaded confidence band will differ slightly every time the chapter is knitted. Adding set.seed(...) at the top of this block, and of the lag block below, would make the figures reproduce exactly. No R code has been changed here.

Simulating a confidence band. Rather than deriving the uncertainty around a predicted value algebraically, this block obtains it by simulation:

  1. Build a ladder of 100 TDE values spanning the range of interest, holding the economic controls at their medians.
  2. Draw 100 plausible coefficient vectors from the fitted model’s own distribution — its estimates and variance-covariance matrix. Each draw is one version of “what the model might be”.
  3. For each rung of the ladder, compute the predicted congruence under all 100 draws, and take the 2.5th, 50th and 97.5th percentiles.
  4. Repeat across the five imputations and average.

The middle percentile traces the expected value; the outer two become the edges of the shaded band.

# min(MoodData$totalEff.hat): 0.158
# max(MoodData$totalEff.hat): 1.69

# Generate a sequence of TDE values from 0 to 2 (slightly wider than observed range)
# seq(from, to, length.out=100) builds the ladder of TDE values that becomes the
# horizontal axis. The commented figures above record the observed range, so the
# 0-to-2 span extends slightly beyond the data at both ends.
newx <- seq(min(0), max(2), length.out = 100) # interval larger than observed TDE

# Create a new data frame for prediction, with median values of covariates
newdata <- data.frame(
  "Intercept" = rep(1, length(100)), # Constant/intercept term
  "TDE" = newx, # Varying TDE values
  "change.gdppc" = rep(
    median(MoodData$change.gdppc),
    length(100)
  ), # Median GDP growth
  "inflation" = rep(
    median(MoodData$inflation, na.rm = T),
    length(100)
  )
) # Median inflation


# Display model coefficients from model4 (used for validation/debugging)
model4[[1]]$coefficients
##  (Intercept)          TDE change.gdppc    inflation 
##       0.7071      -0.0958       1.1243      -0.1846
# Initialize list to store simulated predictions from each of the five imputations
all.preds <- list()

# Loop over the 5 imputed versions of model 4
for (i in 1:5) {
  # Draw 100 coefficient vectors from multivariate normal (using model covariance)
  # This is step 2 above. Drawing from the MULTIVARIATE normal, using the full
  # variance-covariance matrix rather than the standard errors alone, preserves
  # the correlations between coefficients -- which matters, because the
  # intercept and slope are not independent.
  # unclass() strips the class attribute from the variance-covariance matrix.
  # Recent versions of fixest return vcov() as an object of class
  # "fixest_vcov" rather than a plain matrix, and rmvnorm() internally calls
  # isSymmetric(), which has no method for that class -- producing
  #   "no applicable method for 'isSymmetric' applied to an object of class
  #    fixest_vcov"
  # Removing the class lets isSymmetric() dispatch to the ordinary matrix
  # method. The numbers are untouched; only the label on the object changes.
  coefs <- rmvnorm(100, model4[[i]]$coefficients, unclass(vcov(model4[[i]])))
  # Create empty matrix to store 2.5%, 50%, and 97.5% quantiles for each TDE value
  # Three rows (lower bound, median, upper bound) by 100 columns (TDE values).
  preds <- matrix(NA, nrow = 3, ncol = 100)

  # For each TDE value (row in newdata), compute predicted quantiles
  # %*% is matrix multiplication: each of the 100 coefficient draws is combined
  # with this rung's covariate values, giving 100 predicted congruence scores.
  # quantile() then summarises those 100 numbers into three.
  for (j in 1:100) {
    preds[, j] <- quantile(coefs %*% as.matrix(newdata)[j, ],
      prob = c(0.025, 0.5, 0.975)
    )
  }
  # Store prediction matrix for each model
  all.preds[[i]] <- preds
}

# Average the predicted quantiles across the 5 model versions
# Reduce("+", ...) sums the five 3x100 matrices element by element; dividing by
# five averages them. `expectations` is therefore the pooled result: row 1 the
# lower bound, row 2 the median, row 3 the upper bound.
expectations <- Reduce("+", all.preds) / length(all.preds)

# Set up plot area
par(mfrow = c(1, 1), mar = c(4, 4, 0, 0), cex.axis = 1.2)

# Create an empty plot with x and y limits
plot(
  x = c(min(newx), max(newx)), y = c(0, 3),
  xlab = "", ylab = "", type = "n", bty = "n",
  xlim = c(0, 2), ylim = c(0, 1)
)

# Add axis labels
mtext(side = 1, line = 2, text = "Total Duvergerian Effect", cex = 1.5)
mtext(side = 2, line = 2, text = "Expected values of many-to-many congruence", cex = 1.5)
# Plot shaded confidence interval around the expected values
# polygon() draws a filled shape. Running along newx and then back along its
# reverse, with the lower bounds followed by the reversed upper bounds, traces
# the outline of the band in a single closed path.
polygon(
  x = c(newx, rev(newx)),
  y = c(expectations[1, ], rev(expectations[3, ])),
  col = adjustcolor("grey", alpha.f = 0.95),
  border = NA
)
# Plot median expected values (fitted line)
# NOTE: the band above is drawn from `expectations`, the average across all five
# imputations, but this line is drawn from `preds` -- the matrix left over from
# the final pass of the loop, i.e. imputation 5 only. The two therefore come
# from different sources. Using expectations[2,] would make the line the pooled
# median, consistent with its own band. Left as it was run.
lines(x = newx, y = preds[2, ], lty = 1, col = "white", lwd = 3)

# Save plot as PDF
# dev.copy() writes a copy of the on-screen figure to a PDF in the working
# directory; invisible() suppresses the device number that would otherwise
# print. Note this writes a file every time the chapter is knitted.
invisible(dev.copy(pdf, "expected_many2many.pdf", h = 7, w = 10))
invisible(dev.off())

How to read Figure 9.5. The horizontal axis is TDE, from permissive on the left to constraining on the right; the vertical axis is expected many-to-many congruence. The white line is the expected value and the grey band its 95% simulation interval, with the economic controls held at their medians.

The chapter’s claim is a downward slope: as systems become more constraining, the distribution of party positions reproduces the distribution of citizen preferences less well. Judge the claim by whether the band at the left end of the range overlaps the band at the right end — if it does, the apparent slope is within simulation noise. Panel (b) repeats the exercise with a lagged specification; a similar slope there is evidence the result is not an artefact of timing.

Figure 9.5a visualizes the estimated effect of the Total Duvergerian Effect (TDE) on many-to-many ideological congruence. As predicted in the theoretical discussion of Chapter 9, higher levels of TDE—representing more constraining electoral systems—are associated with lower levels of congruence between the ideological distribution of citizens and that of elected legislators. The downward slope of the line shows a clear negative relationship, while the shaded band reflects the model’s uncertainty. Substantively, this pattern suggests that when electoral rules penalize small or ideologically distinct parties, voters are less able to express their sincere preferences, resulting in legislatures that poorly reflect the full diversity of public opinion.

Figure 9.5 panel (b) Expected values of many-to-many congruence (lag)

This figure plots the predicted values of many-to-many congruence as a function of the Total Duvergerian Effect (TDE), using lagged citizen preferences. It reflects the results from model 5 in Table 9.2 and helps visualize the delayed effect of electoral system restrictiveness on how well legislatures represent the full ideological distribution of the electorate.

# Generate a sequence of TDE values from 0 to 2 (slightly broader than empirical range)
newx <- seq(min(0), max(2), length.out = 100) # interval larger than observed TDE

# Create a new data frame for predictions, using median economic values
newdata <- data.frame(
  "TDE" = newx,
  "change.gdppc" = rep(
    median(MoodData$change.gdppc),
    length(100)
  ),
  "inflation" = rep(
    median(MoodData$inflation, na.rm = T),
    length(100)
  )
)

# Initialize list to store predicted values from each imputed model
all.preds <- list()

# Loop over the 5 imputed lagged models with fixed effects
for (i in 1:5) {
  # Simulate 100 draws of model coefficients from multivariate normal distribution
  # unclass() for the same reason as in panel (a): newer fixest returns a
  # classed "fixest_vcov" object that rmvnorm()'s internal isSymmetric() call
  # cannot dispatch on. Numerically inert.
  coefs <- rmvnorm(100, model5b[[i]]$coefficients, unclass(vcov(model5b[[i]])))

  # Matrix to hold quantile predictions for each newx value
  preds <- matrix(NA, nrow = 3, ncol = 100)

  # For each newx value, compute predicted value including fixed effect for reference year
  for (j in 1:100) {
    preds[, j] <- quantile(coefs[, 1:3] %*% as.matrix(newdata)[j, ]
      + fixef(model5b[[i]])$year[1], prob = c(0.025, 0.5, 0.975)) # Add fixed effect for baseline year
  }
  # Store simulated predictions
  all.preds[[i]] <- preds
}

# Average the predicted quantiles across the 5 models
expectations.2 <- Reduce("+", all.preds) / length(all.preds)

# Set up plotting parameters
par(mar = c(4, 4, 0, 0), cex.axis = 1.2)

# Create empty plot window
plot(
  x = c(min(newx), max(newx)), y = c(0, 3),
  xlab = "", ylab = "", type = "n", bty = "n",
  xlim = c(0, 2), ylim = c(0, 1)
)

# Add axis labels
mtext(side = 1, line = 2, text = "Total Duvergerian Effect", cex = 1.5)
mtext(side = 2, line = 2, text = "Expected values of many-to-many congruence", cex = 1.5)

# Plot 95% confidence band around predicted values
polygon(
  x = c(newx, rev(newx)),
  y = c(expectations.2[1, ], rev(expectations.2[3, ])),
  col = adjustcolor("grey", alpha.f = 0.95),
  border = NA
)

# Plot median expected congruence line
# NOTE: as in panel (a), the band above is drawn from the pooled
# `expectations.2`, while this line is drawn from `preds` -- the matrix left
# over from the final pass of the loop, i.e. imputation 5 alone. Using
# expectations.2[2,] would make line and band consistent. Left as it was run.
lines(x = newx, y = preds[2, ], lty = 1, col = "white", lwd = 3)

# Export figure as PDF
invisible(dev.copy(pdf, "expected_many2many_lag.pdf", h = 7, w = 10))
invisible(dev.off())

Figure 9.5b shows the lagged effect of the Total Duvergerian Effect (TDE) on many-to-many congruence. The plot is based on model 5 from Table 9.2, which includes year fixed effects but excludes Average Personalism. Consistent with the theory outlined in Chapter 9, the figure confirms a negative relationship between electoral system restrictiveness and the extent to which legislative ideological distributions match those of citizens. Importantly, this negative effect persists even when using past-year citizen preferences, indicating that the influence of electoral design on representation is not merely contemporaneous but also endures over time. Although the slope is modest, its direction and magnitude suggest that more constraining electoral systems limit the diversity of political representation, even after accounting for annual shocks and economic conditions.

Added-variable plots based on model1 Table 9.2

This figure presents an added-variable plot to visualize the partial relationship between the Total Duvergerian Effect (TDE) and many-to-many ideological congruence, controlling for GDP growth and inflation. This plot corresponds to model 4 in Table 9.2 and serves as a graphical diagnostic tool: it plots the residuals of congruence (after removing the effect of economic covariates) against the residuals of TDE (purged of the same controls). The fitted line and confidence band reveal whether TDE has an independent relationship with congruence once macroeconomic context is held constant.

# Regress congruence on GDP and inflation, then extract residuals (y-axis)
y.res <- residuals(lm(many2many.const ~ change.gdppc + inflation, data = MoodData))
# Regress TDE on the same covariates, extract residuals (x-axis)
x.res <- residuals(lm(totalEff.hat ~ change.gdppc + inflation, data = MoodData))
# Create 100 evenly spaced points across the observed range of x.res for smoother
newx <- seq(min(x.res), max(x.res), length = 100)

# Predict y|x using simple linear regression with confidence intervals
preds <- predict(lm(y.res ~ x.res),
  newdata = data.frame("x.res" = newx),
  interval = "confidence"
)

# Set up clean plotting environment
par(mfrow = c(1, 1), mar = c(4, 4, 0, 0), cex.axis = 1.2)
# Create empty plot
plot(y.res ~ x.res,
  xlab = "", ylab = "", type = "n", bty = "n",
  xlim = c(min(x.res), max(x.res)), ylim = c(-0.5, 0.5)
)

# Add axis labels
mtext(side = 1, line = 2, text = "Total Duvergerian Effect | predictors", cex = 1.5)
mtext(side = 2, line = 2, text = "One-to-one congruence | predictors", cex = 1.5)

# Add shaded 95% confidence interval
polygon(
  x = c(newx, rev(newx)),
  y = c(preds[, "lwr"], rev(preds[, "upr"])),
  col = adjustcolor("grey", alpha.f = 0.95),
  border = NA
)

# Add fitted line for partial relationship
lines(x = newx, y = preds[, "fit"], lty = 1, col = "white", lwd = 3)

# Label each observation using shortID
text(xy.coords(x.res, y.res),
  labels = MoodData$shortID, col = "black", cex = 0.8
)

# Export plot as PDF
invisible(dev.copy(pdf, "avplot_many2many.pdf", h = 7, w = 10))
invisible(dev.off())

This figure graphically confirms the negative effect of TDE on many-to-many congruence, controlling for GDP growth and inflation. The downward-sloping fitted line shows that even after accounting for macroeconomic context, countries with more restrictive electoral systems (higher TDE) tend to have lower representational congruence between citizens and legislators. The slope is moderate but consistent, and the 95% confidence band remains narrow across the range of TDE residuals, indicating statistical precision. The clustering of observations and the presence of some leverage points (e.g. EC96, MX06, GU12) do not distort the overall trend. This plot visually supports the argument developed in Chapter 9: TDE is a meaningful predictor of representation failure, not just at the median (one-to-one), but across the ideological distribution (many-to-many).

Moving Forward

In this chapter, we examined the relationship between electoral system incentives—captured by the Total Duvergerian Effect (TDE)—and the ideological congruence between citizens and their elected representatives. We distinguished between two key concepts of congruence: one-to-one congruence, which compares the median citizen’s and median legislator’s ideological positions, and many-to-many congruence, which considers the similarity between the entire distributions of citizen and legislator preferences. Our findings suggest that while TDE has little consistent impact on one-to-one congruence, systems with high TDE tend to reduce many-to-many congruence, limiting the degree to which the ideological diversity of the electorate is reflected in legislatures. These results reinforce the idea that more permissive, proportional electoral systems better preserve the ideological heterogeneity of voters in legislative bodies.

By integrating theoretical expectations with detailed empirical data from Latin America, this chapter contributes to a nuanced understanding of how electoral institutions shape democratic representation beyond mere party counts or platforms. However, congruence is just one link in the chain of democratic responsiveness. The next part of the book shifts focus from interparty competition to intraparty politics, exploring how electoral incentives influence the behavior of individual politicians within parties. Chapter 10 will begin this exploration by analyzing how electoral systems shape campaign dynamics and the intensity of personalistic appeals, thereby bridging the macro-level patterns of party competition with the micro-level behavior of candidates.

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
data.table 1.17.8
dplyr 1.1.4
fixest 0.14.1
forcats 1.0.0
ggplot2 3.5.2
gridExtra 2.3
kableExtra 1.4.1
lubridate 1.9.4
mice 3.19.0
mvtnorm 1.3-3
purrr 1.2.2
readr 2.1.5
sandwich 3.1-1
stargazer 5.2.3
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.