Skip to content

Instantly share code, notes, and snippets.

@ronyhe
Last active March 23, 2021 20:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ronyhe/56c5dcd5b9ff05ef5a2dce8847473286 to your computer and use it in GitHub Desktop.
Save ronyhe/56c5dcd5b9ff05ef5a2dce8847473286 to your computer and use it in GitHub Desktop.
The Bader-Ofer election system in Israel
import _ from 'lodash'
type ExcessAgreements = [string, string][]
interface Inputs {
readonly threshold: number
readonly results: Record<string, number>
readonly excessAgreements: ExcessAgreements
readonly seats: number
}
interface VotesAndSeats {
readonly votes: number
seats: number
}
type Group = Record<string, VotesAndSeats>
function baderOfer({
excessAgreements,
threshold,
results,
seats,
}: Inputs): Record<string, number> {
const sumVotes = sumValues(results)
const minimalVotesNeeded = sumVotes * threshold
const relevantResults = _.pickBy(results, v => v >= minimalVotesNeeded)
return afterThresholdExclusion({
excessAgreements: _.filter(excessAgreements, names =>
_.every(names, name => _.has(relevantResults, name))
),
results: relevantResults,
seats,
})
}
function afterThresholdExclusion({
excessAgreements,
results,
seats,
}: Omit<Inputs, 'threshold'>): Record<string, number> {
const sumVotes = sumValues(results)
const pricePerSeat = sumVotes / seats
const allocation: Record<string, VotesAndSeats> = _.mapValues(
results,
v => ({
votes: v,
seats: Math.floor(v / pricePerSeat),
})
)
allocateExtraSeats(allocation, excessAgreements, seats)
return _.mapValues(allocation, 'seats')
}
function allocateExtraSeats(
initialAllocation: Record<string, VotesAndSeats>,
excessAgreements: ExcessAgreements,
seats: number
): void {
const seatsAllocated = _(initialAllocation).values().sumBy('seats')
const seatsRemaining = seats - seatsAllocated
const groups = createGroups(initialAllocation, excessAgreements)
_.times(seatsRemaining).forEach(() => {
const winner = whichGroupGetsAnExtraSeat(groups)
initialAllocation[winner].seats++
})
}
function createGroups(
results: Record<string, VotesAndSeats>,
excessAgreements: ExcessAgreements
): Group[] {
const groups = _.mapValues(results, (v, k) => ({ [k]: v }))
excessAgreements.forEach(([a, b]) => {
groups[a][b] = groups[b][b]
delete groups[b]
})
return _.values(groups)
}
function whichGroupGetsAnExtraSeat(groups: Group[]): string {
const highestPayingGroup = _.maxBy(groups, priceGroupWillPayForExtraSeat)
return whichPartyGetsAnExtraSeat(highestPayingGroup!)
}
function whichPartyGetsAnExtraSeat(group: Group): string {
const winningEntry = _(group)
.entries()
.maxBy(([_, vs]) => pricePartyWillPayForExtraSeat(vs))
return winningEntry![0]
}
function priceGroupWillPayForExtraSeat(group: Group): number {
const allSeats = _(group).values().sumBy('seats')
const allVotes = _(group).values().sumBy('votes')
return allVotes / (allSeats + 1)
}
function pricePartyWillPayForExtraSeat(vs: VotesAndSeats): number {
return vs.votes / (vs.seats + 1)
}
function sumValues(obj: Record<string, number>): number {
return _.sum(_.values(obj))
}
function print(seats: Record<string, number>) {
console.table(seats)
}
function mainWikiExample() {
// https://he.wikipedia.org/wiki/%D7%97%D7%95%D7%A7_%D7%91%D7%93%D7%A8-%D7%A2%D7%95%D7%A4%D7%A8
const votes = {
a: 501_000,
b: 403_000,
c: 204_000,
d: 92_000,
}
print(
baderOfer({
results: votes,
excessAgreements: [],
threshold: 0,
seats: 120,
})
)
}
function wikiExampleWithExcessAgreement() {
// https://he.wikipedia.org/wiki/%D7%97%D7%95%D7%A7_%D7%91%D7%93%D7%A8-%D7%A2%D7%95%D7%A4%D7%A8
const votes = {
a: 501_000,
b: 403_000,
c: 204_000,
d: 92_000,
}
print(
baderOfer({
results: votes,
excessAgreements: [['c', 'd']],
threshold: 0,
seats: 120,
})
)
}
function knesset20Example() {
// From the python implementation here: https://sourceforge.net/projects/baderofermethod/
const results = {
Likud: 985408,
'Zionist Union': 786313,
'Joint List': 446583,
'Yesh Atid': 371602,
Kulanu: 315360,
'The Jewish Home': 283910,
Shas: 241613,
'Yisrael Beiteinu': 214906,
'United Torah Judaism': 210143,
Meretz: 165529,
Yachad: 125158,
'Ale Yarok': 47180,
'Arab List': 4301,
'The Greens': 2992,
'We are all friends Na Nach': 2493,
"U'Bizchutan": 1802,
'Hope for Change': 1385,
'Pirate Party of Israel': 895,
'Flower Party': 823,
'Brit Olam': 761,
Or: 502,
'Living with Dignity': 423,
'Economy Party': 337,
Democratura: 242,
'Social Leadership': 223,
'Defending Our Children - No More Feeding Them Pornography': 0,
}
const excessAgreements: ExcessAgreements = [
['Likud', 'The Jewish Home'],
['Zionist Union', 'Meretz'],
['Yisrael Beiteinu', 'Kulanu'],
['Shas', 'United Torah Judaism'],
]
print(
baderOfer({
results,
excessAgreements,
threshold: 0.0325,
seats: 120,
})
)
}
function knesset23Example() {
// Results from https://votes23.bechirot.gov.il/
const results = {
likud: 1_352_449,
kahol_lavan: 1_220_381,
meshutefet: 581_507,
shas: 352_853,
yehadut_ha_tora: 274_437,
avoda_gesher_merez: 267_480,
israel_beytenu: 263_365,
yemina: 249_689,
ozma_yehudit: 19_402,
ozam_liberalit: 3_781,
}
const excessAgreements: ExcessAgreements = [
['likud', 'yemina'],
['kahol_lavan', 'avoda_gesher_merez'],
['yehadut_ha_tora', 'shas'],
]
print(
baderOfer({
results,
excessAgreements,
threshold: 0.0325,
seats: 120,
})
)
}
function main() {
console.log('wiki main example')
mainWikiExample()
console.log('\nwiki example with excess agreement')
wikiExampleWithExcessAgreement()
console.log('\nKnesset 20 (March 2015)')
knesset20Example()
console.log('\nKnesset 23 (March 2020)')
knesset23Example()
}
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment