Skip to content

Commit

Permalink
Merge pull request #41 from lzear/nanson
Browse files Browse the repository at this point in the history
Add Nanson method and Baldwin method
  • Loading branch information
lzear authored Jan 12, 2021
2 parents 2c1f2ba + cd86fba commit 87a048c
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 120 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ for more information.
**⚠️Maximal lotteries & Randomized Condorcet⚠️** (Errors included): Returns
probabilities for each candidate that should be used for a lottery between the
Candidates. If a candidate is the Condorcet winner, its probability will be 1.
Despite being non-deterministic, those methods are the most fair.
Despite being non-deterministic, those methods are the fairest.

**Ranked pairs**: Using the duel results as edges, build an acyclic graph
starting by the strongest score differences. The roots of the graph are the
Expand All @@ -79,9 +79,15 @@ candidates.
**Approval voting**: Each voter can select (“approve”) any number of candidates.
The winner is the most-approved candidate.

**Borda's count**: For each voter, every candidate is given a number of points
**Borda count**: For each voter, every candidate is given a number of points
which equals the number of candidates ranked lower in the voter's preference.

**Nanson method**: Iterative Borda count in which, each round, candidates scoring
the average score or less are eliminated.

**Baldwin method**: Iterative Borda count in which, each round, candidates scoring
the lowest score are eliminated.

