voting_systems 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/systems/baldwin.rb +21 -0
- data/lib/systems/borda.rb +10 -0
- data/lib/systems/bucklin.rb +37 -0
- data/lib/systems/coombs.rb +30 -0
- data/lib/systems/copeland.rb +26 -0
- data/lib/systems/instant_runoff.rb +28 -0
- data/lib/systems/ranked_pairs.rb +65 -0
- data/lib/util.rb +78 -0
- data/lib/voting_systems.rb +3 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: dbaccad0b37d37b7b7ffcc8d79ea2d9037a1c378e2396c2392ee448fe52fac62
|
4
|
+
data.tar.gz: fec6d0e30f46a6651b02eda841afb80d9c74d2d297ae013babf0eb87a9fecb8c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6220738417c568c3ed55c88f20d958d7c76ca819365b770f48cb55a6a1c64b84484173a411f2c4de7f5c76c304c2fbe6df9b6c7598975d0f5c117df86f4d3268
|
7
|
+
data.tar.gz: 4e0bf1481a8f615ad293837099a369456361bad7e452bdd803debfa86cf11131da52a67a2fa017f5e9b725c421dd533f17b8630cf456438d4dc6047838accaf0
|
@@ -0,0 +1,21 @@
|
|
1
|
+
def baldwin votes
|
2
|
+
votes, options = normalize votes
|
3
|
+
while true
|
4
|
+
# calculate scores using Borda counts
|
5
|
+
scores = borda_counts votes, options
|
6
|
+
# group and sort the options by score
|
7
|
+
groups = options.group_by {|option| scores[option]}
|
8
|
+
sorted = groups.sort_by {|score, tied| -score}
|
9
|
+
# puts
|
10
|
+
# sorted.each {|score, tied| puts "#{score.to_f} #{tied}"}
|
11
|
+
# find the options with the lowest score
|
12
|
+
leasts = sorted.last[1]
|
13
|
+
# if all remaining options are tied, they are the winners
|
14
|
+
return leasts if leasts.size == options.size
|
15
|
+
# remove the options with the lowest score
|
16
|
+
votes.collect! {|count, ranking|
|
17
|
+
[count, ranking.collect {|tied| tied - leasts}]
|
18
|
+
}
|
19
|
+
options -= leasts
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
def borda votes
|
2
|
+
votes, options = normalize votes
|
3
|
+
# calculate scores using Borda counts
|
4
|
+
scores = borda_counts votes, options
|
5
|
+
# sort the results and return the winner/s
|
6
|
+
groups = options.group_by {|option| scores[option]}
|
7
|
+
sorted = groups.sort_by {|score, tied| -score}
|
8
|
+
# sorted.each {|score, tied| puts "#{score.to_f} #{tied}"}
|
9
|
+
sorted.first[1]
|
10
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
def scores_for_places votes, places
|
2
|
+
# Add up votes for each option, including 1st-place votes, 2nd-place votes,
|
3
|
+
# and so on up to k-place votes, where k = places.
|
4
|
+
scores = Hash.new 0
|
5
|
+
votes.each {|count, ranking|
|
6
|
+
remaining = places
|
7
|
+
ranking.each {|tied|
|
8
|
+
if remaining > tied.size
|
9
|
+
# each tied option uses up one "place"
|
10
|
+
tied.each {|option| scores[option] += count}
|
11
|
+
remaining -= tied.size
|
12
|
+
else
|
13
|
+
# not enough remaining "places" to go around, so give each tied option
|
14
|
+
# an equal fraction of what there is
|
15
|
+
tied.each {|option| scores[option] += count * (remaining.quo tied.size)}
|
16
|
+
break
|
17
|
+
end
|
18
|
+
}
|
19
|
+
}
|
20
|
+
scores
|
21
|
+
end
|
22
|
+
|
23
|
+
def bucklin votes
|
24
|
+
votes, options = normalize votes
|
25
|
+
sum_of_counts = votes.transpose[0].inject 0, :+
|
26
|
+
(1..options.size).each {|places|
|
27
|
+
# count the votes up to places
|
28
|
+
scores = scores_for_places votes, places
|
29
|
+
# find the winners
|
30
|
+
groups = options.group_by {|option| scores[option]}
|
31
|
+
# p groups
|
32
|
+
best = groups.max_by {|score, tied| score}
|
33
|
+
# if the best group has a majority, it is the winner
|
34
|
+
return best[1] if best[0] > sum_of_counts / 2r
|
35
|
+
}
|
36
|
+
raise # should never get here, by the pigeonhole principle
|
37
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
def coombs votes
|
2
|
+
# this implements the version of Coombs in which elimination proceeds
|
3
|
+
# regardless of whether a candidate is ranked first by a majority of voters
|
4
|
+
votes, remaining = normalize votes
|
5
|
+
while true
|
6
|
+
votes.each {|count, ranking| ranking.delete []}
|
7
|
+
# p votes
|
8
|
+
|
9
|
+
# see how many times each option was ranked last
|
10
|
+
scores = Hash.new 0
|
11
|
+
votes.each {|count, ranking|
|
12
|
+
tied = ranking.last
|
13
|
+
tied.each {|option| scores[option] += count.quo tied.size}
|
14
|
+
}
|
15
|
+
|
16
|
+
# find the options which were ranked last the most
|
17
|
+
groups = remaining.group_by {|option| scores[option]}
|
18
|
+
# p groups
|
19
|
+
mosts = groups.max_by {|score, tied| score}[1]
|
20
|
+
|
21
|
+
# if all remaining options are tied, they are the winners
|
22
|
+
return mosts if mosts.size == remaining.size
|
23
|
+
|
24
|
+
# remove options which were ranked last the most
|
25
|
+
votes.collect! {|count, ranking|
|
26
|
+
[count, ranking.collect {|tied| tied - mosts}]
|
27
|
+
}
|
28
|
+
remaining -= mosts
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
def copeland votes
|
2
|
+
# implements standard Copeland (pairwise victories minus pairwise defeats)
|
3
|
+
votes, options = normalize votes
|
4
|
+
scores = winning_votes votes # how many times each option beat each other option
|
5
|
+
# puts "scores: #{scores}"
|
6
|
+
# calculate pairwise victories and pairwise defeats
|
7
|
+
victories, defeats = {}, {}
|
8
|
+
options.each {|option1|
|
9
|
+
victories[option1] = options.select {|option2|
|
10
|
+
scores[[option1, option2]] > scores[[option2, option1]]
|
11
|
+
}
|
12
|
+
defeats[option1] = options.select {|option2|
|
13
|
+
scores[[option1, option2]] < scores[[option2, option1]]
|
14
|
+
}
|
15
|
+
}
|
16
|
+
# puts "victories: #{victories}"
|
17
|
+
# puts "defeats: #{defeats}"
|
18
|
+
# calculate results
|
19
|
+
groups = options.group_by {|option|
|
20
|
+
victories[option].size - defeats[option].size
|
21
|
+
}
|
22
|
+
sorted = groups.sort_by {|margin, tied| -margin}
|
23
|
+
# puts 'results:'
|
24
|
+
# sorted.each {|margin, tied| puts " #{tied} --> #{margin}"}
|
25
|
+
sorted.first[1]
|
26
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
def instant_runoff votes
|
2
|
+
votes, remaining = normalize votes
|
3
|
+
while true
|
4
|
+
votes.each {|count, ranking| ranking.delete []}
|
5
|
+
# p votes
|
6
|
+
|
7
|
+
# see how many times each option was ranked first
|
8
|
+
scores = Hash.new 0
|
9
|
+
votes.each {|count, ranking|
|
10
|
+
tied = ranking.first
|
11
|
+
tied.each {|option| scores[option] += count.quo tied.size}
|
12
|
+
}
|
13
|
+
|
14
|
+
# find the options which were ranked first the least
|
15
|
+
groups = remaining.group_by {|option| scores[option]}
|
16
|
+
# p groups
|
17
|
+
leasts = groups.min_by {|score, tied| score}[1]
|
18
|
+
|
19
|
+
# if all remaining options are tied, they are the winners
|
20
|
+
return leasts if leasts.size == remaining.size
|
21
|
+
|
22
|
+
# remove options which were ranked first the least
|
23
|
+
votes.collect! {|count, ranking|
|
24
|
+
[count, ranking.collect {|tied| tied - leasts}]
|
25
|
+
}
|
26
|
+
remaining -= leasts
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
This variant of ranked pairs is similar to the CIVS version. We use winning
|
4
|
+
votes rather than margins to compare preferences, and we keep a preference as
|
5
|
+
long as it does not create a new cycle when considered in conjunction with
|
6
|
+
stricty stronger preferences. Thus, tied preferences can be kept even if they
|
7
|
+
jointly introduce a cycle, as long as none of them does so individually. We
|
8
|
+
depart from CIVS in the final ranking method. Instead of finding successive
|
9
|
+
Schwartz sets, we just take the condensation of the graph. The condensation in
|
10
|
+
this case has a unique topological ordering of equivalence classes, and that is
|
11
|
+
what we use for our ranking.
|
12
|
+
|
13
|
+
=end
|
14
|
+
|
15
|
+
require 'rgl/adjacency'
|
16
|
+
require 'rgl/condensation'
|
17
|
+
require 'rgl/topsort'
|
18
|
+
|
19
|
+
def results_from_pairs sorted_pairs
|
20
|
+
graph = RGL::DirectedAdjacencyGraph.new
|
21
|
+
sorted_pairs.each {|tied|
|
22
|
+
# puts
|
23
|
+
# puts "graph is: "
|
24
|
+
# puts graph
|
25
|
+
# puts "tied is #{tied}"
|
26
|
+
keep = tied.select {|winner, loser|
|
27
|
+
# keep if there is no path from loser to winner, meaning that this edge
|
28
|
+
# will not create a cycle
|
29
|
+
next true if not graph.has_vertex? loser
|
30
|
+
not graph.bfs_iterator(loser).include? winner
|
31
|
+
}
|
32
|
+
# puts "keep is #{keep}"
|
33
|
+
keep.each {|winner, loser| graph.add_edge winner, loser}
|
34
|
+
}
|
35
|
+
# puts "final graph is: "
|
36
|
+
# puts graph
|
37
|
+
# puts
|
38
|
+
condensed = graph.condensation_graph
|
39
|
+
# puts "condensed is "
|
40
|
+
# condensed.edges.each {|edge| p edge.to_a}
|
41
|
+
# puts
|
42
|
+
results = condensed.topsort_iterator.to_a # should be unique
|
43
|
+
# puts "results is "
|
44
|
+
# p results
|
45
|
+
results
|
46
|
+
end
|
47
|
+
|
48
|
+
def ranked_pairs votes
|
49
|
+
votes, options = normalize votes
|
50
|
+
scores = winning_votes votes # how many times each option beat each other option
|
51
|
+
return options if scores.empty? # every vote ranked all the options the same
|
52
|
+
# group and sort the pairs by winning votes
|
53
|
+
groups = scores.keys.group_by {|key| scores[key]}
|
54
|
+
sorted = groups.sort_by {|winning_votes, pairs| -winning_votes}
|
55
|
+
# puts
|
56
|
+
# p sorted
|
57
|
+
sorted_pairs = sorted.transpose[1]
|
58
|
+
# puts
|
59
|
+
# puts 'sorted pairs:'
|
60
|
+
# sorted_pairs.each {|tied| p tied}
|
61
|
+
# puts
|
62
|
+
# puts 'calculating results...'
|
63
|
+
results = results_from_pairs sorted_pairs
|
64
|
+
results.first.to_a
|
65
|
+
end
|
data/lib/util.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
def borda_counts votes, options
|
2
|
+
# calculate the Borda count of each option
|
3
|
+
scores = Hash.new 0
|
4
|
+
votes.each {|count, ranking|
|
5
|
+
# add to the scores
|
6
|
+
remaining = options.size
|
7
|
+
ranking.each_with_index {|tied, i|
|
8
|
+
# each tied option gets the average of their possible ranks
|
9
|
+
average = remaining - (tied.size - 1) / 2r
|
10
|
+
tied.each {|option| scores[option] += count * average}
|
11
|
+
remaining -= tied.size
|
12
|
+
}
|
13
|
+
raise unless remaining == 0 # sanity check
|
14
|
+
}
|
15
|
+
scores
|
16
|
+
end
|
17
|
+
|
18
|
+
def convert_legrand legrand
|
19
|
+
# format from https://www.cse.wustl.edu/~legrand/rbvote/calc.html
|
20
|
+
legrand.each_line.collect {|line|
|
21
|
+
comment = line.index '#'
|
22
|
+
line = line[0...comment] if comment
|
23
|
+
fields = line.strip.split ':'
|
24
|
+
case fields.size
|
25
|
+
when 0 then next
|
26
|
+
when 1
|
27
|
+
# no count provided, so default to 1
|
28
|
+
count = 1
|
29
|
+
ranks = fields[0]
|
30
|
+
when 2
|
31
|
+
count = fields[0].to_i
|
32
|
+
ranks = fields[1]
|
33
|
+
else raise 'only one colon allowed per line'
|
34
|
+
end
|
35
|
+
groups = ranks.split '>'
|
36
|
+
ranking = groups.collect {|group| group.split '='}
|
37
|
+
[count, ranking]
|
38
|
+
}.compact
|
39
|
+
end
|
40
|
+
|
41
|
+
def normalize votes
|
42
|
+
# normalize votes and return along with all options that appeared
|
43
|
+
votes = convert_legrand votes if votes.is_a? String
|
44
|
+
raise 'votes must contain at least one option' if votes.empty?
|
45
|
+
raise 'each vote must have the form [count, ranking]' if votes.find {|vote| vote.size != 2}
|
46
|
+
options = votes.transpose[1].flatten.uniq
|
47
|
+
votes = votes.collect {|count, ranking|
|
48
|
+
raise 'ranking must be an array' unless ranking.is_a? Array
|
49
|
+
# treat missing options as tied for last place
|
50
|
+
missing = options - ranking.flatten
|
51
|
+
ranking = ranking + [missing] unless missing.empty?
|
52
|
+
# make sure no options appear twice
|
53
|
+
raise 'duplicate option in ranking' unless ranking.flatten.size == options.size
|
54
|
+
# normalize single ranks as singleton ties
|
55
|
+
ranking = ranking.collect {|x| x.is_a?(Array) ? x : [x]}
|
56
|
+
# remove empty ties
|
57
|
+
ranking = ranking.select {|tied| not tied.empty?}
|
58
|
+
[count, ranking]
|
59
|
+
}
|
60
|
+
[votes, options]
|
61
|
+
end
|
62
|
+
|
63
|
+
def winning_votes votes
|
64
|
+
# see how many times each option beat each other option
|
65
|
+
scores = Hash.new 0
|
66
|
+
votes.each {|count, ranking|
|
67
|
+
(0...ranking.size).each {|i|
|
68
|
+
(i+1...ranking.size).each {|j|
|
69
|
+
ranking[i].each {|option1|
|
70
|
+
ranking[j].each {|option2|
|
71
|
+
scores[[option1, option2]] += count
|
72
|
+
}
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
77
|
+
scores
|
78
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: voting_systems
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tim Smith
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-07-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rgl
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description:
|
28
|
+
email:
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- lib/systems/baldwin.rb
|
34
|
+
- lib/systems/borda.rb
|
35
|
+
- lib/systems/bucklin.rb
|
36
|
+
- lib/systems/coombs.rb
|
37
|
+
- lib/systems/copeland.rb
|
38
|
+
- lib/systems/instant_runoff.rb
|
39
|
+
- lib/systems/ranked_pairs.rb
|
40
|
+
- lib/util.rb
|
41
|
+
- lib/voting_systems.rb
|
42
|
+
homepage: https://github.com/smithtim/voting_systems
|
43
|
+
licenses:
|
44
|
+
- AGPL-3.0
|
45
|
+
metadata: {}
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 2.7.7
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Implementations of various voting systems.
|
66
|
+
test_files: []
|