sapor 0.1b1

Sign up to get free protection for your applications and to get access to all the features.
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