**Instant-runoff**: Considering only the top choice of each voter, the candidate
with the fewest votes is eliminated. The election repeats until there is a
winner. This voting system is very similar to single transferable vote method.
Expand Down
25 changes: 13 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"module": "dist/votes.es5.js",
"types": "dist/votes.d.ts",
"files": [
"dist"
"dist",
"src"
],
"author": "lzear",
"repository": {
Expand All @@ -20,7 +21,7 @@
"eslint": "eslint --ext .js --ext .ts .",
"fix:prettier": "prettier --write \"**/*.*\"",
"prebuild": "rimraf dist",
"build": "rollup -c rollup.config.js && typedoc",
"build": "rollup -c rollup.config.js && npx typedoc src/votes.ts",
"build:watch": "tsc --module commonjs && rollup -c rollup.config.js -w",
"test": "jest --coverage",
"test:watch": "jest --coverage --watch",
Expand Down Expand Up @@ -53,26 +54,26 @@
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.0.1",
"@types/jest": "^26.0.19",
"@types/lodash": "^4.14.165",
"@types/node": "^14.14.14",
"@typescript-eslint/eslint-plugin": "^4.11.0",
"@typescript-eslint/parser": "^4.11.0",
"@types/jest": "^26.0.20",
"@types/lodash": "^4.14.167",
"@types/node": "^14.14.20",
"@typescript-eslint/eslint-plugin": "^4.13.0",
"@typescript-eslint/parser": "^4.13.0",
"commitizen": "^4.2.2",
"dotenv": "^8.2.0",
"eslint": "^7.16.0",
"eslint": "^7.17.0",
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-prettier": "^3.3.0",
"husky": "^4.3.6",
"eslint-plugin-prettier": "^3.3.1",
"husky": "^4.3.7",
"jest": "^26.6.3",
"lint-staged": "^10.5.3",
"prettier": "^2.2.1",
"rimraf": "^3.0.2",
"rollup": "^2.35.1",
"rollup": "^2.36.1",
"rollup-plugin-sizes": "^1.0.3",
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.29.0",
"semantic-release": "^17.3.0",
"semantic-release": "^17.3.1",
"shelljs": "^0.8.4",
"travis-deploy-once": "^5.0.11",
"ts-jest": "^26.4.4",
Expand Down
27 changes: 27 additions & 0 deletions src/methods/baldwin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import difference from 'lodash/difference'
import {
SystemUsingRankings,
ScoreObject,
VotingSystem,
Ballot,
} from '../../types'
import { borda } from '../borda'
import { scoresToRanking } from '../../utils'

export const baldwin: SystemUsingRankings = {
type: VotingSystem.Baldwin,
computeFromBallots(ballots: Ballot[], candidates: string[]): ScoreObject {
const score: ScoreObject = {}
let remainingCandidates = candidates
let points = 0
while (remainingCandidates.length > 0) {
const bordaScores = borda.computeFromBallots(ballots, remainingCandidates)
const ranking = scoresToRanking(bordaScores)
const losers = ranking[ranking.length - 1]
for (const loser of losers) score[loser] = points
remainingCandidates = difference(remainingCandidates, losers)
points++
}
return score
},
}
2 changes: 1 addition & 1 deletion src/methods/coombs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const coombs: SystemUsingRankings = {
ranking: [...ballot.ranking].reverse(),
weight: ballot.weight,
}))
let remainingCandidates = [...candidates]
let remainingCandidates = candidates
let points = 0
while (remainingCandidates.length > 0) {
const fptpScore = firstPastThePost.computeFromBallots(
Expand Down
22 changes: 22 additions & 0 deletions src/methods/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { VotingSystem } from '../types'
import { approbation } from './approbation'
import { baldwin } from './baldwin'
import { borda } from './borda'
import { coombs } from './coombs'
import { copeland } from './copeland'
Expand All @@ -8,13 +9,15 @@ import { instantRunoff } from './instant-runoff'
import { kemeny } from './kemeny'
import { maximalLotteries } from './maximal-lotteries'
import { minimax } from './minimax'
import { nanson } from './nanson'
import { rankedPairs } from './ranked-pairs'
import { randomizedCondorcet } from './randomized-condorcet'
import { schulze } from './schulze'
import { twoRoundRunoff } from './two-round-runoff'

export const methods = {
[VotingSystem.Approbation]: approbation,
[VotingSystem.Baldwin]: baldwin,
[VotingSystem.Borda]: borda,
[VotingSystem.Coombs]: coombs,
[VotingSystem.Copeland]: copeland,
Expand All @@ -23,8 +26,27 @@ export const methods = {
[VotingSystem.Kemeny]: kemeny,
[VotingSystem.MaximalLotteries]: maximalLotteries,
[VotingSystem.Minimax]: minimax,
[VotingSystem.NANSON]: nanson,
[VotingSystem.RankedPairs]: rankedPairs,
[VotingSystem.RandomizedCondorcet]: randomizedCondorcet,
[VotingSystem.Schulze]: schulze,
[VotingSystem.TwoRoundRunoff]: twoRoundRunoff,
}

export {
approbation,
baldwin,
borda,
coombs,
copeland,
firstPastThePost,
instantRunoff,
kemeny,
maximalLotteries,
minimax,
nanson,
rankedPairs,
randomizedCondorcet,
schulze,
twoRoundRunoff,
}
33 changes: 33 additions & 0 deletions src/methods/nanson/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import sum from 'lodash/sum'
import difference from 'lodash/difference'
import {
SystemUsingRankings,
ScoreObject,
VotingSystem,
Ballot,
} from '../../types'
import { borda } from '../borda'

export const nanson: SystemUsingRankings = {
type: VotingSystem.NANSON,
computeFromBallots(ballots: Ballot[], candidates: string[]): ScoreObject {
const score: ScoreObject = {}
let remainingCandidates = candidates
let points = 0
while (remainingCandidates.length > 0) {
const bordaScores = borda.computeFromBallots(ballots, remainingCandidates)
const scores = Object.values(bordaScores)
const avg = sum(scores) / scores.length
const losers = remainingCandidates.filter((c) => bordaScores[c] <= avg)
let maxPoints = points + 1
for (const loser of losers) {
const p = points + bordaScores[loser] + 1
score[loser] = p
if (p > maxPoints) maxPoints = p
}
remainingCandidates = difference(remainingCandidates, losers)
points = maxPoints
}
return score
},
}
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type ScoreObject = { [candidate: string]: number }

export enum VotingSystem {
Approbation = 'APPROBATION',
Baldwin = 'BALDWIN',
Borda = 'BORDA',
Coombs = 'COOMBS',
Copeland = 'COPELAND',
Expand All @@ -16,6 +17,7 @@ export enum VotingSystem {
InstantRunoff = 'INSTANT_RUNOFF',
MaximalLotteries = 'MAXIMAL_LOTTERIES',
Minimax = 'MINIMAX',
NANSON = 'NANSON',
RandomizedCondorcet = 'RANDOMIZED_CONDORCET',
RankedPairs = 'RANKED_PAIRS',
Schulze = 'SCHULZE',
Expand Down
68 changes: 57 additions & 11 deletions src/votes.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { methods, SystemUsingMatrix, SystemUsingRankings, utils } from './votes'
import { VotingSystem } from './types'
import { matrixFromBallots } from './utils'
import { approbation } from './methods/approbation'
import { borda } from './methods/borda'
import { copeland } from './methods/copeland'
import { firstPastThePost } from './methods/first-past-the-post'
import { instantRunoff } from './methods/instant-runoff'
import { kemeny } from './methods/kemeny'
import { maximalLotteries } from './methods/maximal-lotteries'
import { minimax } from './methods/minimax'
import { rankedPairs } from './methods/ranked-pairs'
import { schulze } from './methods/schulze'
import { twoRoundRunoff } from './methods/two-round-runoff'
import {
approbation,
baldwin,
borda,
coombs,
copeland,
firstPastThePost,
instantRunoff,
kemeny,
maximalLotteries,
minimax,
nanson,
randomizedCondorcet,
rankedPairs,
schulze,
twoRoundRunoff,
} from './votes'
import { abcde, balinski, dummyProfile, sW } from './test/testUtils'

describe('Test all methods', () => {
Expand Down Expand Up @@ -39,10 +45,12 @@ describe('Test all methods', () => {
}, {})
expect(allResults).toStrictEqual({
APPROBATION: ['a'],
BALDWIN: ['a'],
BORDA: ['a'],
COOMBS: ['a'],
FIRST_PAST_THE_POST: ['a'],
INSTANT_RUNOFF: ['a'],
NANSON: ['a'],
TWO_ROUND_RUNOFF: ['a'],
})
Object.values(allResults).forEach((v) => expect(v).toStrictEqual(['a']))
Expand Down Expand Up @@ -82,6 +90,15 @@ describe('Test all methods', () => {
e: 22,
})
})
it('votes with baldwin', () => {
expect(baldwin.computeFromBallots(balinski, abcde)).toStrictEqual({
a: 0,
b: 3,
c: 4,
d: 2,
e: 1,
})
})
it('votes with borda', () => {
expect(borda.computeFromBallots(balinski, abcde)).toStrictEqual({
a: 135,
Expand All @@ -91,6 +108,15 @@ describe('Test all methods', () => {
e: 182,
})
})
it('votes with coombs', () => {
expect(coombs.computeFromBallots(balinski, abcde)).toStrictEqual({
a: 0,
b: 3,
c: 4,
d: 2,
e: 1,
})
})
it('votes with FPTP', () => {
expect(firstPastThePost.computeFromBallots(balinski, abcde)).toStrictEqual({
a: 33,
Expand All @@ -109,6 +135,15 @@ describe('Test all methods', () => {
e: 30,
})
})
it('votes with instant nanson', () => {
expect(nanson.computeFromBallots(balinski, abcde)).toStrictEqual({
a: 136,
b: 243,
c: 244,
d: 193,
e: 183,
})
})
it('votes with two-round runoff', () => {
expect(twoRoundRunoff.computeFromBallots(balinski, abcde)).toStrictEqual({
a: 36,
Expand Down Expand Up @@ -140,6 +175,17 @@ describe('Test all methods', () => {
e: 1,
})
})
it('votes with randomizedCondorcet', () => {
expect(
randomizedCondorcet.computeFromMatrix(matrixFromBallots(sW, abcde)),
).toStrictEqual({
a: 0.3333333333333333,
b: 0,
c: 0.3333333333333333,
d: 0,
e: 0.3333333333333333,
})
})
it('votes with schulze', () => {
expect(
schulze.computeFromMatrix(matrixFromBallots(sW, abcde)),
Expand Down
42 changes: 39 additions & 3 deletions src/votes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
import { methods } from './methods'
import {
approbation,
baldwin,
borda,
coombs,
copeland,
firstPastThePost,
instantRunoff,
kemeny,
maximalLotteries,
minimax,
nanson,
rankedPairs,
randomizedCondorcet,
schulze,
twoRoundRunoff,
methods,
} from './methods'
import {
System,
VotingSystem,
Expand All @@ -11,13 +28,32 @@ import {
import * as utils from './utils'

export {
methods,
// enum
VotingSystem,
// All methods:
methods,
approbation,
baldwin,
borda,
coombs,
copeland,
firstPastThePost,
instantRunoff,
kemeny,
maximalLotteries,
minimax,
nanson,
rankedPairs,
randomizedCondorcet,
schulze,
twoRoundRunoff,
// utils
utils,
// types
System,
SystemUsingRankings,
SystemUsingMatrix,
Matrix,
ScoreObject,
Ballot,
utils,
}
Loading

0 comments on commit 87a048c

Please sign in to comment.