-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a9f3ff2
commit 4352dd8
Showing
8 changed files
with
311 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/target | ||
.DS_Store | ||
.vscode/ |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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" |
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
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. | ||
|
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
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; | ||
} | ||
|
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pub mod ballot; | ||
pub mod tally; |
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
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()); | ||
} | ||
} |
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
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()); | ||
} | ||
} | ||
|