Sitemap

Electoral-sim: A spatial voting simulator for comparing electoral systems

12 min readMar 9, 2026

--

I am not a social scientist nor have I professionally worked in scientific disciplines adjacent to those that study electoral systems. However, I have always had an interest in politics and have found arguments for electoral systems reform quite compelling. So, a few years ago I finally decided to start reading academic papers about various electoral systems and realized immediately that there were so many that I had never heard of. I have only heard of plurality voting and Ranked Choice Voting, till that point. Being a curious person, I became quite curious about testing how these systems work on different types of electorates and couldn’t really find a lot of resources to explore that. So I decided to finally give it a shot myself and write up a simple simulation framework (https://github.com/mukhes3/electoral_sim) for others who might be interested in these types of explorations.

What is electoral_sim?

electoral_sim is an open-source Python simulation framework that lets you compare how different electoral systems perform across different types of electorates. The core idea is simple: rather than studying electoral systems purely in theory, we can simulate them on synthetic populations of voters and measure how well each system’s outcome reflects the preferences of those voters.

The framework is built on a classical idea in political science called the spatial model of voting, which goes back to Harold Hotelling’s 1929 paper on competition and was later formalised by Anthony Downs in his 1957 book An Economic Theory of Democracy. The basic intuition is simple: represent every voter and every candidate as points on a 2D map, where the axes represent two political dimensions, say economic policy and social policy. A voter prefers whichever candidate is closest to them on this map, and an electoral system takes those preferences as input and produces an outcome. The question we can then ask is: how close is that outcome to the centre of the voter population? Different systems give very different answers to that question, and the answer depends heavily on what the voter population looks like. The package is designed to be accessible to anyone who is curious about electoral systems, regardless of their background.

Components of the package

The framework is built around five core components that work together to simulate an election from start to finish.

Electorate. This is your pool of voters. Each voter is represented as a point in a two-dimensional ideological space: the horizontal axis captures economic preferences (left to right) and the vertical axis captures social preferences (libertarian to authoritarian). Voter populations can be specified as Gaussian distributions, mixtures of Gaussians, or uniform distributions — so you can model anything from a tightly clustered consensus electorate to a sharply divided polarised one.

Candidates. Candidates are also points in the same two-dimensional space. Their positions can be fixed manually or sampled from a distribution. The key property is that every voter has a well-defined distance to every candidate, which is what drives all the ballot derivations downstream.

Ballots. Given the voter and candidate positions, the framework automatically derives every ballot type needed to run any of the implemented systems — plurality votes, full preference rankings, approval indicators, and continuous scores. All of these are derived under sincere voting: every voter votes for the candidate closest to their preferences, with no strategic behaviour. They are computed once and stored in a BallotProfile object so that all systems operate on exactly the same underlying data.

Systems. This is the heart of the package. The framework currently implements nine standard electoral systems — from Plurality and IRV to Borda Count, Approval Voting, Score Voting, Condorcet-Schulze, and two proportional representation systems (D’Hondt and MMP). Each system takes the same BallotProfile as input and returns a unified ElectionResult, making cross-system comparison straightforward.

Metrics. Once a system has produced an outcome, the framework evaluates it against the voter distribution. The primary metric is the distance between the electoral outcome and the geometric median of the voter population — the point that minimises the total distance to all voters. Additional metrics include majority satisfaction, mean voter-outcome distance, and a Gini coefficient capturing how unequally the outcome is distributed across the electorate.

Specifying and loading scenarios

Rather than writing Python code every time you want to define a new election context, electoral_sim lets you specify everything in a simple YAML configuration file. A scenario file defines the number of voters, the distribution they are drawn from, and the positions of the candidates. Here is an example:

name: "Polarized Bimodal"
real_world_analog: "Contemporary USA, Brexit-era UK"
n_voters: 5000
electorate:
type: gaussian_mixture
components:
- weight: 0.55
mean: [0.72, 0.58]
std: [0.10, 0.08]
- weight: 0.45
mean: [0.25, 0.38]
std: [0.10, 0.08]
candidates:
- {label: "Far-Right", position: [0.80, 0.75]}
- {label: "Right", position: [0.72, 0.58]}
- {label: "Center", position: [0.50, 0.48]}
- {label: "Left", position: [0.28, 0.42]}
- {label: "Far-Left", position: [0.15, 0.25]}

The package ships with eight scenarios out of the box. All scenario files live in the configs/scenarios/ directory of the repository. Loading a scenario and running all systems takes just a few lines of Python:

import numpy as np
from electoral_sim.scenario import load_scenario
from electoral_sim.systems import get_all_systems
from electoral_sim.metrics import run_simulation

rng = np.random.default_rng(42)
config, electorate, candidates = load_scenario(
"configs/scenarios/02_polarized_bimodal.yaml", rng=rng
)

systems = get_all_systems(rng=rng)
metrics = run_simulation(electorate, candidates, systems)

These have been demonstrated in notebooks/quickstart.ipynb .

Some experiments on different electorates and different electoral systems

Having described the basics of the repo, let me demonstrate how this can be used using some experiments documented in notebooks/electoral_systems_comparison.ipynb . Before diving into the results, let me briefly describe each electoral system I used and each electoral scenario.

The electoral systems

I tested nine real-world systems plus one hypothetical benchmark I designed myself.

Plurality (First Past the Post). The simplest system most of us are familiar with. Every voter picks one candidate, and whoever gets the most votes wins.

Two-Round Runoff. If no candidate gets more than 50% in the first round, the top two candidates face each other in a second round. This gives voters a chance to consolidate around one of the two frontrunners. France uses this system for its presidential elections.

Instant Runoff Voting (IRV / Ranked Choice Voting). Voters rank candidates in order of preference. If no candidate gets a majority of first-choice votes, the last-place candidate is eliminated and their voters’ second choices are redistributed. This continues until someone has a majority. Australia has used this system for over a century.

Borda Count. Voters rank all candidates, and each candidate gets points based on where they appear in each ballot — more points for higher rankings. The candidate with the most total points wins.

Approval Voting. Voters can vote for as many candidates as they want — they simply mark every candidate they find acceptable. The candidate with the most approvals wins.

Score Voting. Voters give each candidate a numerical score, say between 0 and 10. The candidate with the highest average score wins. Think of it like rating movies on a streaming platform, except the highest-rated candidate becomes the winner.

Condorcet — Schulze. This one is a bit more involved. The system runs a hypothetical head-to-head comparison between every pair of candidates — who would win if it were just these two? A candidate who beats every other candidate in these pairwise matchups is called the Condorcet winner and is elected. If no such candidate exists, the Schulze method uses a path-strength algorithm to find the most consistently dominant candidate.

Party-List PR (D’Hondt). Rather than electing a single winner, seats in a legislature are allocated proportionally to the vote share each party receives. It is used in the Netherlands, Spain, and many other countries.

Mixed Member Proportional (MMP). A hybrid system: half the seats are filled by plurality winners in local districts, and the other half are used to top up each party’s representation to match their overall vote share.

Fractional Ballot (hypothetical). This is a system I designed as an experimental benchmark. Rather than casting a single vote, each voter’s influence is distributed across all candidates in proportion to how close each candidate is to their preferences — candidates closer to you get a larger share of your vote. It uses a Boltzmann softmax formula to compute these weights, parameterised by a temperature σ that controls how concentrated the weights are. At low σ it behaves like Plurality; at high σ it spreads weight evenly across all candidates. It is not used anywhere in the real world and would be difficult to implement in practice.

The electoral scenarios

Get Sumit Mukherjee’s stories in your inbox

Join Medium for free to get updates from this writer.

I designed eight scenarios to represent different types of real-world electorates based on my understanding. I want to caveat that I’m not a political scientist, so these may not be completely accurate. Each one places voters in the two-dimensional ideological space described earlier, with different distributional shapes to capture different political contexts.

Unimodal Consensus. Most voters cluster around a single central point with moderate spread. Think of Nordic countries or other consensus democracies where the political centre of gravity is relatively stable and most voters are not too far from each other.

Polarized Bimodal. Voters are split into two distinct clusters with a gap between them — a left bloc and a right bloc with very few voters in the middle. This is the scenario most relevant to contemporary US politics or Brexit-era Britain, where the two sides have moved far apart and the centre has hollowed out.

Multimodal Fragmented. Voters are spread across several distinct clusters with no single dominant group. This mirrors countries like the Netherlands, Israel, or Italy where many parties each represent a distinct constituency.

Dominant Party with Minorities. One large voter cluster dominates, but several smaller minority clusters exist on the periphery. This captures systems like Japan under the LDP or Hungary, where one party has a structural majority but opposition groups persist.

Asymmetric Skewed. The voter distribution is lopsided — one side has more voters but they are more spread out, while the other side is smaller but more tightly clustered. This reflects patterns seen in many Latin American countries.

Two-Party Symmetric Polarized. A stylised version of a mature two-party system where both parties are roughly equal in size and symmetrically placed on opposite sides of the ideological space.

Two-Party Asymmetric Centrist Majority. Similar to the above but with a larger centrist bloc leaning toward one side. This reflects many European two-bloc systems where one coalition has a structural advantage because the centre tilts toward them.

Two-Party Dominant Left. One party has a clear dominant position on the left, with a formal primary process selecting among several left-leaning candidates before facing a more fragmented right.

Comparing different scenarios based on distance to geometric median

We used the geometric median as the standard metric to compare various systems in the different scenarios. The heatmap below shows the distance to geometric median for all systems and scenarios — greener means the outcome was closer to the centre of voter preferences, redder means it was further away. The black border in each column highlights the best performing system for that scenario.

Press enter or click to view image in full size

Setting aside the Fractional Ballot system, we see a few patterns.

Plurality is the most inconsistent system across the board. It does reasonably well in the Consensus scenario (0.120) but much worse in the Two-Party Symmetric Polarized scenario (0.298) and the Polarized scenario (0.232). It seems that when faced with a divided electorate, plurality just elects whoever the largest bloc prefers, completely ignoring everyone else. Two-Round Runoff and Approval Voting follow a very similar pattern.

IRV behaves almost identically to Plurality in most scenarios — they produce the same outcome in six of eight cases. The one place IRV stands out is the Two-Party Dominant Left scenario, where it achieves 0.013, the best result among standard systems. This may be a candidate placement coincidence rather than a structural advantage, as the dominant party’s candidates happen to bracket the geometric median closely.

Borda Count, Score Voting, and Condorcet cluster together and consistently outperform the plurality-based systems. They all produce identical results across most scenarios — these methods tend to agree when voter preferences are reasonably well-structured. Their worst scenario is Fragmented (0.134), where the splintered voter distribution makes it hard for any single candidate to emerge as a clear centre.

The PR systems (D’Hondt and MMP) perform similarly to Borda/Score/Condorcet in most scenarios but notably worse in the Fragmented scenario (0.163 vs 0.134).

Now, lets see what happens when we include the Fractional Ballot system. The Fractional Ballot at σ=0.1 is the best performing system in six of eight scenarios by a wide margin. In the Polarized scenario it achieves 0.004 compared to the next best standard system at 0.028 — a more than six-fold improvement. In the Fragmented scenario it scores 0.020 against 0.134 for the best standard system, nearly a seven-fold improvement. These are not marginal gains.

The two exceptions are telling. In the Dominant scenario the Fractional Ballot at σ=0.1 gets 0.077, which is better than standard systems (0.096) but the gap is much smaller than elsewhere. At higher σ values it gets actively worse — 0.149 at σ=1 — because the large dominant cluster drags the centroid away from the geometric median. This is the core weakness of any centroid-based method: when the biggest group of voters is far from the true centre, averaging pulls the outcome toward them.

The second exception is the Two-Party Dominant Left scenario, where IRV’s 0.013 edges out the Fractional Ballot’s 0.026. As noted above, this is a coincidence of candidate placement rather than something structural.

Again, it’s important to note that the Fractional Ballot system is not quite a practical system to implement because it will be non-trivial to infer the distance between the political positions of voters to the candidates.

Visualizing the results on spatial maps

I created some helper tools to visualize the outcomes of the simulations, that can be found in electoral_sim/utils/. Shown below are the candidates and winners visualized on a 2D spatial map of policy preference for the polarized bimodal scenario.

Press enter or click to view image in full size

Each panel shows the same electorate — the blue density cloud represents where voters are concentrated, with darker regions indicating more voters. The teal cross marks the geometric median, which is what we are trying to get close to. The star marks the elected candidate (or in the case of PR systems, the purple diamond marks the median legislator and the cross marks the seat-share centroid).

The most striking thing about this figure is how clearly you can see the failure of plurality-based systems in a polarised electorate. Plurality, Two-Round Runoff, IRV, and Approval Voting all elect the “Right” candidate — sitting squarely in the centre of the larger right-leaning cluster, far from the geometric median in the middle. They all score d=0.232. The larger blob wins, full stop.

Borda Count, Score Voting, and Condorcet all do something more interesting — they elect the “Center” candidate, which sits much closer to the geometric median (d=0.028). These systems reward candidates who are broadly acceptable across the whole electorate rather than just intensely preferred by the majority cluster.

The PR panels are worth looking at carefully. D’Hondt and MMP both show the median legislator (purple diamond) landing near the Left candidate rather than the Centre — this is the artefact I mentioned earlier, where proportional seat distribution in a two-cluster electorate places the median seat-holder inside one of the clusters rather than between them. The seat-share centroid (the small cross) is actually much closer to the geometric median, which is why I report both metrics for PR systems.

The Fractional Ballot panels at σ=0.1 are visually the most satisfying — the outcome lands almost exactly on the geometric median. You can see how the softmax weighting pulls the result away from both clusters and toward the low-density centre of the distribution.

This and other such results can be reproduced using the notebooks/electoral_systems_comparison.ipynb notebook.

Conclusion

My primary hope with this package is that it makes it easy for anyone curious about electoral systems to actually test their intuitions rather than just argue about them in the abstract. You do not need a background in political science — if you can write a little Python and have an opinion about how elections should work, you can run your own simulations and see what the numbers say.

I am also hopeful that this becomes a starting point rather than an endpoint. There is a lot of room to build on it — more sophisticated voter behaviour like strategic voting, endogenous candidate positioning, higher-dimensional preference spaces, or entirely new electoral mechanisms. If you design a system you think might work better, the framework is built to make that easy to test.

Finally, some of the package details and the experimental results can be found in the companion Arxiv preprint.

--

--

Sumit Mukherjee
Sumit Mukherjee

Written by Sumit Mukherjee

I am ML4Health/Computational Biology researcher with an interest in topics at the intersection of genetics and machine learning.