Skip to content

Commit

Permalink
Use wikipedia example
Browse files Browse the repository at this point in the history
  • Loading branch information
corydickson committed Nov 29, 2022
1 parent a9f3ff2 commit 4352dd8
Show file tree
Hide file tree
Showing 8 changed files with 311 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/target
.DS_Store
.vscode/
75 changes: 75 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "pairwise-voting"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
rand = "0.8.4"
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Ranked Pairs Voting

## Definition

This is a Rust implementation of T.N. Tideman's Ranked Pairs algorithm, with test cases provided by the example given in the [Wikipedia article](https://en.wikipedia.org/wiki/Ranked_pairs). The goal of the ranked pair election/ballot is to select a winner amongst a list of preferences. It generates a sorted list of pairs and ensures that at least one candidate wins
amongst all the possible choices. The procedure first tallies and compares each pair of candidates (choices) and determines a winner. Then these majorities are sorted, in descending
order of magnitude from greatest to least. A "lock-in" graph is created, where the largest magnitude majority is considered first to create a DAG in which the source (i.e. the node with no incoming edges) is the winner. In the possiblity of a tie, where there is one or more source nodes, a winner is selected at random amongst them.

## Citations

- Tideman, T.N. Independence of clones as a criterion for voting rules. Soc Choice Welfare 4, 185–206 (1987). https://doi.org/10.1007/BF00433944
- Zavist, T.M., Tideman, T.N. Complete independence of clones in the ranked pairs rule. Soc Choice Welfare 6, 167–173 (1989). https://doi.org/10.1007/BF00303170

## Quickstart

`cargo build`
`cargo run` to see full working example with announced winner.
`cargo test` to run the example for each discrete procedure step.

52 changes: 52 additions & 0 deletions src/ballot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
pub type Candidate = String;
pub type Ballot = [Candidate; 4];

pub fn create_wikipedia_ballots() -> Vec<[String; 4]> {
// Example election provided by: https://en.wikipedia.org/wiki/Ranked_pairs
let mut ballots: Vec<Ballot> = Vec::new();

for _i in 0..7 {
let ballot: [String; 4] = [String::from("w"),String::from("x"), String::from("z"), String::from("y")];

ballots.push(ballot);
}
assert_eq!(ballots.len(), 7);

for _i in 0..2 {
let ballot = [String::from("w"),String::from("y"), String::from("x"), String::from("z")];

ballots.push(ballot);
}
assert_eq!(ballots.len(), 9);

for _i in 0..4 {
let ballot = [String::from("x"),String::from("y"), String::from("z"), String::from("w")];

ballots.push(ballot);
}
assert_eq!(ballots.len(), 13);

for _i in 0..5 {
let ballot = [String::from("x"),String::from("z"), String::from("w"), String::from("y")];

ballots.push(ballot);
}
assert_eq!(ballots.len(), 18);

for _i in 0..1 {
let ballot = [String::from("y"),String::from("w"), String::from("x"), String::from("z")];

ballots.push(ballot);
}
assert_eq!(ballots.len(), 19);

for _i in 0..8 {
let ballot = [String::from("y"),String::from("z"), String::from("w"), String::from("x")];

ballots.push(ballot);
}
assert_eq!(ballots.len(), 27);

return ballots;
}

2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod ballot;
pub mod tally;
21 changes: 21 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use pairwise_voting::ballot::create_wikipedia_ballots;
use pairwise_voting::tally::*;
use rand::thread_rng;
use rand::seq::SliceRandom;

fn main() {
let candidates = vec![String::from("w"),String::from("x"), String::from("z"), String::from("y")];
let ballots = create_wikipedia_ballots();
// Execute ranked pairs procedure
let results = tally(ballots);
let majorities = sort_majorities(&results);
let sources = lock(&candidates, majorities);

if sources.len() == 1 {
let winner = sources.get(0).unwrap();
println!("The winner is {}", winner);
} else {
println!("There is a tie between candidates: {:?}", sources);
println!("Randomly selecting winner: {:?}", sources.choose(&mut thread_rng()).unwrap());
}
}
130 changes: 130 additions & 0 deletions src/tally.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use crate::ballot::*;
use std::collections::HashMap;

type Pair = (Candidate, Candidate);
type Score = i32;

