Skip to content

Commit

Permalink
Merge pull request #26 from lzear/maximal-lotteries
Browse files Browse the repository at this point in the history
feat: add a new voting system: maximal lotteries
  • Loading branch information
lzear authored Dec 16, 2020
2 parents a699adc + 67458d5 commit a1839de
Show file tree
Hide file tree
Showing 21 changed files with 622 additions and 123 deletions.
1 change: 1 addition & 0 deletions .codacy.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
exclude_paths:
- '**/*.test.ts'
- 'src/test/'
- 'tools/gh-pages-publish.ts'
3 changes: 3 additions & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
exclude_patterns:
- '**/*.test.ts'
- 'src/test/'
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ See
[Comparison of electoral systems (Wikipedia)](https://en.wikipedia.org/wiki/Comparison_of_electoral_systems)
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.

**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
winners.
Expand Down
2 changes: 1 addition & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
coverage:
range: 0..100
range: "0..100"
22 changes: 11 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"eslint": "eslint --ext .js --ext .ts .",
"fix:prettier": "prettier --write \"**/*.*\"",
"prebuild": "rimraf dist",
"build": "tsc --module commonjs && rollup -c rollup.config.js && typedoc",
"build": "rollup -c rollup.config.js && typedoc",
"build:watch": "tsc --module commonjs && rollup -c rollup.config.js -w",
"test": "jest --coverage",
"test:watch": "jest --coverage --watch",
Expand Down Expand Up @@ -55,33 +55,33 @@
"@commitlint/cli": "^11.0.0",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"@types/jest": "^26.0.16",
"@rollup/plugin-node-resolve": "^11.0.1",
"@types/jest": "^26.0.19",
"@types/lodash": "^4.14.165",
"@types/node": "^14.14.10",
"@typescript-eslint/eslint-plugin": "^4.9.0",
"@typescript-eslint/parser": "^4.9.0",
"@types/node": "^14.14.14",
"@typescript-eslint/eslint-plugin": "^4.10.0",
"@typescript-eslint/parser": "^4.10.0",
"commitizen": "^4.2.2",
"dotenv": "^8.2.0",
"eslint": "^7.15.0",
"eslint-config-prettier": "^7.0.0",
"eslint-plugin-prettier": "^3.2.0",
"husky": "^4.3.4",
"eslint-plugin-prettier": "^3.3.0",
"husky": "^4.3.6",
"jest": "^26.6.3",
"lint-staged": "^10.5.3",
"prettier": "^2.2.1",
"rimraf": "^3.0.2",
"rollup": "^2.34.1",
"rollup": "^2.35.1",
"rollup-plugin-sizes": "^1.0.3",
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.29.0",
"semantic-release": "^17.3.0",
"shelljs": "^0.8.4",
"travis-deploy-once": "^5.0.11",
"ts-jest": "^26.4.4",
"ts-node": "^9.1.0",
"ts-node": "^9.1.1",
"typedoc": "^0.19.2",
"typescript": "^4.1.2"
"typescript": "^4.1.3"
},
"dependencies": {
"lodash": "^4.17.20"
Expand Down
9 changes: 1 addition & 8 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,15 @@ export default {
name: 'votes',
format: 'umd',
sourcemap: true,
globals: {
lodash: '_',
},
},
{
file: pkg.module,
name: 'votes',
format: 'es',
sourcemap: true,
globals: {
lodash: '_',
},
},
],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: ['lodash'],
external: Object.keys(pkg.dependencies),
watch: {
include: 'src/**',
},
Expand Down
24 changes: 0 additions & 24 deletions src/descriptions.ts

This file was deleted.

4 changes: 4 additions & 0 deletions src/methods/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { copeland } from './copeland'
import { firstPastThePost } from './first-past-the-post'
import { instantRunoff } from './instant-runoff'
import { kemeny } from './kemeny'
import { maximalLotteries } from './maximal-lotteries'
import { minimax } from './minimax'
import { rankedPairs } from './ranked-pairs'
import { randomizedCondorcet } from './randomized-condorcet'
import { schulze } from './schulze'
import { twoRoundRunoff } from './two-round-runoff'

