Skip to content

Vote Counting

Vote counting functions for FPTP and PR electoral systems. Both delegates to Numba-accelerated backends for performance.


count_fptp

First Past The Post: winner takes all in each constituency. Uses Numba parallel acceleration when available.

from electoral_sim.core.counting import count_fptp

result = count_fptp(
    constituencies=constituency_array,
    votes=vote_array,
    n_constituencies=10,
    n_parties=5,
)

Parameters: | Parameter | Type | Description | |-----------|------|-------------| | constituencies | np.ndarray | (n_voters,) constituency assignments per voter | | votes | np.ndarray | (n_voters,) vote choices (party indices) | | n_constituencies | int | Total number of constituencies | | n_parties | int | Number of parties |

Returns: Dict with keys: | Key | Type | Description | |-----|------|-------------| | system | str | Always "FPTP" | | seats | np.ndarray | (n_parties,) seat counts per party | | vote_counts | np.ndarray | (n_parties,) raw vote counts per party | | n_constituencies | int | Number of constituencies |

Under the hood: Calls fptp_count_fast() from electoral_sim.engine.numba_accel, which uses Numba parallel JIT when available, falling back to vectorized NumPy.

Example:

import numpy as np
from electoral_sim.core.counting import count_fptp

# 1000 voters across 10 constituencies, 5 parties
constituencies = np.random.randint(0, 10, 1000)
votes = np.random.randint(0, 5, 1000)

result = count_fptp(constituencies, votes, 10, 5)
print(f"Winning party: {np.argmax(result['seats'])}")
print(f"Seats: {result['seats']}")
print(f"Votes: {result['vote_counts']}")


count_pr

Proportional Representation with seat allocation. Counts votes vectorized and delegates seat allocation to the systems module.

from electoral_sim.core.counting import count_pr

result = count_pr(
    votes=vote_array,
    n_parties=5,
    n_seats=100,
    allocation_method="dhondt",
    threshold=0.05,
)

Parameters: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | votes | np.ndarray | required | (n_voters,) vote choices (party indices) | | n_parties | int | required | Number of parties | | n_seats | int | required | Total seats to allocate | | allocation_method | Literal["dhondt", "sainte_lague", "hare", "droop"] | "dhondt" | Allocation algorithm | | threshold | float | 0.0 | Minimum vote share for representation (0-1) |

Returns: Dict with keys: | Key | Type | Description | |-----|------|-------------| | system | str | Always "PR" | | method | str | Allocation method used | | seats | np.ndarray | (n_parties,) allocated seats | | vote_counts | np.ndarray | (n_parties,) raw vote counts | | n_seats | int | Total seats |

Under the hood: 1. Vote counting via np.bincount (vectorized) 2. Seat allocation via allocate_seats() from electoral_sim.systems.allocation

Example:

import numpy as np
from electoral_sim.core.counting import count_pr

votes = np.random.randint(0, 5, 10_000)

# D'Hondt with 5% threshold
result_dhondt = count_pr(votes, n_parties=5, n_seats=120, allocation_method="dhondt", threshold=0.05)

# Sainte-Laguë (more proportional for small parties)
result_sl = count_pr(votes, n_parties=5, n_seats=120, allocation_method="sainte_lague")

print("D'Hondt seats:", result_dhondt["seats"])
print("Sainte-Laguë seats:", result_sl["seats"])

Note: With a threshold > 0, the allocation function internally zeroes-out parties below the threshold before running the allocation algorithm.