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.
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ require_relative 'util.rb'
2
+ # require the voting systems
3
+ Dir.glob(__dir__ + '/systems/*.rb') {|f| require_relative f}
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: []