sapor 0.1b1

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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/Area Class Diagram.dia +0 -0
  3. data/Area Class Diagram.png +0 -0
  4. data/Class Diagram.dia +0 -0
  5. data/Class Diagram.png +0 -0
  6. data/Examples.md +361 -0
  7. data/LICENSE +674 -0
  8. data/README.md +70 -0
  9. data/Rakefile +18 -0
  10. data/Technical Documentation.md +14 -0
  11. data/bin/create_installation_package.sh +49 -0
  12. data/bin/install.sh +45 -0
  13. data/bin/sapor.rb +22 -0
  14. data/bin/sapor.sh +105 -0
  15. data/lib/sapor.rb +44 -0
  16. data/lib/sapor/binomials_cache.rb +45 -0
  17. data/lib/sapor/combinations_distribution.rb +180 -0
  18. data/lib/sapor/dichotomies.rb +98 -0
  19. data/lib/sapor/dichotomy.rb +138 -0
  20. data/lib/sapor/first_past_the_post.rb +78 -0
  21. data/lib/sapor/leveled_proportional.rb +64 -0
  22. data/lib/sapor/log4r_logger.rb +49 -0
  23. data/lib/sapor/log_facade.rb +40 -0
  24. data/lib/sapor/number_formatter.rb +45 -0
  25. data/lib/sapor/poll.rb +137 -0
  26. data/lib/sapor/polychotomy.rb +359 -0
  27. data/lib/sapor/proportional.rb +128 -0
  28. data/lib/sapor/pseudorandom_multirange_enumerator.rb +87 -0
  29. data/lib/sapor/regional_data/area.rb +80 -0
  30. data/lib/sapor/regional_data/catalonia-2012-2015.psv +100 -0
  31. data/lib/sapor/regional_data/catalonia-2012.psv +87 -0
  32. data/lib/sapor/regional_data/catalonia.rb +90 -0
  33. data/lib/sapor/regional_data/norway.rb +408 -0
  34. data/lib/sapor/regional_data/united_kingdom.rb +1075 -0
  35. data/lib/sapor/regional_data/utopia.rb +66 -0
  36. data/sapor.gemspec +35 -0
  37. data/spec/integration/area_spec.rb +28 -0
  38. data/spec/integration/poll_spec.rb +107 -0
  39. data/spec/integration/sample.poll +7 -0
  40. data/spec/spec_helper.rb +31 -0
  41. data/spec/unit/area_spec.rb +115 -0
  42. data/spec/unit/binomials_cache_spec.rb +34 -0
  43. data/spec/unit/catalonia_spec.rb +82 -0
  44. data/spec/unit/combinations_distribution_spec.rb +241 -0
  45. data/spec/unit/denominators_spec.rb +34 -0
  46. data/spec/unit/dichotomies_spec.rb +154 -0
  47. data/spec/unit/dichotomy_spec.rb +320 -0
  48. data/spec/unit/first_past_the_post_spec.rb +53 -0
  49. data/spec/unit/leveled_proportional_spec.rb +51 -0
  50. data/spec/unit/norway_spec.rb +47 -0
  51. data/spec/unit/number_formatter_spec.rb +173 -0
  52. data/spec/unit/poll_spec.rb +105 -0
  53. data/spec/unit/polychotomy_spec.rb +332 -0
  54. data/spec/unit/proportional_spec.rb +86 -0
  55. data/spec/unit/pseudorandom_multirange_enumerator_spec.rb +82 -0
  56. metadata +119 -0
