Understanding the Mathematics Behind NZ Elections
Introduction
New Zealand held a general election on 7 November 2026. Like many voters, I cast my vote and became curious about how the election system works behind the scenes, especially the technical process used to allocate seats in Parliament based on the election results. In this note, I discuss some of the fascinating mathematics that underpins New Zealand’s electoral system.
Very brief introduction to NZ elections
- New Zealand holds a general election every three years.
- MMP (Mixed Member Proportional) is New Zealand’s electoral system. In the 2026 election, Members of Parliament (MPs) were elected from 71 electorates (64 general electorates and 7 Māori electorates) and from party lists of parties that received at least 5% of the party vote.
- Voters cast two votes: one for a political party (the party vote) and one for an individual candidate in their electorate (the electorate vote).
- Seats in Parliament (120 in the 2026 election) are allocated to eligible parties in proportion to their share of the party vote.
- Overhang occurs when a party wins more electorate seats than the total number of seats it would otherwise be entitled to based on its share of the party vote (see Reference [1]).
An allocation problem
We formulate the seat-allocation problem in New Zealand elections as follows. Suppose there are $ S $ seats (e.g. $ S=120 $). We wish to allocate these $ S $ seats among $ m $ parties (e.g. $ m=6 $), which have received $ v_1, v_2, \ldots, v_m $ votes, respectively. The task is to determine the number of seats allocated to party $ i $, denoted by $ s_i $, for $ i=1, 2, \ldots, m $. Without loss of generality, we assume $ v_1\ge v_2\ge \cdots \ge v_m $.
A proportional allocation would give
$$ s_i = S\frac{v_i}{\sum_{i=1}^m v_i},\ i=1,\ 2,\ \ldots,\ m. $$
However, the problem is not quite that simple, because the seat allocations $ s_i $ must be integers.
Next, we formulate the allocation problem as an integer programming problem.
Decision variables: $ s_1,\ s_2,\ \ldots,\ s_m $.
Objective function:
$$ f(s_1, \cdots, s_m) = \sum_{i=1}^m v_i \left(\frac{s_i}{v_i} - \frac{S}{V}\right)^2, $$
where $ V=\sum_{i=1}^m v_i $.Constraints:
- $ \sum_{i=1}^m s_i = S $;
- $ s_1,\ s_2,\ \ldots,\ s_m $ must be integers.
Goal: Minimize $ f(s_1,\ \cdots,\ s_m) $.
Remarks:
-
The objective function above is equivalent to the one presented in [2]. In fact, one of the motivations for writing this note is to help myself, and possibly others, gain a deeper understanding of the Sainte-Laguë method.
-
The objective function can be simplified as
$$ f(s_1, \cdots, s_m) = \sum_{i=1}^m \frac{s_i^2}{v_i} - \frac{S^2}{V}. $$
Therefore, minimizing $ f(s_1, \cdots, s_m) $ is equivalent to minimizing
$$ f^*(s_1, \cdots, s_m) = \sum_{i=1}^m \frac{s_i^2}{v_i}. $$
If Constraint 2 is removed, we can use the method of Lagrange multipliers to obtain the solution
$$ s_i = S\frac{v_i}{\sum_{i=1}^m v_i},\ i=1,\ 2,\ \ldots,\ m. $$
As expected, this is precisely the proportional allocation shown earlier.
We now seek an algorithm for solving the integer programming problem. Inspired by [3], we can rewrite $ f^*(\cdots) $ as
\begin{align} f^*(s_1, \ldots, s_m) = & \sum_{i=1}^m \frac{s_i^2}{v_i}\\ = & \sum_{i=1}^m \frac{1}{v_i}\sum_{j=0}^{s_i-1}(2j+1). \end{align}
Key Insight: To make $ f^*(\cdots) $ as small as possible, we should repeatedly choose the largest value of
$$ \frac{v_i}{2j+1}\ \ \ (\hbox{where}\ j\ge 0). $$
With the above insight, we have the following algorithm (i.e. Sainte-Laguë method):
- Allocate one seat at a step.
- At each step, assign the next seat to the party with the largest value of $ v_i/(2j+1) $, where $ j $ is the number of seats that party $ i $ has already been allocated.
Trying out the allocation algorithm using the 2026 election data
library(data.table)
library(knitr)
allocate_seats <- function(v, S = 120)
{m <- length(v)
s <- integer(m) # initially set seat numbers all 0's
dt <- data.table(id = seq_len(m),
the_gain = - v / 1) # using minus sign for ordering purpose
setkey(dt, the_gain) # order from the largest to the smallest
for (k in seq_len(S)) {
# largest gain index
i <- dt$id[1]
s[i] <- s[i] + 1
# Updated the gains
dt$the_gain[1] <- - v[i] / (2 * s[i] + 1)
setkey(dt, the_gain)
}
s
}
# source of the data: https://www.electionresults.govt.nz/electionresults_2026_preliminary/
vote_result <-
data.table(party = c("National Party", "Labour Party", "Green Party",
"ACT New Zealand", "New Zealand First Party",
"Opportunity Party"), # "Te Pāti Māori"),
votes = c(706680L, 710330L, 321902L, 264121L, 236630L, 144298L)) #, 13845L))
allocation_re <- copy(vote_result)
allocation_re[, `:=`(allocated_seats = allocate_seats(v = votes))]
add_TPM <- data.table(party = "Te Pāti Māori",
votes = 13845L,
allocated_seats = 0L)
allocation_re_with_TPM <- rbind(allocation_re, add_TPM)
allocation_re_with_TPM[, `:=`(electorate_seats = c(42L, 21L, 2L, 1L, 0L, 0L, 5L))]
allocation_re_with_TPM[, `:=`(total_seats = pmax(allocated_seats, electorate_seats))]
timestamp <- Sys.Date()
kable(allocation_re_with_TPM,
caption = sprintf("PROJECTED Number of seats on %s", timestamp),
label = NA)
Table: PROJECTED Number of seats on 2026-11-14
| party | votes | allocated_seats | electorate_seats | total_seats |
|---|---|---|---|---|
| National Party | 706680 | 36 | 42 | 42 |
| Labour Party | 710330 | 36 | 21 | 36 |
| Green Party | 321902 | 16 | 2 | 16 |
| ACT New Zealand | 264121 | 13 | 1 | 13 |
| New Zealand First Party | 236630 | 12 | 0 | 12 |
| Opportunity Party | 144298 | 7 | 0 | 7 |
| Te Pāti Māori | 13845 | 0 | 5 | 5 |
References
[1] Vote in elections the link
[2] Sainte-Laguë method the link
[3] Wright, T. (2017). Exact Optimal Sample Allocation: More Efficient Than Neyman. Statistics and Probability Letters 129 pp. 50-57.