Expand All @@ -17,8 +19,10 @@ export const methods = {
[VotingSystem.FirstPastThePost]: firstPastThePost,
[VotingSystem.InstantRunoff]: instantRunoff,
[VotingSystem.Kemeny]: kemeny,
[VotingSystem.MaximalLotteries]: maximalLotteries,
[VotingSystem.Minimax]: minimax,
[VotingSystem.RankedPairs]: rankedPairs,
[VotingSystem.RandomizedCondorcet]: randomizedCondorcet,
[VotingSystem.Schulze]: schulze,
[VotingSystem.TwoRoundRunoff]: twoRoundRunoff,
}
38 changes: 38 additions & 0 deletions src/methods/maximal-lotteries/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import zipObject from 'lodash/zipObject'
import { solve } from '../../simplex'
import {
SystemUsingMatrix,
VotingSystem,
Matrix,
ScoreObject,
} from '../../types'
import { makeAntisymetric } from '../../utils'
import { findCondorcet } from '../../utils/condorcet'

export const computeLottery = (
matrix: Matrix,
): { [candidate: string]: number } => {
const condorset = findCondorcet(matrix)

const subVector = solve(condorset.array).map((v) => Math.max(0, v))

// Fixups because I can't implement the simplex algorithm correctly
// ---------- Begin ----------
const sum = subVector.reduce((acc, cur) => acc + cur, 0)
const normalizedVector = subVector.map((v) => v / sum)
// ---------- End ----------

return matrix.candidates
.filter((candidate) => !condorset.candidates.includes(candidate))
.reduce(
(acc, cur) => ({ [cur]: 0, ...acc }),
zipObject(condorset.candidates, normalizedVector),
)
}

export const maximalLotteries: SystemUsingMatrix = {
type: VotingSystem.MaximalLotteries,
computeFromMatrix(matrix: Matrix): ScoreObject {
return computeLottery(makeAntisymetric(matrix))
},
}
89 changes: 89 additions & 0 deletions src/methods/maximal-lotteries/maximal-lotteries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { performPivots, simplexTableau } from '../../simplex'
import { matrixString } from '../../simplex/utils'
import { maximalLotteries } from './index'

const example1 = [
[0, -2, 8],
[2, 0, -4],
[-8, 4, 0],
]

const example2 = [
[0, -4, -4, -4, -4],
[4, 0, -2, 8, 6],
[4, 2, 0, -4, 2],
[4, -8, 4, 0, 6],
[4, -6, -2, -6, 0],
]

const example3 = [
[0, -5, 7, 15, -1],
[5, 0, -13, 21, -9],
[-7, 13, 0, -11, 3],
[-15, -21, 11, 0, -22],
[1, 9, -3, 22, 0],
]

const candidates = ['a', 'b', 'c', 'd', 'e']

describe('maximal lotteries', () => {
it('works with "simple" example', () => {
expect(
maximalLotteries.computeFromMatrix({
array: example1,
candidates: ['a', 'b', 'c'],
}),
).toEqual({
a: 0.2857142857142857,
b: 0.5714285714285714,
c: 0.14285714285714285,
})
})
it('works with "complexer" example', () => {
expect(
maximalLotteries.computeFromMatrix({ array: example2, candidates }),
).toEqual({
a: 0,
b: 0.2857142857142857,
c: 0.5714285714285714,
d: 0.14285714285714285,
e: 0,
})
})
it('works with "example3" example', () => {
expect(
maximalLotteries.computeFromMatrix({
array: example3,
candidates: ['a', 'b', 'c', 'd', 'e'],
}),
).toEqual({
a: 0.6428571428571428,
b: 0,
c: 0,
d: 0,
e: 0.3571428571428572,
})
})
})