@@ -0,0 +1,98 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Statistical Analysis of Polling Results (SAPoR)
4
+ # Copyright (C) 2014 Filip van Laenen <f.a.vanlaenen@ieee.org>
5
+ #
6
+ # This file is part of SAPoR.
7
+ #
8
+ # SAPoR is free software: you can redistribute it and/or modify it under the
9
+ # terms of the GNU General Public License as published by the Free Software
10
+ # Foundation, either version 3 of the License, or (at your option) any later
11
+ # version.
12
+ #
13
+ # SAPoR is distributed in the hope that it will be useful, but WITHOUT ANY
14
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15
+ # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16
+ #
17
+ # You can find a copy of the GNU General Public License in /doc/gpl.txt
18
+ #
19
+
20
+ module Sapor
21
+ #
22
+ # Represents a set of dichotomies.
23
+ #
24
+ class Dichotomies
25
+ include NumberFormatter
26
+
27
+ def initialize(results, population_size, threshold = nil)
28
+ sample_size = results.values.inject(:+)
29
+ @dichotomy_hash = {}
30
+ results.each_pair do |choice, number|
31
+ @dichotomy_hash[choice] = Dichotomy.new(number, sample_size,
32
+ population_size)
33
+ end
34
+ @threshold = threshold
35
+ end
36
+
37
+ def refine
38
+ @dichotomy_hash.values.each(&:refine)
39
+ end
40
+
41
+ def error_estimate
42
+ @dichotomy_hash.values.map(&:error_estimate).max
43
+ end
44
+
45
+ def confidence_interval_values(choice, level)
46
+ @dichotomy_hash[choice].confidence_interval_values(level)
47
+ end
48
+
49
+ def report
50
+ choice_lengths = @dichotomy_hash.keys.map(&:length)
51
+ choice_lengths << 6
52
+ max_choice_width = choice_lengths.max
53
+ sorted_choices = sort_choices_by_label_and_mpv
54
+ lines = sorted_choices.map do |choice|
55
+ create_report_line(choice, @dichotomy_hash[choice], max_choice_width)
56
+ end
57
+ "Most probable fractions and 95% confidence intervals:\n" +
58
+ 'Choice'.ljust(max_choice_width) + ' MPF CI(95%)' +
59
+ (@threshold.nil? ? '' : ' P(≥' + (100 * @threshold).to_i.to_s +
60
+ '%)') +
61
+ "\n" + lines.join("\n")
62
+ end
63
+
64
+ def progress_report
65
+ size = @dichotomy_hash.values.first.values.size
66
+ "Number of data points: #{with_thousands_separator(size)}."
67
+ end
68
+
69
+ private
70
+
71
+ def compare_choices_by_label_and_mpv(a, b)
72
+ if a == OTHER
73
+ 1
74
+ elsif b == OTHER
75
+ -1
76
+ else
77
+ mpv_a = @dichotomy_hash[a].most_probable_value
78
+ mpv_b = @dichotomy_hash[b].most_probable_value
79
+ mpv_a == mpv_b ? a <=> b : mpv_b <=> mpv_a
80
+ end
81
+ end
82
+
83
+ def sort_choices_by_label_and_mpv
84
+ @dichotomy_hash.keys.sort do |a, b|
85
+ compare_choices_by_label_and_mpv(a, b)
86
+ end
87
+ end
88
+
89
+ def create_report_line(choice, dichotomy, max_choice_width)
90
+ choice.ljust(max_choice_width) + ' ' + \
91
+ six_char_percentage(dichotomy.most_probable_fraction) + ' ' + \
92
+ six_char_percentage(dichotomy.confidence_interval.first) + '–' + \
93
+ six_char_percentage(dichotomy.confidence_interval.last) +
94
+ (@threshold.nil? ? '' : ' ' +
95
+ six_char_percentage(dichotomy.threshold_probability(@threshold)))
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,138 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Statistical Analysis of Polling Results (SAPoR)
4
+ # Copyright (C) 2014 Filip van Laenen <f.a.vanlaenen@ieee.org>
5
+ #
6
+ # This file is part of SAPoR.
7
+ #
8
+ # SAPoR is free software: you can redistribute it and/or modify it under the
9
+ # terms of the GNU General Public License as published by the Free Software
10
+ # Foundation, either version 3 of the License, or (at your option) any later
11
+ # version.
12
+ #
13
+ # SAPoR is distributed in the hope that it will be useful, but WITHOUT ANY
14
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15
+ # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16
+ #
17
+ # You can find a copy of the GNU General Public License in /doc/gpl.txt
18
+ #
19
+
20
+ module Sapor
21
+ #
22
+ # Represents a dichotomy.
23
+ #
24
+ class Dichotomy
25
+ def initialize(number, sample_size, population_size)
26
+ @number = number
27
+ @sample_size = sample_size
28
+ @population_size = population_size
29
+ @distribution = CombinationsDistribution.new
30
+ middle = @population_size / 2
31
+ @distribution[middle] = combinations_for(middle)
32
+ end
33
+
34
+ def combinations(value)
35
+ @distribution[value]
36
+ end
37
+
38
+ def confidence_interval(level = 0.95)
39
+ value_confidence_interval(level).map { |x| x.to_f / @population_size }
40
+ end
41
+
42
+ def confidence_interval_values(level = 0.95)
43
+ @distribution.confidence_interval_values(level)
44
+ end
45
+
46
+ def error_estimate
47
+ estimate_error
48
+ end
49
+
50
+ def most_probable_fraction
51
+ most_probable_value.to_f / @population_size
52
+ end
53
+
54
+ def most_probable_value
55
+ @distribution.most_probable_value
56
+ end
57
+
58
+ def refine
59
+ new_values = find_refinement_values
60
+ new_values.each do |value|
61
+ @distribution[value] = combinations_for(value)
62
+ end
63
+ end
64
+
65
+ def threshold_probability(threshold)
66
+ @distribution.threshold_probability(threshold, @population_size)
67
+ end
68
+
69
+ def value_confidence_interval(level = 0.95)
70
+ @distribution.confidence_interval(level, @population_size)
71
+ end
72
+
73
+ def values
74
+ @distribution.values
75
+ end
76
+
77
+ private
78
+
79
+ def combinations_for(value)
80
+ dual_value = @population_size - value
81
+ dual_number = @sample_size - @number
82
+ if value < @number || dual_value < dual_number
83
+ 0.to_lf
84
+ else
85
+ BinomialsCache.binomial(value, @number) * \
86
+ BinomialsCache.binomial(dual_value, dual_number)
87
+ end
88
+ end
89
+
90
+ def find_refinement_value_at_bottom
91
+ bottom = values.min
92
+ if bottom == 0
93
+ []
94
+ else
95
+ [(bottom.to_f / 3).round]
96
+ end
97
+ end
98
+
99
+ def find_refinement_values_between(low, high)
100
+ new_low = ((2 * low + high).to_f / 3).round
101
+ new_high = ((2 * high + low).to_f / 3).round
102
+ refinement_values = []
103
+ refinement_values << new_high unless new_high == high
104
+ refinement_values << new_low unless new_low == new_high || new_low == low
105
+ refinement_values
106
+ end
107
+
108
+ def find_refinement_values_in_between
109
+ sorted_values = values.sort
110
+ refinement_values = (0..(@distribution.size - 2)).to_a.map do |i|
111
+ find_refinement_values_between(sorted_values[i], sorted_values[i + 1])
112
+ end
113
+ refinement_values.flatten
114
+ end
115
+
116
+ def find_refinement_value_at_top
117
+ top = values.max
118
+ if top == @population_size
119
+ []
120
+ else
121
+ [((2.to_f * @population_size + top) / 3).round]
122
+ end
123
+ end
124
+
125
+ def find_refinement_values
126
+ find_refinement_value_at_bottom + find_refinement_values_in_between +
127
+ find_refinement_value_at_top
128
+ end
129
+
130
+ def estimate_error
131
+ if @distribution.size == @population_size + 1
132
+ 0
133
+ else
134
+ 1.to_f / @distribution.size
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,78 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Statistical Analysis of Polling Results (SAPoR)
4
+ # Copyright (C) 2014 Filip van Laenen <f.a.vanlaenen@ieee.org>
5
+ #
6
+ # This file is part of SAPoR.
7
+ #
8
+ # SAPoR is free software: you can redistribute it and/or modify it under the
9
+ # terms of the GNU General Public License as published by the Free Software
10
+ # Foundation, either version 3 of the License, or (at your option) any later
11
+ # version.
12
+ #
13
+ # SAPoR is distributed in the hope that it will be useful, but WITHOUT ANY
14
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15
+ # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16
+ #
17
+ # You can find a copy of the GNU General Public License in /doc/gpl.txt
18
+ #
19
+
20
+ module Sapor
21
+ #
22
+ # Class representing an electoral system based on the First-Past-The-Post
23
+ # system.
24
+ #
25
+ class FirstPastThePost
26
+ def initialize(last_election_result, last_detailed_election_result)
27
+ @last_election_result = last_election_result
28
+ @last_detailed_election_result = last_detailed_election_result
29
+ end
30
+
31
+ def project(simulation)
32
+ multiplicators = calculate_multiplicators(simulation)
33
+ result = create_empty_result(simulation)
34
+ @last_detailed_election_result.each_value do |local_last_result|
35
+ winner = local_winner(local_last_result, multiplicators)
36
+ result[winner] += 1
37
+ end
38
+ result
39
+ end
40
+
41
+ private
42
+
43
+ def calculate_multiplicators(simulation)
44
+ simulation_sum = simulation.values.inject(:+)
45
+ last_election_sum = @last_election_result.values.inject(:+)
46
+ multiplicators = {}
47
+ simulation.each_key do |choice|
48
+ new_fraction = simulation[choice].to_f / simulation_sum
49
+ last_fraction = @last_election_result[choice].to_f / last_election_sum
50
+ multiplicators[choice] = new_fraction / last_fraction
51
+ end
52
+ multiplicators
53
+ end
54
+
55
+ def create_empty_result(simulation)
56
+ result = {}
57
+ simulation.each_key do |choice|
58
+ result[choice] = 0
59
+ end
60
+ result[OTHER] = 0
61
+ result
62
+ end
63
+
64
+ def local_winner(local_last_result, multiplicators)
65
+ new_local_result = {}
66
+ local_last_result.each_pair do |choice, votes|
67
+ if multiplicators.key?(choice)
68
+ new_local_result[choice] = votes * multiplicators[choice]
69
+ else
70
+ new_local_result[choice] = votes
71
+ end
72
+ end
73
+ max_votes = new_local_result.values.max
74
+ winner = new_local_result.select { |_k, v| v.equal?(max_votes) }.keys.first
75
+ multiplicators.key?(winner) ? winner : OTHER
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Statistical Analysis of Polling Results (SAPoR)
4
+ # Copyright (C) 2014 Filip van Laenen <f.a.vanlaenen@ieee.org>
5
+ #
6
+ # This file is part of SAPoR.
7
+ #
8
+ # SAPoR is free software: you can redistribute it and/or modify it under the
9
+ # terms of the GNU General Public License as published by the Free Software
10
+ # Foundation, either version 3 of the License, or (at your option) any later
11
+ # version.
12
+ #
13
+ # SAPoR is distributed in the hope that it will be useful, but WITHOUT ANY
14
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15
+ # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16
+ #
17
+ # You can find a copy of the GNU General Public License in /doc/gpl.txt
18
+ #
19
+
20
+ module Sapor
21
+ #
22
+ # Class representing a proportional electoral system with a second round of
23
+ # leveling seats.
24
+ #
25
+ class LeveledProportional
26
+ def initialize(last_election_result, last_detailed_election_result,
27
+ seat_distribution, leveling_seats, leveling_threshold,
28
+ denominators_class)
29
+ @proportional = Proportional.new(last_election_result,
30
+ last_detailed_election_result,
31
+ seat_distribution, denominators_class)
32
+ @leveling_seats = leveling_seats
33
+ @leveling_threshold = leveling_threshold
34
+ @denominators_class = denominators_class
35
+ end
36
+
37
+ def project(simulation)
38
+ result = @proportional.project(simulation)
39
+ threshold = @leveling_threshold * simulation.values.inject(:+)
40
+ quotients = []
41
+ simulation.each_pair do |choice, votes|
42
+ next if votes < threshold
43
+ denominators(result[choice]).each do |d|
44
+ quotients << [choice, votes.to_f / d]
45
+ end
46
+ end
47
+ seats = quotients.sort { |a, b| b.last <=> a.last }.map(&:first).slice(0, @leveling_seats)
48
+ seats.each do |seat|
49
+ if result.key?(seat)
50
+ result[seat] += 1
51
+ else
52
+ result[seat] = 1
53
+ end
54
+ end
55
+ result
56
+ end
57
+
58
+ private
59
+
60
+ def denominators(seats)
61
+ @denominators_class.get(seats + @leveling_seats).reverse_each.take(@leveling_seats)
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Statistical Analysis of Polling Results (SAPoR)
4
+ # Copyright (C) 2014 Filip van Laenen <f.a.vanlaenen@ieee.org>
5
+ #
6
+ # This file is part of SAPoR.
7
+ #
8
+ # SAPoR is free software: you can redistribute it and/or modify it under the
9
+ # terms of the GNU General Public License as published by the Free Software
10
+ # Foundation, either version 3 of the License, or (at your option) any later
11
+ # version.
12
+ #
13
+ # SAPoR is distributed in the hope that it will be useful, but WITHOUT ANY
14
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15
+ # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16
+ #
17
+ # You can find a copy of the GNU General Public License in /doc/gpl.txt
18
+ #
19
+
20
+ module Sapor
21
+ #
22
+ # Builder to create Log4rLoggers.
23
+ #
24
+ class Log4rLoggerBuilder
25
+ def create_logger
26
+ Log4rLogger.new
27
+ end
28
+ end
29
+
30
+ require 'rubygems'
31
+ require 'log4r'
32
+ #
33
+ # Logger using Log4r.
34
+ #
35
+ class Log4rLogger
36
+ def initialize
37
+ @logger = Log4r::Logger.new 'stdout'
38
+ stdout_outputter = Log4r::Outputter.stdout
39
+ @logger.outputters = stdout_outputter
40
+ stdout_format = Log4r::PatternFormatter.new(pattern: '%d %l: %m')
41
+ stdout_outputter.formatter = stdout_format
42
+ @logger.level = Log4r::INFO
43
+ end
44
+
45
+ def info(message)
46
+ @logger.info(message)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Statistical Analysis of Polling Results (SAPoR)
4
+ # Copyright (C) 2014 Filip van Laenen <f.a.vanlaenen@ieee.org>
5
+ #
6
+ # This file is part of SAPoR.
7
+ #
8
+ # SAPoR is free software: you can redistribute it and/or modify it under the
9
+ # terms of the GNU General Public License as published by the Free Software
10
+ # Foundation, either version 3 of the License, or (at your option) any later
11
+ # version.
12
+ #
13
+ # SAPoR is distributed in the hope that it will be useful, but WITHOUT ANY
14
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15
+ # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16
+ #
17
+ # You can find a copy of the GNU General Public License in /doc/gpl.txt
18
+ #
19
+
20
+ module Sapor
21
+ #
22
+ # A facade for logging.
23
+ #
24
+ class LogFacade
25
+ @logger_builder = nil
26
+
27
+ class << self
28
+ attr_writer :logger_builder
29
+ end
30
+
31
+ def self.builder
32
+ @logger_builder = Log4rLoggerBuilder.new if @logger_builder.nil?
33
+ @logger_builder
34
+ end
35
+
36
+ def self.create_logger
37
+ builder.create_logger
38
+ end
39
+ end
40
+ end