This is a replication file. It contains every line of R code needed to reproduce the tables and figures of Chapter 8, 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 8 tests whether the Total Duvergerian Effect (TDE) is correlated with the dispersion of party platforms along the Left–Right ideological continuum. The measure of dispersion is based on the Earth Mover’s Distance (EMD) algorithm, which considers how far, in a mathematical sense, the distribution of party locations in a given system is from being “uniform”. The focus is on the array of parties across some length of the spectrum, not polarization — the presence of major parties only at each extreme. We find that strong systems, those with high TDE scores, tend to result in parties taking up a limited number of positions, while weak systems lead to parties arraying themselves more evenly across the spectrum.
Dispersion is not polarization, and the distinction drives the chapter. Consider two five-party systems on a left–right scale. In the first, all five parties bunch at the two extremes and none occupies the centre. In the second, the five spread evenly from left to right. A polarization measure calls the first system extreme and the second moderate. A dispersion measure asks a different question — how much of the spectrum is occupied at all — and calls the second system more dispersed.
The chapter argues that electoral rules govern dispersion. That is why it builds two measures: Dalton’s polarization score, as a familiar benchmark, and EMD, the measure it actually uses. Figure 8.2 then shows how weakly the two track each other, which is the justification for preferring EMD.
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.%>% is the “pipe”: a %>% f() is
another way of writing f(a). Long pipelines below read top
to bottom, one operation per line.df$col pulls the column col out of the
data frame df.for (i in 1:n) { ... } repeats the braced instructions
n times.y ~ x reads “y is explained by
x”.Why dplyr:: is spelled out in the code
below. filter() and select() have
namesakes in other packages — filter() in base R’s
stats, select() in MASS.
R resolves a name to whichever package was attached most recently, so a
bare filter() can silently become the wrong function if
either is loaded. Writing dplyr::filter() removes the
ambiguity. Note that library(tidyverse) is no defence:
loading an already-attached package does nothing, so it cannot restore
dplyr’s precedence.
# Load relevant libraries
library(tidyverse) # bundle of data-handling packages (dplyr, ggplot2, ...)
library(emdist) # Earth Mover's Distance -- supplies emd() , the chapter's
# central measure of ideological dispersion
library(sjPlot) # publication-ready regression tables and model plots
library(ggrepel) # keeps overlapping point labels legible in scatterplots
library(gridExtra) # arranges several ggplot panels into one figureThis chapter examines how electoral system incentives shape the ideological distribution of parties within party systems. Building on the Total Duvergerian Effect (TDE) introduced earlier, we investigate whether more constraining systems produce ideologically clustered parties, while permissive systems encourage greater dispersion along the left–right spectrum. To measure ideological dispersion, we use the earth mover’s distance (EMD), which quantifies how party positions deviate from an even spread across the ideological space. Using expert-coded party location data from the V-PARTY project, we test the relationship between TDE and ideological dispersion across a broad range of countries and elections.
Our results confirm that stronger electoral constraints are linked to more clustered party systems, while weaker constraints allow for more dispersed party locations. This analysis complements the previous chapter on party system size and leads into the next chapter’s focus on ideological congruence between parties and voters.
This file contains calls to a smaller dataset used in Chapter 8 (The Distribution of Partisan Ideological Locations), as well as the code necessary to produce all graphs in the chapter.
Load relevant datasets
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/ch08/... 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 08 materials
# the short file names below. This is the one line to edit for your machine.
# Load the main V-Party dataset from CSV (party-level data including ideology and seat share)
# Expert-coded party positions from the V-Party project. One row per party per
# election. The two columns that matter are v2pariglef (the party's left-right
# location) and v2panumbseat (how many seats it won).
dta <- read.csv("data/ch08/csv/V-Dem-CPD-Party-V2.csv")
# Load additional data from .RData files:
# load() restores objects under the names they were saved with, so no assignment
# is needed -- the objects simply appear in memory.
load("data/ch08/vdem_aug_2024.RData")
# Country-level TDE and AP scores from Chapter 6, merged in later. Note the
# vintage in the filename: chapters use different versions of this file and they
# are not interchangeable.
load("data/shared/RealSystems_Scores_GBM_Aug_2024.RData")Clean and organize variables
# Keep only parties that have at least one seat in the legislature
# (We are only interested in parties that won representation)
dta <- dta %>% dplyr::filter(v2panumbseat > 0) # Dropping parties without mass (seats)
# Defining mass and location
dta <- dta %>%
# Convert year to factor so that it can be treated as categorical
mutate(year = as.factor(year)) %>%
# Keep only the variables we will use
dplyr::select(country_name, year, v2pariglef, v2panumbseat, v2patotalseat, v2paenname, v2pashname) %>%
# Drop rows where any of the key variables are missing
drop_na(v2patotalseat, v2panumbseat, v2pariglef) %>%
# Group data by country and year to calculate mass within each electoral context
group_by(country_name, year) %>%
# Create a variable 'mass' for the seat share of each party
mutate(mass = v2panumbseat / sum(v2panumbseat)) %>% # In some case sum(v2panumbseat) != v2patotalseat
# Add a row number to track each party's position within its country-year group
mutate(rn = row_number()) %>%
ungroup() %>%
# Rename variables to more readable names:
# v2pariglef corresponds to party position on left right dimension from -4 far left to 4 far right
# - 'location' for party ideology (from –4 to 4, left to right)
# - 'country' for country name
# - 'party_name' and 'party_abbrev' for party identifiers
rename(location = v2pariglef, country = country_name, party_name = v2paenname, party_abbrev = v2pashname) %>%
# Remove the grouping so future operations apply to the full dataset
# Keep only the final variables we need in a clean format
dplyr::select(country, year, rn, mass, location, party_name, party_abbrev) %>%
# Sort the data by country, year, and row number
arrange(country, year, rn)
# Remove cases where a party controls 100% of the seats(i.e., mass = 1). These are usually one-party regimes and not useful for studying ideological dispersion.
dta <- dta %>% dplyr::filter(mass < 1)We seek to obtain a polarization score similar to Dalton (2008). We will only use this polarization score to compare it with a measure of centrifugal-centripetal distribution.
Dalton (2008) suggests a polarization score that squares the ideological distance between party i and the mean ideological position of all parties, multiplies this squared distance by party i’s vote share, adds over all parties and divides by 5, and then obtains the square root of the latter quantity. Dalton appears to divide by 5 because that is the midpoint of the ideological scale. The midpoint in the V-party scores that we employ is, in principle, 0, so we do not need to divide by any positive integer. In addition, Dalton suggests that the ideological scale he employs goes from 1 to 9, but if this is true, the maximum value that the polarization scale can take is not 10, but 8. He suggests that he uses a party’s vote share — a number between 0 and 1 — but he really uses a percentage form — a number between 0 and 100. Below, we use an actual share.
For the reasons expressed in the previous paragraph, our polarization scores are a rescaling of the ones that Dalton presents. Furthermore, they are calculated on party positions derived from V-party, whereas he uses data from CSES.
Why this is written as a loop over rows. Polarization is a property of a party system, not of a party, so it has to be computed once per country-year and then written back onto every party in that system.
The data are sorted so that each country-year occupies a consecutive
block of rows, and rn records each party’s position within
its block. That lets the loop reconstruct where the current block starts
by counting backwards (first_row <- i - r + 1). Modern
dplyr would express this as group_by() plus
mutate(), but the loop makes each arithmetic step visible,
which is useful here because the formula is being deliberately modified
from Dalton’s original — see the discussion above about the divisor and
the scale.
# Create a new variable in the dataset to store the polarization score
# Pre-filling with NA creates the column; the loop overwrites it block by block.
dta$polarization <- NA
# Loop over each row of the dataset
for (i in 1:nrow(dta)) {
# Handle the last row separately (avoid indexing out of bounds)
if (i == nrow(dta)) {
dta_pol <- NA
r <- dta$rn[i] # get the rank of the party
first_row <- i - r + 1 # compute start of the current country-year block
last_row <- i # the current (last) row is the end of the block
# Subset the dataset to only include the parties for this country-year
dta_pol <- dta[first_row:last_row, ]
# Create a dataframe with just the mass and location of parties
country_matrix <- data.frame(
"mass" = dta_pol$mass,
"location" = dta_pol$location
)
# Compute Dalton's polarization score
tmp <- c()
for (k in 1:nrow(country_matrix)) {
tmp[k] <- country_matrix$mass[k] * (country_matrix$location[k] -
mean(country_matrix$location))^2
}
# Assign the polarization score to all parties in this country-year block
for (l in first_row:last_row) {
dta$polarization[l] <- sqrt(sum(tmp))
}
}
# If the next row corresponds to a new country-year, repeat the same calculation for the current country-year block
else if (dta$rn[i] > dta$rn[i + 1]) {
dta_pol <- NA
r <- dta$rn[i]
first_row <- i - r + 1
last_row <- i
dta_pol <- dta[first_row:last_row, ]
# Create data.frame with country-specific parties
country_matrix <- data.frame(
"mass" = dta_pol$mass,
"location" = dta_pol$location
)
# Build Dalton's polarization score
tmp <- c()
for (k in 1:nrow(country_matrix)) {
tmp[k] <- country_matrix$mass[k] * (country_matrix$location[k] -
mean(country_matrix$location))^2
}
for (l in first_row:last_row) {
dta$polarization[l] <- sqrt(sum(tmp))
}
}
}The following snippets include algorithms to generate an earth-mover’s distance measure of political party location, which is closer to the notion of centripetal-centrifugal competition.
What the Earth Mover’s Distance measures. The name is literal. Picture each party as a pile of earth sitting at its position on the left–right scale, the size of the pile being the party’s seat share. Now picture the reference landscape we would like to compare it against: eleven equal piles spread evenly from −5 to +5.
EMD is the minimum amount of work — earth moved, times distance moved — needed to reshape the actual landscape into the reference one. A party system already spread evenly across the spectrum needs little shifting, so its EMD is low. A system where every party bunches in one place needs a great deal of shifting, so its EMD is high.
Read it, then, as a distance from evenness: high EMD means concentrated, low EMD means dispersed. That sign is easy to get backwards, and it matters for reading every figure in this chapter.
The structure of this loop is identical to the polarization loop above — walk through the country-year blocks, compute one number per system, write it back onto every party in the block. Only the quantity computed differs.
# Define the reference (uniform) distribution:
# 11 equally-spaced ideological positions from -5 to 5
# Each has an equal mass of 1/11
# This is the benchmark every real party system is compared against: perfectly
# even occupation of the ideological spectrum. rep(1/11, 11) repeats the value
# 1/11 eleven times, so the masses sum to 1, matching the seat shares.
# emd() needs a matrix rather than a data frame, hence as.matrix().
uni_matrix <- as.matrix(data.frame("mass" = rep(1 / 11, 11), "location" = seq(-5, 5, by = 1)))
# Create x-values to plot or understand the distribution
x_values <- seq(-5, 5, length.out = 11)
# Set the mean and standard deviation
mean_value <- 0
sd_value <- 1.5
# Create a new column in the dataset to store the EMD score for each country-year
dta$emd <- NA
# Loop over the dataset row by row
for (i in 1:nrow(dta)) {
# Handle the very last row separately to avoid index error
if (i == nrow(dta)) {
dta_emd <- NA
r <- dta$rn[i] # get party rank
first_row <- i - r + 1 # start of current country-year block
last_row <- i # last row is end of block
# Subset the current country-year
dta_emd <- dta[first_row:last_row, ]
# Create a matrix with each party's seat share (mass) and ideological position
country_matrix <- as.matrix(data.frame("mass" = dta_emd$mass, "location" = dta_emd$location))
# Compute the Earth Mover’s Distance (EMD) using Manhattan distance
# This tells us how far the actual party distribution is from a uniform one
# dist = "manhattan" sets the cost of moving mass to the straight-line
# distance along the ideological scale, so shifting a party two units left
# costs twice as much as shifting it one.
# Note that the result is stored in an object called `emd`, the same name as
# the function itself. R keeps functions and variables in separate
# namespaces, so this works -- but it is a habit worth avoiding.
emd <- emd(country_matrix, uni_matrix, dist = "manhattan")
# Assign the EMD score to all parties in this country-year
for (k in first_row:last_row) {
dta$emd[k] <- emd
}
}
# For any row where the next row begins a new country-year block
else if (dta$rn[i] > dta$rn[i + 1]) {
dta_emd <- NA
r <- dta$rn[i]
first_row <- i - r + 1
last_row <- i
dta_emd <- dta[first_row:last_row, ]
# I need everything in matrix to get the EMD
country_matrix <- as.matrix(data.frame("mass" = dta_emd$mass, "location" = dta_emd$location))
# Getting the EMD
emd <- emd(country_matrix, uni_matrix, dist = "manhattan")
for (k in first_row:last_row) {
dta$emd[k] <- emd
}
}
}The following code produces graphs for the examples we consider in the chapter (Figure 8.1). This figure visually compares four stylized party systems in terms of their ideological dispersion and polarization. It overlays each party distribution with a uniform distribution to highlight deviations, and reports both the EMD and Dalton-style polarization scores.
Invented systems, not data. The four party systems in this figure are constructed by hand. That is the point: they are chosen so that dispersion and polarization come apart, demonstrating that the two measures capture different things before either is applied to real cases. Read the two reported scores beneath each panel as the argument of the figure.
## Define four hypothetical party systems using mass-location matrices
# 1. All parties at center (perfect centripetal convergence)
full.center.convergence <- matrix(data = c(0.5, 0, 0.5, 0), ncol = 2, byrow = T)
# 2. All parties at the extremes (perfect polarization)
full.extreme.divergence <- matrix(data = c(0.5, -5, 0.5, 5), ncol = 2, byrow = T)
# 3. Evenly spread but located close to center
even.dispersion <- matrix(c(0.25, -2, 0.25, -1, 0.25, 0, 0.25, 1), ncol = 2, byrow = T)
# 4. Evenly spread but over a wider range
even.dispersion.2 <- matrix(c(0.25, -4, 0.25, -2, 0.25, 0, 0.25, 2), ncol = 2, byrow = T)
# Label columns consistently
colnames(full.center.convergence) <- colnames(full.extreme.divergence) <- c("mass", "location")
colnames(even.dispersion) <- colnames(even.dispersion.2) <- c("mass", "location")
# Calculate and display EMD values for each case (not used for plotting yet)
emd(full.center.convergence,
uni_matrix,
dist = "manhattan"
)## [1] 2.73
## [1] 2.27
## [1] 1.82
## [1] 1.23
# Expand each example into full-length vectors to match the 11-position reference scale
full.center.convergence.expanded <- data.frame(matrix(data = c(0, -5, 0, -4, 0, -3, 0, -2, 0, -1, 1, 0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5), ncol = 2, byrow = T))
full.extreme.divergence.expanded <- data.frame(matrix(data = c(0.5, -5, 0, -4, 0, -3, 0, -2, 0, -1, 0, 0, 0, 1, 0, 2, 0, 3, 0, 4, 0.5, 5), ncol = 2, byrow = T))
even.dispersion.expanded <- data.frame(matrix(c(0, -5, 0, -4, 0, -3, 0.25, -2, 0.25, -1, 0.25, 0, 0.25, 1, 0, 2, 0, 3, 0, 4, 0, 5), ncol = 2, byrow = T))
even.dispersion.2.expanded <- data.frame(matrix(c(0, -5, 0, -4, 0.25, -3, 0, -2, 0.25, -1, 0, 0, 0.25, 1, 0, 2, 0.25, 3, 0, 4, 0, 5), ncol = 2, byrow = T))
# Assign column names
colnames(full.center.convergence.expanded) <- colnames(full.extreme.divergence.expanded) <- c("mass", "location")
colnames(even.dispersion.expanded) <- colnames(even.dispersion.2.expanded) <- c("mass", "location")
# Prepare uniform reference distribution (11 equal mass positions)
uniform.expanded <- data.frame(uni_matrix)
colnames(uniform.expanded) <- c("mass", "location")
# Arrange plotting area into 2 rows × 2 columns
par(mfrow = c(2, 2))
# Case 1: Evenly-sized dispersed system (low concentration)
barplot(even.dispersion.2.expanded$mass,
names.arg = even.dispersion.2.expanded$location,
col = rgb(100 / 255, 100 / 255, 100 / 255,
alpha = 0.5
),
xlab = "Party locations",
ylab = "Seat share",
main = "Evenly-sized dispersed party system",
ylim = c(0, 1)
)
# Overlay the second bar plot
barplot(uniform.expanded$mass,
names.arg = uniform.expanded$location,
col = rgb(1, 1, 1, alpha = 0.5),
add = TRUE
)
legend("topleft",
bty = "n",
legend = c(
paste0(
"EMD ",
round(emd(even.dispersion.2,
uni_matrix,
dist = "manhattan"
), 2)
),
paste0("Polarization ", 2.24)
)
)
# Case 2: Evenly-sized concentrated system (parties clustered)
barplot(even.dispersion.expanded$mass,
names.arg = even.dispersion.expanded$location,
col = rgb(100 / 255, 100 / 255, 100 / 255,
alpha = 0.5
),
xlab = "Party locations",
ylab = "Seat share",
main = "Evenly-sized concentrated party system",
ylim = c(0, 1)
)
# Overlay the second bar plot
barplot(uniform.expanded$mass,
names.arg = uniform.expanded$location,
col = rgb(1, 1, 1, alpha = 0.5),
add = TRUE
)
legend("topleft",
bty = "n",
legend = c(
paste0(
"EMD ",
round(emd(even.dispersion,
uni_matrix,
dist = "manhattan"
), 2)
),
paste0("Polarization ", 1.12)
)
)
# Case 3: Maximally polarized system (parties at extremes)
barplot(full.extreme.divergence.expanded$mass,
names.arg = full.extreme.divergence.expanded$location,
col = rgb(100 / 255, 100 / 255, 100 / 255,
alpha = 0.5
),
xlab = "Party locations",
ylab = "Seat share",
main = "Maximally-dispersed polarized party system",
ylim = c(0, 1)
)
# Overlay the second bar plot
barplot(uniform.expanded$mass,
names.arg = uniform.expanded$location,
col = rgb(1, 1, 1, alpha = 0.5),
add = TRUE
)
legend("topleft",
bty = "n",
legend = c(
paste0(
"EMD ",
round(emd(full.extreme.divergence,
uni_matrix,
dist = "manhattan"
), 2)
),
paste0("Polarization ", 3.54)
)
)
# Case 4: Fully converged system (all parties at center)
barplot(full.center.convergence.expanded$mass,
names.arg = full.center.convergence.expanded$location,
col = rgb(100 / 255, 100 / 255, 100 / 255,
alpha = 0.5
),
xlab = "Party locations",
ylab = "Seat share",
main = "Maximally-concentrated party system",
ylim = c(0, 1)
)
# Overlay the second bar plot
barplot(uniform.expanded$mass,
names.arg = uniform.expanded$location,
col = rgb(1, 1, 1, alpha = 0.5),
add = TRUE
)
legend("topleft",
bty = "n",
legend = c(
paste0(
"EMD ",
round(emd(full.center.convergence,
uni_matrix,
dist = "manhattan"
), 2)
),
paste0("Polarization ", 0)
)
)Figure 8.1 presents four stylized distributions of party locations over the ideological spectrum, each overlaid with a uniform distribution used as a benchmark for measuring dispersion. The Earth Mover’s Distance (EMD) values shown in each plot quantify how far each distribution deviates from the uniform reference. The figure illustrates that EMD is lower when parties are more evenly spread and seat shares are balanced. The top-left panel, with parties evenly distributed across four positions, yields the lowest EMD (1.23). In contrast, the bottom-right panel, where all parties are concentrated at the center, produces the highest EMD (2.73). The figure also reports Dalton-style polarization scores for each case, which, unlike EMD, capture the extent to which parties are located near the ideological extremes. Consistent with the distinction between the two concepts, the most polarized system (bottom-left) is not the most dispersed, and the most concentrated system (bottom-right) is the least polarized. This visualization underscores the chapter’s argument that dispersion and polarization are related but conceptually and empirically distinct.
We correlate Dalton’s polarization score and our preferred EMD score.
The unit of analysis changes here. Up to this point
the data have been party-level: one row per party per election. EMD and
polarization, though, are properties of a whole system, and so
is TDE. This block therefore merges in the country-level variables and
then collapses to one row per country-year, keeping the party-level
version separately as dta1 for later use.
Watch which object the figures below use. dta is
country-level from here on; dta1 remains party-level.
# Convert 'year' from factor to numeric for merging. The double conversion is
# necessary: as.numeric() on a factor returns its internal codes (1, 2, 3...)
# rather than the years, so it must be sent through as.character() first.
dta$year <- as.numeric(as.character(dta$year)) # Convert 'year' from factor to numeric for merging
# Merge party-level dataset with country-year level V-Dem variables
# by = c("country", "year") joins on the two columns together, so each party
# receives the values belonging to its own country in its own election year.
# all.x = T keeps every row of the party data even where no match is found.
dta <- merge(dta, vdem, by = c("country", "year"), all.x = T)
# Merge with another dataset (likely contains TDE, personalism, and system family variables)
# This is data2export, the country-level TDE and AP scores loaded at the top.
# No all.x here, so this merge DROPS parties whose country-year has no TDE
# score -- an intentional narrowing of the sample to systems we can score.
dta <- merge(dta, data2export, by = c("country", "year"))
# Keep only observations that correspond to the first election in a given year
dta <- dta %>%
dplyr::filter(election_count == 1)
# Save the full (party-level) dataset as dta1
dta1 <- dta # Party-level data
# Now create the country-level dataset:
# Keep only one row per country-year with relevant variables
# starts_with("totalEff") selects every column whose name begins with that
# string -- the five TDE predictions plus their average -- without listing them
# one by one. distinct() then removes the duplicate rows created by having had
# one row per party, leaving a single row per country-year.
dta <- dta %>% # country-level data
dplyr::select(
country, year, emd, polarization,
family, democracy, regionalism,
presidentialism, starts_with("totalEff"),
starts_with("pers")
) %>%
distinct(.keep_all = TRUE)
# Relabel the electoral system family for clarity in plots
dta$family[dta$family == "maj"] <- "Majoritarian"
dta$family[dta$family == "mixed"] <- "Mixed"
dta$family[dta$family == "pr"] <- "PR"
# Clean environment: remove everything except 'dta' and 'dta1'
# setdiff(ls(), c(...)) lists every object in memory EXCEPT the two named, and
# rm() deletes them. This frees the large raw datasets and guarantees that
# nothing below can accidentally depend on an intermediate object.
rm(list = setdiff(ls(), c("dta", "dta1")))How to read Figure 8.2, panel (a). Each point is one election, with Dalton’s polarization on one axis and EMD on the other. The question is how tightly they move together.
A weak or messy relationship is the result the chapter wants, because it establishes that the two measures are not interchangeable — a system can be polarized without being concentrated, and concentrated without being polarized. Had the two lined up neatly, there would be no case for introducing EMD at all. Remember the direction: high EMD means parties are concentrated, not spread.
# Display the correlation matrix between polarization and EMD
with(dta, cor(cbind(polarization, emd)))## polarization emd
## polarization 1.000 -0.753
## emd -0.753 1.000
# Generate a scatterplot with a linear fit line
fig8.2a <- ggplot(dta, aes(x = polarization, y = emd)) +
geom_point() + # individual observations
geom_smooth(aes(color = NULL), method = "lm", se = TRUE) + # regression line with CI
theme_bw() +
labs(
x = "Dalton's Polarization Score",
y = "Earth Mover's Distance (EMD)"
) +
# Add a correlation label to the top-left of the plot
annotate("text",
x = 0.3, y = 3.3,
label = paste0(
"corr = ",
round(
with(
dta,
cor(polarization, emd)
),
2
)
),
hjust = 1, vjust = 1.1, size = 5
) +
# Adjust text sizes for aesthetics
theme(
plot.title = element_text(hjust = 0.5),
axis.text = element_text(size = 12),
axis.title = element_text(size = 14)
)
# pdf("corr_emd_polarization.pdf", h=6.71, w=8.2)
fig8.2aThe left panel of Figure 8.2 shows the bivariate relationship between Dalton’s polarization score and our preferred measure of ideological dispersion, the Earth Mover’s Distance (EMD). The strong negative correlation (–0.75) confirms that systems with low polarization scores tend to be highly concentrated (high EMD), while those with higher polarization scores are typically more dispersed (low EMD). This result is not surprising, since party systems in which all parties cluster at the center are necessarily low in both polarization and dispersion. However, the chapter emphasizes that dispersion and polarization are analytically distinct: while many systems fall along the diagonal, others exhibit high polarization but low dispersion—such as party systems with two dominant parties at opposing poles and no centrist alternatives.
We can see a number of examples that have high polarization scores, but also high EMD scores, which means that these are party systems that are not very evenly distributed. For example, with polarization scores around 2, we see countries like Switzerland (1995) with a very low EMD score of 1.19, and countries like Ukraine (1994) with a very high EMD score of 3.01. Conversely, among countries with low EMD score around 1.5, we see countries with middling polarization (1.47) like Sweden (2014), and countries with high polarization (2.15) like Nicaragua (1990).
How to read Figure 8.2, panel (b). Panel (a) made the statistical point; this panel makes it concrete by naming specific countries. Look for cases that sit far from where a simple correspondence between the two measures would place them — those are the systems that a polarization measure alone would mischaracterise, and they are the reason the chapter proceeds with EMD.
# Identify country-years with similar polarization scores (~2) but very different EMD values
# Example: Switzerland (low EMD), Ukraine (high EMD)
dta[dta$polarization > 2 & dta$polarization < 2.2 & dta$emd < 1.25, ]## country year emd polarization family democracy regionalism
## 231 Cyprus 1970 1.20 2.14 Majoritarian 0.478 0.000
## 917 Switzerland 1991 1.24 2.10 PR 0.889 0.994
## 918 Switzerland 1995 1.19 2.07 PR 0.890 0.994
## presidentialism totalEff.hat1 totalEff.hat2 totalEff.hat3 totalEff.hat4
## 231 1 0.548 0.461 0.493 0.529
## 917 0 0.434 0.552 0.390 0.697
## 918 0 0.434 0.552 0.390 0.697
## totalEff.hat5 totalEff.hat totalEff.hat1.district totalEff.hat2.district
## 231 0.552 0.516 2.07 2.17
## 917 0.365 0.488 1.25 1.25
## 918 0.365 0.488 1.25 1.25
## totalEff.hat3.district totalEff.hat4.district totalEff.hat5.district
## 231 2.06 1.96 2.34
## 917 1.27 1.71 1.18
## 918 1.27 1.71 1.18
## totalEff.hat.district pers.hat1 pers.hat2 pers.hat3 pers.hat4 pers.hat5
## 231 2.12 0.379 0.378 0.379 0.379 0.379
## 917 1.33 0.482 0.512 0.492 0.493 0.508
## 918 1.33 0.482 0.512 0.492 0.493 0.508
## pers.hat pers.hat1.district pers.hat2.district pers.hat3.district
## 231 0.379 0.432 0.452 0.455
## 917 0.497 0.492 0.465 0.495
## 918 0.497 0.492 0.465 0.495
## pers.hat4.district pers.hat5.district pers.hat.district
## 231 0.446 0.431 0.443
## 917 0.487 0.483 0.484
## 918 0.487 0.483 0.484
## country year emd polarization family democracy regionalism
## 980 Ukraine 1994 3.01 2 Majoritarian 0.551 0.572
## presidentialism totalEff.hat1 totalEff.hat2 totalEff.hat3 totalEff.hat4
## 980 1 1.55 1.52 1.5 1.51
## totalEff.hat5 totalEff.hat totalEff.hat1.district totalEff.hat2.district
## 980 1.53 1.52 2.33 2.34
## totalEff.hat3.district totalEff.hat4.district totalEff.hat5.district
## 980 2.24 2.32 2.25
## totalEff.hat.district pers.hat1 pers.hat2 pers.hat3 pers.hat4 pers.hat5
## 980 2.29 0.428 0.428 0.427 0.429 0.424
## pers.hat pers.hat1.district pers.hat2.district pers.hat3.district
## 980 0.427 0.453 0.447 0.459
## pers.hat4.district pers.hat5.district pers.hat.district
## 980 0.452 0.449 0.452
# Identify country-years with similar EMD scores (~1.5) but different polarization values
# Example: Sweden (low polarization), Nicaragua (high polarization)
dta[dta$emd > 1.48 & dta$emd < 1.52 & dta$polarizatio < 1.5, ]## country year emd polarization family democracy regionalism presidentialism
## 911 Sweden 2014 1.51 1.47 PR 0.921 0.886 0
## totalEff.hat1 totalEff.hat2 totalEff.hat3 totalEff.hat4 totalEff.hat5
## 911 0.484 0.694 0.429 0.718 0.558
## totalEff.hat totalEff.hat1.district totalEff.hat2.district
## 911 0.576 1.03 1.14
## totalEff.hat3.district totalEff.hat4.district totalEff.hat5.district
## 911 1.16 1.05 1.01
## totalEff.hat.district pers.hat1 pers.hat2 pers.hat3 pers.hat4 pers.hat5
## 911 1.08 0.466 0.474 0.476 0.449 0.482
## pers.hat pers.hat1.district pers.hat2.district pers.hat3.district
## 911 0.469 0.495 0.493 0.506
## pers.hat4.district pers.hat5.district pers.hat.district
## 911 0.492 0.486 0.494
## country year emd polarization family democracy regionalism
## 232 Cyprus 1976 1.50 2.12 Majoritarian 0.556 0.00
## 500 Italy 1994 1.52 2.02 Mixed 0.836 0.98
## 692 Nicaragua 1990 1.49 2.15 PR 0.637 0.00
## presidentialism totalEff.hat1 totalEff.hat2 totalEff.hat3 totalEff.hat4
## 232 1 0.548 0.461 0.493 0.529
## 500 0 0.612 0.566 0.675 0.595
## 692 1 0.179 0.195 0.323 0.269
## totalEff.hat5 totalEff.hat totalEff.hat1.district totalEff.hat2.district
## 232 0.552 0.516 2.073 2.172
## 500 0.641 0.618 0.947 1.057
## 692 0.206 0.234 0.924 0.913
## totalEff.hat3.district totalEff.hat4.district totalEff.hat5.district
## 232 2.055 1.962 2.341
## 500 0.946 1.088 0.799
## 692 0.922 0.949 0.745
## totalEff.hat.district pers.hat1 pers.hat2 pers.hat3 pers.hat4 pers.hat5
## 232 2.121 0.379 0.378 0.379 0.379 0.379
## 500 0.968 0.413 0.423 0.426 0.446 0.433
## 692 0.890 0.408 0.418 0.426 0.428 0.431
## pers.hat pers.hat1.district pers.hat2.district pers.hat3.district
## 232 0.379 0.432 0.452 0.455
## 500 0.428 0.400 0.407 0.391
## 692 0.422 0.405 0.403 0.393
## pers.hat4.district pers.hat5.district pers.hat.district
## 232 0.446 0.431 0.443
## 500 0.402 0.404 0.401
## 692 0.403 0.398 0.401
# Manually select the four illustrative cases for plotting
country_example <- c("Switzerland", "Ukraine", "Sweden", "Nicaragua")
year_example <- c(1995, 1994, 2014, 1990)
# Set plotting area to 4 rows × 1 column, and adjust margins
par(mfrow = c(4, 1), mar = c(1, 0.5, 2, 0))
# Loop over each selected country-year to generate a minimalist barplot
for (i in 1:length(country_example)) {
# Filter party-level data for the given country and year
tmp_dat <- dta1[dta1$country == country_example[i] & dta1$year == year_example[i], ]
adj.text <- rep(c(-0.1, 0.1), floor(nrow(tmp_dat) / 2)) # Alternate label placement to reduce overlap
plot(c(-4, 4), c(0, 1), axes = F, xlab = "", ylab = "", type = "n") # Initialize empty plot
# Add x-axis line but hide labels for clarity
axis(1, at = c(-4, 4), labels = c(NA, NA))
# Draw vertical bars for each party, where:
# - x-position is ideological location
# - height is mass (seat share)
segments(
x0 = tmp_dat$location, x1 = tmp_dat$location,
y0 = rep(0, nrow(tmp_dat)), y1 = tmp_dat$mass,
lwd = 3
)
# Simplify party labels by removing long suffixes
nice.names <- trimws(str_remove(
tmp_dat$party_name,
"\\s*(/|\\sof\\s).*"
))
# Add rotated party labels beneath each bar
# Use full abbreviations for Nicaragua, simplified names for others
if (i == 4) {
mtext(tmp_dat$party_abbrev,
side = 1, line = -0.5, adj = 0,
at = tmp_dat$location + adj.text,
las = 2, cex = 0.6
)
} else {
mtext(nice.names,
side = 1, line = -0.5, adj = 0,
at = tmp_dat$location + adj.text,
las = 2, cex = 0.6
)
}
# Add country and year as plot title
title(
paste(unique(tmp_dat$country),
unique(tmp_dat$year),
sep = " "
),
adj = 0, line = -1
)
}The right panel of Figure 8.2 illustrates four real-world examples that help disentangle the relationship between dispersion and polarization. Switzerland (1995) and Ukraine (1994) both exhibit similar levels of polarization—around 2—but differ significantly in terms of ideological dispersion. Switzerland shows a wide spread of parties across the spectrum with relatively balanced representation, resulting in a low EMD score of 1.19. In contrast, Ukraine’s party system is concentrated at the extremes, with no parties in the center, leading to a high EMD score of 3.01. Conversely, Sweden (2014) and Nicaragua (1990) both have EMD scores around 1.5, indicating moderate dispersion, but differ in polarization: while Sweden’s parties are more clustered near the center, Nicaragua’s party system is divided between two distant poles, resulting in a higher polarization score. These examples reinforce the argument that dispersion (EMD) and polarization are related but analytically distinct concepts.
Supplementary material begins here. This file contains four figures that do not appear in the printed chapter, interleaved with those that do. They are kept because they are informative, but a reader following the book should know which is which.
| Section | In the chapter? |
|---|---|
| Boxplot of EMD by electoral system family | No |
| Figure 8.3: TDE vs. EMD | Yes |
| Scatterplot of TDE vs. Dalton’s polarization | No |
| EMD vs. ideological range | No |
| Polarization vs. ideological range | No |
| Table 8.1, Figure 8.4, substantive predictions | Yes |
Gathering the four supplementary plots at the end of the file would make the published sequence easier to follow. That is a reordering of sections rather than a change to any R code, and it has been left alone pending a decision.
As a supplementary visualization, we explore how party system dispersion varies by electoral system type.
# Create a ggplot object using electoral family (PR, Mixed, Majoritarian) as x-axis
# and EMD (dispersion score) as the y-axis
gg1 <- ggplot(dta, aes(x = family, y = emd)) +
# Draw boxplots filled in light grey for each family type
geom_boxplot(fill = "lightgrey") +
# Add a horizontal dashed line at the mean EMD value across all systems
geom_hline(yintercept = mean(dta$emd), linetype = "dashed") +
theme_bw(base_size = 20) +
# Flip axes so family types appear on the y-axis and EMD on the x-axis
coord_flip() +
labs(x = "", y = "Earth Mover's Distance (EMD)")This boxplot illustrates how party system dispersion—measured by Earth Mover’s Distance (EMD)—varies across electoral system types. Majoritarian systems tend to exhibit higher EMD scores, indicating more concentrated ideological distributions. In contrast, proportional representation (PR) systems display lower median EMD values, consistent with greater ideological dispersion. Mixed systems fall in between but also show considerable variation. The dashed horizontal line marks the mean EMD across all observations. This visualization aligns with the theoretical expectation that more permissive systems (lower TDE) foster more dispersed party systems.
Figure 8.3 plots the relationship between the Total Duvergerian Effect (TDE) and Earth Mover’s Distance (EMD) across all electoral systems in the dataset. Each point represents a country-year, with electoral system families distinguished by color.
How to read Figure 8.3. This is the chapter’s core bivariate result: TDE on one axis, EMD on the other, one point per election.
The predicted pattern is a positive slope, and the direction of EMD is what makes that the right expectation. High TDE means a constraining system; high EMD means parties are concentrated rather than spread. So constraining systems should sit toward the upper right and permissive systems toward the lower left. If you find yourself expecting a negative slope, re-read the note on EMD above — the measure counts distance from evenness, not spread itself.
This is a raw association with nothing held constant. Table 8.1 is where controls enter.
# pdf(file = 'correlation_EMD_v_TDE.pdf')
# Begin a ggplot using TDE (totalEff.hat) on x-axis and EMD on y-axis
# Color points by electoral system family (PR, Mixed, Majoritarian)
ggplot(dta, aes(x = totalEff.hat, y = emd, color = family)) +
geom_point() + # Plot each country-year as a point
# Add a linear regression line (pooled across system types, se = TRUE shows the confidence interval)
geom_smooth(aes(color = NULL), method = "lm", se = TRUE) +
# Add axis and plot title labels
labs(x = "TDE", y = "Earth Mover's Distance (EMD)", title = "Scatterplot of EMD vs TDE") +
# Manually assign colors to each family (black, light gray, dark gray)
scale_color_manual(values = c("black", "lightgray", "darkgray")) +
# Add correlation coefficient between TDE and EMD to the plot
annotate("text", x = 0.3, y = 3.3, label = paste0("corr=", round(with(dta, cor(totalEff.hat, emd)), 2)), hjust = 1.1, vjust = 1.1) +
theme(plot.title = element_text(hjust = 0.5)) # Center the plot titleFigure 8.3 presents a scatterplot showing the relationship between the Total Duvergerian Effect (TDE) and the Earth Mover’s Distance (EMD) across all electoral systems in the dataset. The figure reveals a positive association between the two variables: as TDE increases, indicating more constraining electoral systems, EMD also tends to rise, suggesting more concentrated distributions of party positions. This supports the chapter’s theoretical expectation that permissive systems (low TDE) foster greater ideological dispersion among parties, while constraining systems (high TDE) lead to ideological concentration. The correlation between TDE and EMD is modest (0.31), and the plot displays substantial variation around the trend line. This variation reflects the fact that electoral incentives are not deterministic—systems with similar TDE scores can yield different configurations of party locations depending on other contextual factors. The figure illustrates that while TDE helps explain patterns of dispersion, it does not fully determine them.
This scatterplot explores the empirical relationship between the Total Duvergerian Effect (TDE) and Dalton’s measure of polarization.
# Create a scatterplot of TDE (x-axis) vs. Dalton's polarization score (y-axis), coloring points by electoral system family
ggplot(dta, aes(x = totalEff.hat, y = polarization, color = family)) +
geom_point() + # Add the raw data points
# Add a pooled linear regression line with confidence interval
geom_smooth(aes(color = NULL), method = "lm", se = TRUE) +
theme_bw() +
# Label axes and title
labs(x = "TDE", y = "Dalton's Polarization", title = "Scatterplot of polarization vs TDE") +
# Manually define color scheme for electoral system types
scale_color_manual(values = c("black", "lightgray", "darkgray")) +
# Annotate the correlation value between TDE and polarization
annotate("text", x = 0.3, y = 3.3, label = paste0("corr=", round(with(dta, cor(totalEff.hat, polarization)), 2)), hjust = 1.1, vjust = 1.1) +
# Center the plot title
theme(plot.title = element_text(hjust = 0.5))The plot reveals a weak negative correlation (–0.17), suggesting that more permissive electoral systems (lower TDE) are slightly more likely to exhibit higher polarization, though the association is modest and the spread of observations is wide. This result reinforces the chapter’s core claim that polarization and dispersion are conceptually distinct: while TDE helps explain patterns of party system dispersion (as shown in Figure 8.3), it appears to play a much weaker role in shaping how ideologically extreme party systems are.
This figure examines the empirical relationship between a party system’s ideological range (the distance between its most left-leaning and most right-leaning parties) and the Earth Mover’s Distance (EMD).
# Group party-level data by country and year
dta1 %>%
group_by(country, year) %>%
# For each country-year, compute the ideological range:
# max position (rightmost party) - min position (leftmost party)
mutate(ran = max(location) - min(location)) %>%
distinct(.keep_all = TRUE) %>% # Retain one row per country-year (collapse to system-level)
# Begin plotting: x-axis is EMD, y-axis is ideological range
ggplot(aes(x = emd, y = ran)) +
geom_point() + # Plot raw points
geom_smooth(method = "lm") + # Add linear regression line with confidence interval
theme_bw() +
# Add axis labels
labs(x = "EMD", y = "Ideological range (position of rightmost - leftmost party)")The plot shows a strong negative correlation, indicating that systems with higher dispersion (higher EMD) tend to have narrower ideological ranges, while systems where parties are spread far apart ideologically tend to have lower EMD scores. This supports the chapter’s argument that dispersion is not simply about how far apart parties are, but about how evenly and widely they are distributed across the ideological space.
This plot explores the relationship between Dalton’s measure of polarization and the ideological range of party systems.
# Start with party-level data
dta1 %>% group_by(country, year) %>% # Group by country and year to operate at the system level
mutate(ran = max(location) - min(location)) %>% # Compute the ideological range
distinct(.keep_all = TRUE) %>% # Retain only one row per country-year (collapse)
# Plot Dalton's polarization (x-axis) vs. ideological range (y-axis)
ggplot(aes(x = polarization, y = ran)) +
geom_point() + # Add points for each observation
geom_smooth(method = "lm") + # Add linear regression line with confidence interval
theme_bw() +
# Axis labels
labs(x = "Dalton's Polarization", y = "Ideological range (position of rightmost - leftmost party)")The strong positive association suggests that, unsurprisingly, polarization tends to increase as parties are positioned farther apart from one another ideologically. However, the chapter argues—and this figure helps to illustrate—that polarization captures how far parties are from the center, not how evenly they are distributed across the space. Thus, while polarization and range are closely related, range alone cannot substitute for a more nuanced measure of dispersion such as Earth Mover’s Distance (EMD).
Table 8.1 reports the results of several linear regression models estimating the relationship between the Total Duvergerian Effect (TDE) and party system dispersion, measured by the Earth Mover’s Distance (EMD).
How to read Table 8.1. Four columns, getting stricter left to right:
| Controls | Year & country fixed effects | |
|---|---|---|
| (1) | none | no |
| (2) | none | yes |
| (3) | democracy, regionalism, presidentialism | no |
| (4) | all three | yes |
Read along the TDE row first. A positive coefficient means constraining systems produce more concentrated party locations — remember that high EMD is concentration, not spread. If it survives into column (4), the association holds even after absorbing everything fixed about each country and each year.
Columns (2) and (4) are demanding: country fixed effects mean the estimate rests on change within a country over time, so a system that never reformed contributes nothing to identifying the effect. A coefficient that shrinks there is not necessarily a failure — it is a narrower question being asked.
Check the AP row as a placebo. The chapter’s argument concerns interparty incentives, so the intraparty measure should not do much work here.
Five datasets, made by subtraction. Elsewhere in the
book the five sets of GBM predictions arrive as five separate datasets.
Here they arrive as five pairs of columns in one table, so the
five analysis datasets are built by keeping one pair and discarding the
other eight columns, then renaming the survivors to the neutral names
TDE and AP.
That is what the five near-identical blocks below do. Reading one is
enough: the minus sign inside select() removes columns, and
rename(new = old) gives the remaining pair the names the
model formulas expect. The models can then be written once and applied
to all five.
# For each of the 5 imputed datasets, select the relevant TDE and AP variables,
# rename them consistently, and drop the others.
# This prepares a list of 5 cleaned data frames, one per imputation.
# Keep prediction set 1: drop columns 2-5 of each pair, then rename what remains.
smallData1 <- dta %>%
dplyr::select(-c(pers.hat2, pers.hat3, pers.hat4, pers.hat5, totalEff.hat2, totalEff.hat3, totalEff.hat4, totalEff.hat5)) %>%
rename(AP = pers.hat1, TDE = totalEff.hat1)
smallData2 <- dta %>%
dplyr::select(-c(pers.hat1, pers.hat3, pers.hat4, pers.hat5, totalEff.hat1, totalEff.hat3, totalEff.hat4, totalEff.hat5)) %>%
rename(AP = pers.hat2, TDE = totalEff.hat2)
smallData3 <- dta %>%
dplyr::select(-c(pers.hat1, pers.hat2, pers.hat4, pers.hat5, totalEff.hat1, totalEff.hat2, totalEff.hat4, totalEff.hat5)) %>%
rename(AP = pers.hat3, TDE = totalEff.hat3)
smallData4 <- dta %>%
dplyr::select(-c(pers.hat1, pers.hat2, pers.hat3, pers.hat5, totalEff.hat1, totalEff.hat2, totalEff.hat3, totalEff.hat5)) %>%
rename(AP = pers.hat4, TDE = totalEff.hat4)
smallData5 <- dta %>%
dplyr::select(-c(pers.hat1, pers.hat2, pers.hat3, pers.hat4, totalEff.hat1, totalEff.hat2, totalEff.hat3, totalEff.hat4)) %>%
rename(AP = pers.hat5, TDE = totalEff.hat5)
allData <- list(smallData1, smallData2, smallData3, smallData4, smallData5)
## No covariates model
# Run the same linear regression on each imputed dataset:
# EMD as a function of TDE and AP (no controls)
lm1 <- lm(emd ~ AP + TDE, data = allData[[1]])
lm2 <- lm(emd ~ AP + TDE, data = allData[[2]])
lm3 <- lm(emd ~ AP + TDE, data = allData[[3]])
lm4 <- lm(emd ~ AP + TDE, data = allData[[4]])
lm5 <- lm(emd ~ AP + TDE, data = allData[[5]])
list0 <- list(lm1, lm2, lm3, lm4, lm5)
out2 <- summary(mice::pool(list0)) # Pool the results using Rubin's rules via `mice::pool`
# Use lm3 as a template to overwrite the pooled coefficients and standard errors
# Replace them in a lm file, we will use a copy of lm3
lm_c1 <- lm3
# Get Betas and SE
se1 <- out2$std.error
for (i in 1:nrow(out2)) {
lm_c1[["coefficients"]][[i]] <- out2$estimate[i]
}
## Year and country fixed-effects Models
# Add fixed effects for year and country to the baseline model
lm1 <- lm(emd ~ AP + TDE + as.factor(year) + country, data = allData[[1]])
lm2 <- lm(emd ~ AP + TDE + as.factor(year) + country, data = allData[[2]])
lm3 <- lm(emd ~ AP + TDE + as.factor(year) + country, data = allData[[3]])
lm4 <- lm(emd ~ AP + TDE + as.factor(year) + country, data = allData[[4]])
lm5 <- lm(emd ~ AP + TDE + as.factor(year) + country, data = allData[[5]])
list0 <- list(lm1, lm2, lm3, lm4, lm5)
out2 <- summary(mice::pool(list0))
# Replace them in a lm file, we will use a copy of lm3
lm_c2 <- lm3
# Get Betas and SE
se2 <- out2$std.error[1:3] # Extract SEs for TDE, AP, Constant
for (i in 1:nrow(out2)) {
lm_c2[["coefficients"]][[i]] <- out2$estimate[i]
}
## Democracy control Model
# Add control for democracy (no fixed effects)
lm1 <- lm(emd ~ AP + TDE + democracy, data = allData[[1]])
lm2 <- lm(emd ~ AP + TDE + democracy, data = allData[[2]])
lm3 <- lm(emd ~ AP + TDE + democracy, data = allData[[3]])
lm4 <- lm(emd ~ AP + TDE + democracy, data = allData[[4]])
lm5 <- lm(emd ~ AP + TDE + democracy, data = allData[[5]])
list0 <- list(lm1, lm2, lm3, lm4, lm5)
out2 <- summary(mice::pool(list0))
# Replace them in a lm file, we will use a copy of lm3
lm_c3 <- lm3
# Get Betas and SE
se3 <- out2$std.error # Only retain SEs for main coefficients
for (i in 1:nrow(out2)) {
lm_c3[["coefficients"]][[i]] <- out2$estimate[i]
}
## Democracy control with year FE model
lm1 <- lm(emd ~ AP + TDE + democracy + as.factor(year), data = allData[[1]])
lm2 <- lm(emd ~ AP + TDE + democracy + as.factor(year), data = allData[[2]])
lm3 <- lm(emd ~ AP + TDE + democracy + as.factor(year), data = allData[[3]])
lm4 <- lm(emd ~ AP + TDE + democracy + as.factor(year), data = allData[[4]])
lm5 <- lm(emd ~ AP + TDE + democracy + as.factor(year), data = allData[[5]])
list0 <- list(lm1, lm2, lm3, lm4, lm5)
out2 <- summary(mice::pool(list0))
# Replace them in a lm file, we will use a copy of lm3
lm_c4 <- lm3
# Get Betas and SE
se4 <- out2$std.error[1:4]
for (i in 1:nrow(out2)) {
lm_c4[["coefficients"]][[i]] <- out2$estimate[i]
}
# All covariates without FE
lm1 <- lm(emd ~ AP + TDE + democracy + regionalism + presidentialism, data = allData[[1]])
lm2 <- lm(emd ~ AP + TDE + democracy + regionalism + presidentialism, data = allData[[2]])
lm3 <- lm(emd ~ AP + TDE + democracy + regionalism + presidentialism, data = allData[[3]])
lm4 <- lm(emd ~ AP + TDE + democracy + regionalism + presidentialism, data = allData[[4]])
lm5 <- lm(emd ~ AP + TDE + democracy + regionalism + presidentialism, data = allData[[5]])
list0 <- list(lm1, lm2, lm3, lm4, lm5)
out2 <- summary(mice::pool(list0))
# Replace them in a lm file, we will use a copy of lm3
lm_c5 <- lm3
# Get Betas and SE
se5 <- out2$std.error
for (i in 1:nrow(out2)) {
lm_c5[["coefficients"]][[i]] <- out2$estimate[i]
}
# All covariates with FE
lm1 <- lm(emd ~ AP + TDE + democracy + regionalism + presidentialism + as.factor(year) + country, data = allData[[1]])
lm2 <- lm(emd ~ AP + TDE + democracy + regionalism + presidentialism + as.factor(year) + country, data = allData[[2]])
lm3 <- lm(emd ~ AP + TDE + democracy + regionalism + presidentialism + as.factor(year) + country, data = allData[[3]])
lm4 <- lm(emd ~ AP + TDE + democracy + regionalism + presidentialism + as.factor(year) + country, data = allData[[4]])
lm5 <- lm(emd ~ AP + TDE + democracy + regionalism + presidentialism + as.factor(year) + country, data = allData[[5]])
list0 <- list(lm1, lm2, lm3, lm4, lm5)
out2 <- summary(mice::pool(list0))
# Replace them in a lm file, we will use a copy of lm3
lm_c6 <- lm3
# Get Betas and SE
se6 <- out2$std.error[1:6]
for (i in 1:nrow(out2)) {
lm_c6[["coefficients"]][[i]] <- out2$estimate[i]
}
# Present the results from four models in a table with consistent formatting
# ---------------------------------------------------------------------------
# The four columns, in order:
# (1) lm_c1 EMD ~ AP + TDE no controls, no FE
# (2) lm_c2 ... + year and country fixed effects no controls, FE
# (3) lm_c5 ... + democracy + regionalism + presidentialism controls, no FE
# (4) lm_c6 controls AND fixed effects controls, FE
#
# `se = list(...)` supplies the POOLED standard errors computed above. This
# matters: the coefficients stored in lm_c1 etc. were overwritten with pooled
# values, but their internal standard errors were not, so stargazer must be
# handed the correct ones explicitly or it would report the errors from a
# single imputation.
#
# type = "html" makes stargazer emit a real HTML table. It previously read
# type = "text", which produces an ASCII table aligned with spaces; because the
# chunk is set to results='asis', that text was passed straight into the page
# and the browser collapsed the spacing, leaving numbers with no columns.
#
# add.lines takes a LIST OF VECTORS, one vector per extra row: the first element
# is the row label and the rest are the cell values, one per model. It was
# previously given a single LaTeX-style string with six values for four models.
# ---------------------------------------------------------------------------
stargazer::stargazer(lm_c1, lm_c2, lm_c5, lm_c6,
se = list(se1, se2, se5, se6),
keep = c("TDE", "AP", "democracy", "regionalism", "presidentialism", "Constant"),
type = "html",
no.space = TRUE,
dep.var.labels = "Earth Mover's Distance (EMD)",
add.lines = list(
c("Year fixed-effects", "No", "Yes", "No", "Yes"),
c("Country fixed-effects", "No", "Yes", "No", "Yes")
)
)| Dependent variable: | ||||
| Earth Mover’s Distance (EMD) | ||||
| (1) | (2) | (3) | (4) | |
| AP | -1.410*** | -0.148 | -0.500** | 0.037 |
| (0.308) | (0.367) | (0.241) | (0.348) | |
| TDE | 0.244*** | 0.108*** | 0.140*** | 0.073** |
| (0.030) | (0.039) | (0.024) | (0.037) | |
| democracy | -0.848*** | -0.614*** | ||
| (0.054) | (0.081) | |||
| regionalism | -0.013 | 0.066 | ||
| (0.030) | (0.057) | |||
| presidentialism | 0.102*** | 0.070* | ||
| (0.022) | (0.037) | |||
| Constant | 2.540*** | 2.200*** | 2.680*** | 2.310*** |
| (0.140) | (0.182) | (0.109) | (0.174) | |
| Year fixed-effects | No | Yes | No | Yes |
| Country fixed-effects | No | Yes | No | Yes |
| Observations | 1,053 | 1,053 | 1,052 | 1,052 |
| R2 | 0.136 | 0.713 | 0.362 | 0.732 |
| Adjusted R2 | 0.135 | 0.655 | 0.359 | 0.676 |
| Residual Std. Error | 0.408 (df = 1050) | 0.258 (df = 874) | 0.351 (df = 1046) | 0.249 (df = 870) |
| F Statistic | 83.000*** (df = 2; 1050) | 12.200*** (df = 178; 874) | 119.000*** (df = 5; 1046) | 13.100*** (df = 181; 870) |
| Note: | p<0.1; p<0.05; p<0.01 | |||
Across all model specifications showed in Table 8.1, TDE is positively and significantly associated with EMD, supporting the chapter’s claim that more constraining electoral systems tend to produce more ideologically concentrated party systems. The effect is strongest in the bivariate model (Column 1) and remains statistically significant, though substantively smaller, after controlling for average personalism (AP), electoral democracy, regionalism, presidentialism, and fixed effects for country and year. Notably, AP is only negatively associated with EMD in the bivariate model, and this relationship disappears once controls are added. These results reinforce the chapter’s argument that electoral system features, particularly TDE, help shape the extent to which parties spread out or cluster ideologically across systems.
To better illustrate the substantive relationship between electoral system permissiveness and party system dispersion, we compute predicted values of EMD across the observed range of TDE scores. These predictions are based on the regression model in Column 3 of Table 8.1, which includes controls for personalism, democracy, regionalism, and presidentialism. The resulting figure provides a visual summary of how changes in TDE are associated with changes in the overall ideological spread of party systems.
# Generate predicted values of EMD over the range of observed TDE scores
# using model 3 from Table 8.1 (lm_c5), which includes all controls but no fixed effects
# plot_model() from sjPlot takes a fitted model and draws predicted values
# rather than raw data. type = "pred" varies the named term across its observed
# range while holding every other predictor at a typical value -- the same
# "hold all else constant" logic used in the simulation chapters, done here in
# one call. colors = "bw" keeps the figure greyscale for print.
plot_model(lm_c5,
terms = "TDE", # the variable for which we want predicted values
type = "pred", # prediction plot with confidence intervals
colors = "bw"
) +
theme_bw(base_size = 20) +
# scale_color_manual(color = "grey")+
labs(y = "EMD", title = "") # Set the y-axis label to EMD and remove the titleFigure 8.4 displays the predicted values of Earth Mover’s Distance (EMD) across the range of observed Total Duvergerian Effect (TDE) scores, based on the regression model in Column 3 of Table 8.1. The figure shows a clear and statistically significant positive association between TDE and EMD: as TDE increases—indicating more constraining electoral rules—EMD also rises, implying that party systems become more ideologically concentrated. The slope is moderate but meaningful, and the confidence band shows relatively narrow uncertainty around the predicted values. This plot reinforces the chapter’s main claim that permissive electoral systems (with low TDE) generate greater dispersion in the ideological positioning of parties, while more constraining systems lead to ideological clustering.
How to read Figure 8.4. A single line with a confidence band, showing expected EMD as TDE moves across its observed range, with personalism, democracy, regionalism and presidentialism held at typical values.
Two things to judge. First the direction: an upward slope means constraining systems produce more concentrated party locations, which is the chapter’s claim. Second the magnitude — read the vertical distance between the ends of the line and ask whether a change of that size in EMD is substantively interesting, not merely statistically detectable. The next section answers that by naming two real systems.
To illustrate the substantive impact of TDE on party system dispersion, we predicted EMD scores for two real-world cases while holding all other variables at their sample means.
# Predicted EMD for Switzerland using its observed TDE (= 0.488)
# All other variables (AP, democracy, regionalism, presidentialism) held at sample means
Switz <- predict(lm_c5, data.frame(
Intercept = 1,
AP = mean(dta$pers.hat),
TDE = unique(dta$totalEff.hat[dta$country == "Switzerland"]),
democracy = mean(dta$democracy),
regionalism = mean(dta$regionalism, na.rm = TRUE),
presidentialism = mean(dta$presidentialism)
),
interval = "confidence"
)
# Predicted EMD for Mexico in 2006 (TDE = 1.15)
Mexico <- predict(lm_c5, data.frame(
Intercept = 1,
AP = mean(dta$pers.hat),
TDE = unique(dta$totalEff.hat[dta$country == "Mexico" & dta$year == 2006]),
democracy = mean(dta$democracy),
regionalism = mean(dta$regionalism, na.rm = TRUE),
presidentialism = mean(dta$presidentialism)
),
interval = "confidence"
)
# Display predicted EMD values (point estimate + confidence interval), rounded to 2 decimals
round(Switz, 2)## fit lwr upr
## 1 2.06 2.03 2.09
## fit lwr upr
## 1 2.15 2.13 2.18
# Compute the standardized difference in predicted EMD between Mexico and Switzerland
# (in units of the standard deviation of EMD)
(Mexico[1] - Switz[1]) / sd(dta$emd)## [1] 0.211
## Predicting Impact of Full Range of TDE
# Predicted EMD for lowest observed TDE in the dataset
min.tde <- predict(lm_c5, data.frame(
Intercept = 1,
AP = mean(dta$pers.hat),
TDE = min(dta$totalEff.hat),
democracy = mean(dta$democracy),
regionalism = mean(dta$regionalism, na.rm = TRUE),
presidentialism = mean(dta$presidentialism)
),
interval = "confidence"
)
# Predicted EMD for highest observed TDE in the dataset
max.tde <- predict(lm_c5, data.frame(
Intercept = 1,
AP = mean(dta$pers.hat),
TDE = max(dta$totalEff.hat),
democracy = mean(dta$democracy),
regionalism = mean(dta$regionalism, na.rm = TRUE),
presidentialism = mean(dta$presidentialism)
),
interval = "confidence"
)
# Compute the standardized difference in predicted EMD across the full TDE range
(max.tde[1] - min.tde[1]) / sd(dta$emd)## [1] 0.623
For Switzerland, a country with a relatively low TDE (0.488), the predicted EMD is 2.06 (95% CI: 2.03–2.09). In contrast, for Mexico in 2006—a case with a much higher TDE (1.15)—the predicted EMD increases to 2.15 (95% CI: 2.13–2.18). This difference amounts to roughly 0.21 standard deviations of the EMD distribution in our sample. A shift from the minimum to the maximum observed TDE values yields a predicted change in EMD of 0.27, equivalent to about 62% of one standard deviation. These results underscore that even modest changes in electoral permissiveness can have a meaningful effect on how ideologically dispersed or concentrated party systems are.
Chapter 8 has examined how electoral system incentives, captured by the Total Duvergerian Effect (TDE), shape the distribution of partisan ideological locations along the left–right continuum. Using a novel dispersion measure based on the earth mover’s distance (EMD), we found that stronger, more constraining electoral systems tend to produce party systems where parties are ideologically concentrated, while weaker, more permissive systems encourage a more dispersed spread of party positions. Importantly, our analysis distinguished dispersion from polarization, showing that dispersion reflects the evenness of party ideological spread rather than clustering at ideological extremes.
This chapter builds on the earlier discussion in Chapter 7 on party system size by moving beyond the number of parties to consider where parties position themselves in ideological space, adding depth to our understanding of interparty political competition. The findings underscore the complexity of electoral incentives: the strength of a system not only shapes how many parties compete but also where they locate ideologically.
Looking ahead, Chapter 9 will extend this investigation by examining the relationship between party positions and voter preferences—known as congruence. It will explore whether electoral systems that encourage dispersed or concentrated party locations also foster closer alignment between parties and their constituents. This next step will connect the interparty ideological dynamics explored here with the crucial question of how well citizens’ preferences are represented in practice.
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.
| 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 |
| Package | Version |
|---|---|
| dplyr | 1.1.4 |
| emdist | 0.3-3 |
| forcats | 1.0.0 |
| ggplot2 | 3.5.2 |
| ggrepel | 0.9.6 |
| gridExtra | 2.3 |
| lubridate | 1.9.4 |
| purrr | 1.2.2 |
| readr | 2.1.5 |
| sjPlot | 2.9.0 |
| stringr | 1.5.1 |
| tibble | 3.3.0 |
| tidyr | 1.3.1 |
| tidyverse | 2.0.0 |
The record for this chapter survives in the originally rendered page: R 4.3.1 (2023-06-16) on macOS 15.4.1 (aarch64, darwin20), knitted on 18 May 2025 with pandoc 3.1.1.
Where a record does survive, it is reproduced in full in
RECOVERED-SessionInformation.md, alongside these files.