describe('pivot', () => {
it('matrixString', () => {
expect(matrixString(example1)).toEqual(`
0 -2 8
2 0 -4
-8 4 0`)
})
it('tableau', () => {
expect(matrixString(simplexTableau(example1))).toEqual(`
0 -1 -1 -1 1 0 0 0 0 1
0 0 -2 8 0 1 0 0 0 0
0 2 0 -4 0 0 1 0 0 0
0 -8 4 0 0 0 0 1 0 0
1 1 1 1 0 0 0 0 1 0`)
})
it('no row', () => {
expect(() => performPivots(simplexTableau(example1), [1, 2, 3, 4])).toThrow(
'no row no pivot',
)
})
})
20 changes: 20 additions & 0 deletions src/methods/randomized-condorcet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {
SystemUsingMatrix,
VotingSystem,
Matrix,
ScoreObject,
} from '../../types'
import { makeAntisymetric } from '../../utils'
import { computeLottery } from '../maximal-lotteries'

export const randomizedCondorcet: SystemUsingMatrix = {
type: VotingSystem.RandomizedCondorcet,
computeFromMatrix(matrix: Matrix): ScoreObject {
const antisymetric = makeAntisymetric(matrix)
const antisymetricUnit = {
...antisymetric,
array: antisymetric.array.map((r) => r.map(Math.sign)),
}
return computeLottery(antisymetricUnit)
},
}
89 changes: 89 additions & 0 deletions src/methods/randomized-condorcet/randomized-condorcet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { performPivots, simplexTableau } from '../../simplex'
import { matrixString } from '../../simplex/utils'
import { randomizedCondorcet } from './index'

const example1 = [
[0, -2, 8],
[2, 0, -4],
[-8, 4, 0],
]

const example2 = [
[0, -4, -4, -4, -4],
[4, 0, -2, 8, 6],
[4, 2, 0, -4, 2],
[4, -8, 4, 0, 6],
[4, -6, -2, -6, 0],
]

const example3 = [
[0, -5, 7, 15, -1],
[5, 0, -13, 21, -9],
[-7, 13, 0, -11, 3],
[-15, -21, 11, 0, -22],
[1, 9, -3, 22, 0],
]

const candidates = ['a', 'b', 'c', 'd', 'e']

describe('randomized condorcet', () => {
it('works with "simple" example', () => {
expect(
randomizedCondorcet.computeFromMatrix({
array: example1,
candidates: ['a', 'b', 'c'],
}),
).toEqual({
a: 0.3333333333333333,
b: 0.3333333333333333,
c: 0.3333333333333333,
})
})
it('works with "complexer" example', () => {
expect(
randomizedCondorcet.computeFromMatrix({ array: example2, candidates }),
).toEqual({
a: 0,
b: 0.3333333333333333,
c: 0.3333333333333333,
d: 0.3333333333333333,
e: 0,
})
})
it('works with "example3" example', () => {
expect(
randomizedCondorcet.computeFromMatrix({
array: example3,
candidates: ['a', 'b', 'c', 'd', 'e'],
}),
).toEqual({
a: 0.3333333333333333,
b: 0,
c: 0.3333333333333333,
d: 0,
e: 0.3333333333333333,
})
})
})

describe('pivot', () => {
it('matrixString', () => {
expect(matrixString(example1)).toEqual(`
0 -2 8
2 0 -4
-8 4 0`)
})
it('tableau', () => {
expect(matrixString(simplexTableau(example1))).toEqual(`
0 -1 -1 -1 1 0 0 0 0 1
0 0 -2 8 0 1 0 0 0 0
0 2 0 -4 0 0 1 0 0 0
0 -8 4 0 0 0 0 1 0 0
1 1 1 1 0 0 0 0 1 0`)
})
it('no row', () => {
expect(() => performPivots(simplexTableau(example1), [1, 2, 3, 4])).toThrow(
'no row no pivot',
)
})
})
Loading

0 comments on commit a1839de

Please sign in to comment.