Last active
March 23, 2021 20:39
-
-
Save ronyhe/56c5dcd5b9ff05ef5a2dce8847473286 to your computer and use it in GitHub Desktop.
The Bader-Ofer election system in Israel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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