UNPKG

ecclesia

Version:

Framework for political and electoral simulations

1 lines 13.7 kB
{"version":3,"sources":["../../src/election/voting.ts","../../src/utils.ts","../../src/election/ballots.ts"],"sourcesContent":["import { min } from \"@gouvernathor/python\";\nimport { Counter } from \"@gouvernathor/python/collections\";\nimport { Collection } from \"@gouvernathor/python/collections/abc\";\nimport { createRandomObj, type RandomObjParam } from \"../utils\";\nimport { Ballots, Order, Scores, Simple } from \"./ballots\";\n\nexport interface DisagreementFunction<T, U> {\n (t: T, u: U): number;\n}\n\nexport interface Voting<Voter, Party, B extends Ballots<Party>> {\n (voters: Collection<Voter>, candidates: Collection<Party>): B;\n}\n\n\n// TODO harmonize all random management seeding moments, across attribution/election/voting\n/**\n * Turns a voting method into one in which the candidates are shuffled randomly\n * before being given to the actual voting method passed to this function.\n *\n * If a RNG instance is passed (through options), it will be used directly,\n * without reseeding, at each call of the voting method.\n *\n * If a seed is passed, the random object is reseeded\n * with the same seed at each call of the voting method,\n * making the resulting function return the same values for the same parameters\n * (assuming the passed base voting method does).\n * If you want the generator to be seeded only once with a given seed, and reused afterwards,\n * seed it yourself and pass it as a random object to this function.\n */\nexport function toShuffledVote<Voter, Party, B extends Ballots<Party>>(\n { voting, ...rest }: {\n voting: Voting<Voter, Party, B>,\n } & RandomObjParam,\n): Voting<Voter, Party, B> {\n return (voters: Collection<Voter>, candidates: Collection<Party>) => {\n const randomObj = createRandomObj(rest);\n const partees = randomObj.shuffled(candidates);\n return voting(voters, partees);\n };\n}\n\n\n// concrete implementations (no random shuffling included !)\n\n/**\n * The most basic and widespread voting system : each voter casts one ballot for\n * one of the available candidates, or (not implemented here) for none of them.\n */\nexport function singleVote<Voter, Party>(\n { disagree }: {\n disagree: DisagreementFunction<Voter, Party>,\n }\n): Voting<Voter, Party, Simple<Party>> {\n return (voters: Collection<Voter>,candidates: Collection<Party>): Simple<Party> => {\n const scores = Counter.fromkeys(candidates, 0);\n for (const voter of voters) {\n // find the party with which disagreement is minimal\n // add it a ballot\n scores.increment(min(candidates, party => disagree(voter, party)));\n }\n return scores;\n }\n}\n\n/**\n * Each voter ranks all, or (not implemented here) some, of the candidates.\n */\nexport function orderingVote<Voter, Party>(\n { disagree }: {\n disagree: DisagreementFunction<Voter, Party>,\n }\n): Voting<Voter, Party, Order<Party>> {\n return (voters: Collection<Voter>,candidates: Collection<Party>): Order<Party> => {\n const order: Party[][] = [];\n for (const voter of voters) {\n order.push([...candidates]\n .sort((a, b) => disagree(voter, a) - disagree(voter, b)));\n }\n return order;\n }\n}\n\n/**\n * Each voter gives a note (or grade) to each candidate.\n * The number of grades must be provided to the constructor.\n *\n * This one is not as straightforward as the two previous ones, even setting\n * strategic voting aside.\n * What is to be considered to be the range of grades to cover ?\n * From nazis to angels, or from the worst present candidate to the best ?\n * The answer lies only in the minds of the voters.\n * The latter is more akin to OrderingVote, so I made the former the default,\n * but it causes issues for lower grades so ApprovalVote uses the latter.\n *\n * In this implementation, each voter gives a grade to each party\n * proportional to the raw disagreement. This may yield situations\n * where every party is graded 0, especially with low ngrades values.\n */\nexport function cardinalVote<Voter, Party>(\n { nGrades, disagree }: {\n nGrades: number,\n disagree: DisagreementFunction<Voter, Party>,\n }\n): Voting<Voter, Party, Scores<Party>> {\n return (voters: Collection<Voter>, candidates: Collection<Party>) => {\n const scores = Scores.fromGrades<Party>(nGrades);\n\n // if the disagreement is .0, the grade will be ngrades-1 and not ngrades\n for (const voter of voters) {\n for (const party of candidates) {\n const grade = Math.min(nGrades - 1,\n Math.floor(nGrades) * (1 - disagree(voter, party)));\n (scores.get(party) as number[])[grade]++;\n }\n }\n return scores;\n };\n}\n\n/**\n * Alternative implementation of CardinalVote.\n */\nexport function balancedCardinalVote<Voter, Party>(\n { nGrades, disagree }: {\n nGrades: number,\n disagree: DisagreementFunction<Voter, Party>,\n }\n): Voting<Voter, Party, Scores<Party>> {\n return (voters: Collection<Voter>, candidates: Collection<Party>) => {\n const scores = Scores.fromGrades<Party>(nGrades);\n\n // if the disagreement is .0, the grade will be ngrades-1 and not ngrades\n for (const voter of voters) {\n const prefs = new Map(Array.from(candidates, party => [party, 1 - disagree(voter, party)]));\n const minPref = Math.min(...prefs.values());\n let maxPref = Math.max(...prefs.values());\n\n if (minPref !== maxPref) { // avoid division by zero\n maxPref -= minPref;\n }\n\n for (const party of candidates) {\n const grade = Math.min(nGrades - 1,\n Math.floor(nGrades * (prefs.get(party)! - minPref) / maxPref));\n (scores.get(party) as number[])[grade]++;\n }\n }\n return scores;\n };\n}\n\n/**\n * Each voter approves or disapproves each of the candidates\n *\n * Technically a special case of grading vote where grades are 0 and 1,\n * but it makes it open to additional attribution methods\n * (proportional ones for example).\n * That's why the format it returns is not the same as with the cardinal vote.\n * If you want a scores-like attribution, use balancedCardinalVote({ nGrades: 2 }) instead.\n */\nexport function approvalVote<Voter, Party>(\n { disagree }: {\n disagree: DisagreementFunction<Voter, Party>,\n }\n): Voting<Voter, Party, Simple<Party>> {\n const cardinal = balancedCardinalVote({ nGrades: 2, disagree });\n return (voters: Collection<Voter>,candidates: Collection<Party>): Simple<Party> => {\n const scores = cardinal(voters, candidates) as Scores<Party>;\n const approvals = new Counter<Party>();\n for (const [party, [_disapproval, approval]] of scores) {\n approvals.increment(party, approval);\n }\n return approvals;\n }\n}\n","import RNG from \"@gouvernathor/rng\";\n\nexport type RandomObjParam = {randomObj: RNG} | {randomSeed?: number|string};\n\nexport function createRandomObj(param?: RandomObjParam): RNG;\nexport function createRandomObj({ randomObj, randomSeed }:\n { randomObj?: RNG, randomSeed?: number | string } = {},\n): RNG {\n if (randomObj === undefined) {\n randomObj = new RNG(randomSeed);\n }\n return randomObj;\n}\n\n\n// https://stackoverflow.com/a/71700658\n\n/**\n * Mutable tuple of a single element type and a given length.\n *\n * Warning: due to limitations in TypeScript, when N is unknown/generic,\n * the typing system does not recognize that the type extends array.\n * Using methods such as map will require a (double) cast first to any then to T[].\n */\nexport type Tuple<\n T,\n N extends number,\n R extends T[] = [],\n> = R['length'] extends N ? R : Tuple<T, N, [T, ...R]>;\n\n/**\n * Immutable version of Tuple.\n *\n * Warning: due to limitations in TypeScript, when N is unknown/generic,\n * the typing system does not recognize that the type extends readonly array.\n * Using methods such as map will require a (double) cast first to any then to readonly T[].\n */\nexport type ReadonlyTuple<T, N extends number> = Readonly<Tuple<T, N>>;\n","import { ReadonlyCounter } from \"@gouvernathor/python/collections\";\n\n/**\n * A counter, mapping each party to its number of ballots.\n *\n * [[PS, 5], [LR: 7]] -> 5 ballots for PS, 7 for LR.\n */\nexport interface Simple<Party> extends ReadonlyCounter<Party> { }\n\n/**\n * A list of ballots, each ballot ordering parties by decreasing preference.\n *\n * [[LR, PS, LFI], [LFI, PS,], ] -> one voter prefers LR then PS then LFI,\n * another prefers LFI then PS and didn't rank LR.\n *\n * Math.max(result.map(ballot => ballot.length)) <= number of candidates-1\n * == if the voting method requires a full ranking\n *\n * There can be no tie between candidates within a ballot.\n * Note that not ranking all the candidates is permitted by this type,\n * although some attribution methods may not support it.\n */\nexport interface Order<Party> extends ReadonlyArray<ReadonlyArray<Party>> { }\n\n/**\n * A mapping from each party to a list of number of ballots, one for each grade.\n *\n * [[PS, [0, 2, 5, 9, 1]], ] -> PS received the worst grade 0 times, the best grade 1 time and so on.\n *\n * result.get(p).length is constant, equal to the number of grades of the voting method.\n *\n * If the voter must grade all the candidates, then sum(result.get(p)) is constant\n * and equal to the number of voters.\n *\n * Any party not mapped will be assumed to have\n * an array of zeros (of length ngrades).\n */\nexport interface Scores<Party> extends ReadonlyMap<Party, ReadonlyArray<number>> {\n readonly ngrades: number;\n get(key: Party): ReadonlyArray<number>;\n}\n\nexport namespace Scores {\n function get<Party>(this: Scores<Party>, key: Party): ReadonlyArray<number> {\n const value = this.get(key);\n if (value === undefined) {\n return Array(this.ngrades).fill(0);\n }\n return value!;\n }\n\n export function fromEntries<Party>(\n elements: readonly [Party, readonly number[]][],\n ): Scores<Party> {\n if (elements.length === 0) {\n throw new Error(\"Use the fromGrades method to create an empty Scores instance\");\n }\n const ths = new Map(elements) as Partial<Scores<Party>> & { ngrades?: number };\n ths.ngrades = elements[0][1].length;\n ths.get = get.bind(ths as any);\n return ths as Scores<Party>;\n }\n\n export function fromGrades<Party>(ngrades: number): Scores<Party> {\n const ths = new Map() as Partial<Scores<Party>> & { ngrades?: number };\n ths.ngrades = ngrades;\n ths.get = get.bind(ths as any);\n return ths as Scores<Party>;\n }\n}\n\n\nexport type Ballots<Party> = Simple<Party> | Order<Party> | Scores<Party>;\n"],"mappings":"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","names":["RNG","Scores"]}