pub fn tally(ballots: Vec<Ballot>) -> HashMap <Pair, Score> {
let mut scores = HashMap::new();

for b in ballots {
for (rank, candidate) in b.iter().enumerate() {
for i in 0..b.len() {
if candidate == &b[i] {
scores.insert((candidate.to_owned(), b[i].to_owned()), 0);
}
else {
let count = scores.entry((candidate.to_owned(), b[i].to_owned())).or_insert(0);
if rank < i {
*count += 1;
}
else {
*count -= 1;
}
}
}
}
}

return scores;
}

pub fn sort_majorities(scores: &HashMap<Pair, Score>) -> Vec<(&Pair, &Score)> {
let mut majorities: Vec<(&Pair, &Score)> = scores.iter().collect();
majorities.retain(|m| m.1 > &0);
majorities.sort_by(|a, b| b.1.cmp(a.1));

return majorities;
}

fn reachable(graph: &HashMap<&String, Vec<&String>>, from: &String, to: &String) -> bool {
let edges = graph.get(from).unwrap();
for &e in edges {
return &e == &to || reachable(graph, &e, to);
}

return false;
}

pub fn lock<'a>(candidates: &'a Vec<Candidate>, majorities: Vec<(&Pair, &Score)>) -> Vec<&'a Candidate> {
let mut graph = HashMap::new();

for c in candidates.iter() {
let edges: Vec<&String> = Vec::new();
graph.insert(c, edges);
}

for m in majorities.iter() {
let candidate_x = &m.0.0;
let candidate_y = &m.0.1;

if !reachable(&graph, candidate_y, candidate_x) {
let edges = graph.entry(&candidate_x).or_insert(vec![]);
edges.push(candidate_y);
}
}

let sources = candidates.into_iter().filter( |c| {
return candidates.into_iter().all(|n| {
let adj = graph.get(&n).unwrap();
return !adj.contains(&c);
});
}).collect::<Vec<_>>();

return sources;
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn tally_count() {
let ballots = create_wikipedia_ballots();
let scores = tally(ballots);

assert_eq!(scores.get(&(String::from("w"), String::from("w"))).unwrap(), &0);
assert_eq!(scores.get(&(String::from("w"), String::from("x"))).unwrap(), &9);
assert_eq!(scores.get(&(String::from("w"), String::from("y"))).unwrap(), &1);
assert_eq!(scores.get(&(String::from("w"), String::from("z"))).unwrap(), &-7);

assert_eq!(scores.get(&(String::from("x"), String::from("w"))).unwrap(), &-9);
assert_eq!(scores.get(&(String::from("x"), String::from("x"))).unwrap(), &0);
assert_eq!(scores.get(&(String::from("x"), String::from("y"))).unwrap(), &5);
assert_eq!(scores.get(&(String::from("x"), String::from("z"))).unwrap(), &11);

assert_eq!(scores.get(&(String::from("y"), String::from("w"))).unwrap(), &-1);
assert_eq!(scores.get(&(String::from("y"), String::from("x"))).unwrap(), &-5);
assert_eq!(scores.get(&(String::from("y"), String::from("y"))).unwrap(), &0);
assert_eq!(scores.get(&(String::from("y"), String::from("z"))).unwrap(), &3);

assert_eq!(scores.get(&(String::from("z"), String::from("w"))).unwrap(), &7);
assert_eq!(scores.get(&(String::from("z"), String::from("x"))).unwrap(), &-11);
assert_eq!(scores.get(&(String::from("z"), String::from("y"))).unwrap(), &-3);
assert_eq!(scores.get(&(String::from("z"), String::from("z"))).unwrap(), &0);
}

#[test]
fn majorities_sorted_correctly() {
let ballots = create_wikipedia_ballots();
let scores = tally(ballots);
let majorities = sort_majorities(&scores);
let correct_ordered_scores = vec![11,9,7,5,3,1];

for (i, m) in majorities.into_iter().enumerate() {
let score = m.1;
assert_eq!(score, correct_ordered_scores.get(i).unwrap())
}
}

#[test]
fn locked_graph_no_cycles() {
let candidates = vec![String::from("w"),String::from("x"), String::from("z"), String::from("y")];
let ballots = create_wikipedia_ballots();
let results = tally(ballots);
let majorities = sort_majorities(&results);
let sources = lock(&candidates, majorities);
assert_eq!(sources.get(0).unwrap(), &candidates.get(0).unwrap());
}
}

0 comments on commit 4352dd8

Please sign in to comment.