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,53 @@
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
+ SAMPLE_RG_ELECTION_RESULT = { 'Red' => 91_811, 'Green' => 190_934 }
21
+
22
+ SAMPLE_DETAILED_ELECTION_RESULT = { 'North' => { 'Red' => 50, 'Green' => 70
23
+ },
24
+ 'South' => { 'Red' => 70, 'Green' => 50,
25
+ 'Blue' => 100 } }
26
+
27
+ FPTP = Sapor::FirstPastThePost.new(SAMPLE_RG_ELECTION_RESULT,
28
+ SAMPLE_DETAILED_ELECTION_RESULT)
29
+
30
+ describe Sapor::FirstPastThePost, '#project' do
31
+ it 'projects same result as last result if fed with last election result' do
32
+ projection = FPTP.project(SAMPLE_RG_ELECTION_RESULT)
33
+ expect(projection['Red']).to eq(0)
34
+ expect(projection['Green']).to eq(1)
35
+ expect(projection['Other']).to eq(1)
36
+ end
37
+
38
+ it 'it moves a seat to Red if Red is polling well enough' do
39
+ projection = FPTP.project('Red' => 91_811 * 70 / 49,
40
+ 'Green' => 190_934)
41
+ expect(projection['Red']).to eq(1)
42
+ expect(projection['Green']).to eq(0)
43
+ expect(projection['Other']).to eq(1)
44
+ end
45
+
46
+ it 'it moves a seat away from Other if Red is polling well enough' do
47
+ projection = FPTP.project('Red' => 91_811 * 101 / 70,
48
+ 'Green' => 190_934 * 70 / 101)
49
+ expect(projection['Red']).to eq(2)
50
+ expect(projection['Green']).to eq(0)
51
+ expect(projection['Other']).to eq(0)
52
+ end
53
+ end
@@ -0,0 +1,51 @@
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
+ require 'spec_helper'
21
+
22
+ SAMPLE_ELECTION_RESULT = { 'Red' => 120, 'Green' => 120, 'Blue' => 100 }
23
+
24
+ SAMPLE_DETAILED_ELECTION_RESULT = { 'North' => { 'Red' => 50, 'Green' => 70
25
+ },
26
+ 'South' => { 'Red' => 70, 'Green' => 50,
27
+ 'Blue' => 100 } }
28
+
29
+ SAMPLE_SEAT_DISTRIBUTION = { 'North' => 3, 'South' => 5 }
30
+
31
+ SAMPLE_LEVELING_SEATS = 2
32
+
33
+ SAMPLE_LEVELING_THRESHOLD = 0.05
34
+
35
+ SAMPLE_POLL_RESULT = { 'Red' => 120, 'Green' => 120 }
36
+
37
+ LEVELED = Sapor::LeveledProportional.new(SAMPLE_ELECTION_RESULT,
38
+ SAMPLE_DETAILED_ELECTION_RESULT,
39
+ SAMPLE_SEAT_DISTRIBUTION,
40
+ SAMPLE_LEVELING_SEATS,
41
+ SAMPLE_LEVELING_THRESHOLD,
42
+ Sapor::SainteLague14Denominators)
43
+
44
+ describe Sapor::LeveledProportional, '#project' do
45
+ it 'projects same result as last result if fed with last election result' do
46
+ projection = LEVELED.project(SAMPLE_POLL_RESULT)
47
+ expect(projection['Red']).to eq(4)
48
+ expect(projection['Green']).to eq(4)
49
+ expect(projection['Blue']).to eq(2)
50
+ end
51
+ end
@@ -0,0 +1,47 @@
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
+ require 'spec_helper'
21
+
22
+ describe Sapor::Norway, '#seats' do
23
+ it 'calculates the number of seats for the election of 2013 correctly' do
24
+ seats = Sapor::Norway.instance.seats(Sapor::Norway::LAST_ELECTION_RESULT)
25
+ expect(seats['Arbeiderpartiet']).to eq(55)
26
+ expect(seats['Høyre']).to eq(48)
27
+ expect(seats['Fremskrittspartiet']).to eq(29)
28
+ expect(seats['Kristelig Folkeparti']).to eq(10)
29
+ expect(seats['Senterpartiet']).to eq(10)
30
+ expect(seats['Venstre']).to eq(9)
31
+ expect(seats['Sosialistisk Venstreparti']).to eq(7)
32
+ expect(seats['Miljøpartiet de Grønne']).to eq(1)
33
+ expect(seats['Rødt']).to eq(0)
34
+ expect(seats['De Kristne']).to eq(0)
35
+ expect(seats['Pensjonistpartiet']).to eq(0)
36
+ expect(seats['Piratpartiet']).to eq(0)
37
+ expect(seats['Kystpartiet']).to eq(0)
38
+ expect(seats['Demokratene i Norge']).to eq(0)
39
+ expect(seats['Kristent Samlingsparti']).to eq(0)
40
+ expect(seats['Det Liberale Folkepartiet']).to eq(0)
41
+ expect(seats['Norges Kommunistiske Parti']).to eq(0)
42
+ expect(seats['Samfunnspartiet']).to eq(0)
43
+ end
44
+ end
45
+
46
+ # TODO: Sanity check that LAST_DETAILED_ELECTION_RESULT sums up to
47
+ # LAST_ELECTION_RESULT
@@ -0,0 +1,173 @@
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
+ require 'spec_helper'
21
+
22
+ #
23
+ # Test class that mixes in the PercentageFormatter.
24
+ #
25
+ class Formatter
26
+ include Sapor::NumberFormatter
27
+ end
28
+
29
+ describe Sapor::NumberFormatter, '#three_digits_percentage' do
30
+ it 'formats 10 as 1000%' do
31
+ output = Formatter.new.three_digits_percentage(10)
32
+ expect(output).to eq('1000%')
33
+ end
34
+
35
+ it 'formats 1 as 100%' do
36
+ output = Formatter.new.three_digits_percentage(1)
37
+ expect(output).to eq('100%')
38
+ end
39
+
40
+ it 'formats 0.9995 as 100%' do
41
+ output = Formatter.new.three_digits_percentage(0.9995)
42
+ expect(output).to eq('100%')
43
+ end
44
+
45
+ it 'formats 0.99949 as 99.9%' do
46
+ output = Formatter.new.three_digits_percentage(0.99949)
47
+ expect(output).to eq('99.9%')
48
+ end
49
+
50
+ it 'formats 0.09995 as 10.0%' do
51
+ output = Formatter.new.three_digits_percentage(0.09995)
52
+ expect(output).to eq('10.0%')
53
+ end
54
+
55
+ it 'formats 0.099949 as 9.99%' do
56
+ output = Formatter.new.three_digits_percentage(0.099949)
57
+ expect(output).to eq('9.99%')
58
+ end
59
+
60
+ it 'formats 0.009995 as 1.00%' do
61
+ output = Formatter.new.three_digits_percentage(0.009995)
62
+ expect(output).to eq('1.00%')
63
+ end
64
+
65
+ it 'formats 0.0099949 as 0.999%' do
66
+ output = Formatter.new.three_digits_percentage(0.0099949)
67
+ expect(output).to eq('0.999%')
68
+ end
69
+
70
+ it 'formats 0.000005 as 0.001%' do
71
+ output = Formatter.new.three_digits_percentage(0.000005)
72
+ expect(output).to eq('0.001%')
73
+ end
74
+
75
+ it 'formats 0.0000049 as 0%' do
76
+ output = Formatter.new.three_digits_percentage(0.0000049)
77
+ expect(output).to eq('0%')
78
+ end
79
+
80
+ it 'formats 0 as 0%' do
81
+ output = Formatter.new.three_digits_percentage(0)
82
+ expect(output).to eq('0%')
83
+ end
84
+ end
85
+
86
+ describe Sapor::NumberFormatter, '#six_char_percentage' do
87
+ it 'formats 1 as 100.0%' do
88
+ output = Formatter.new.six_char_percentage(1)
89
+ expect(output).to eq('100.0%')
90
+ end
91
+
92
+ it 'formats 0.9995 as 100.0%' do
93
+ output = Formatter.new.six_char_percentage(0.9995)
94
+ expect(output).to eq('100.0%')
95
+ end
96
+
97
+ it 'formats 0.99949 as 99.9%' do
98
+ output = Formatter.new.six_char_percentage(0.99949)
99
+ expect(output).to eq(' 99.9%')
100
+ end
101
+
102
+ it 'formats 0.0995 as 10.0%' do
103
+ output = Formatter.new.six_char_percentage(0.0995)
104
+ expect(output).to eq(' 10.0%')
105
+ end
106
+
107
+ it 'formats 0.09949 as 9.9%' do
108
+ output = Formatter.new.six_char_percentage(0.09949)
109
+ expect(output).to eq(' 9.9%')
110
+ end
111
+
112
+ it 'formats 0.00951 as 1.0%' do
113
+ output = Formatter.new.six_char_percentage(0.00951)
114
+ expect(output).to eq(' 1.0%')
115
+ end
116
+
117
+ it 'formats 0.00949 as 0.9%' do
118
+ output = Formatter.new.six_char_percentage(0.00949)
119
+ expect(output).to eq(' 0.9%')
120
+ end
121
+
122
+ it 'formats 0.0005 as 0.1%' do
123
+ output = Formatter.new.six_char_percentage(0.0005)
124
+ expect(output).to eq(' 0.1%')
125
+ end
126
+
127
+ it 'formats 0.00049 as 0.0%' do
128
+ output = Formatter.new.six_char_percentage(0.00049)
129
+ expect(output).to eq(' 0.0%')
130
+ end
131
+
132
+ it 'formats 0 as 0.0%' do
133
+ output = Formatter.new.six_char_percentage(0)
134
+ expect(output).to eq(' 0.0%')
135
+ end
136
+ end
137
+
138
+ describe Sapor::NumberFormatter, '#with_thousands_separator' do
139
+ it 'formats 0 as 0' do
140
+ output = Formatter.new.with_thousands_separator(0)
141
+ expect(output).to eq('0')
142
+ end
143
+
144
+ it 'formats 1 as 1' do
145
+ output = Formatter.new.with_thousands_separator(1)
146
+ expect(output).to eq('1')
147
+ end
148
+
149
+ it 'formats 999 as 999' do
150
+ output = Formatter.new.with_thousands_separator(999)
151
+ expect(output).to eq('999')
152
+ end
153
+
154
+ it 'formats 1_000 as 1,000' do
155
+ output = Formatter.new.with_thousands_separator(1_000)
156
+ expect(output).to eq('1,000')
157
+ end
158
+
159
+ it 'formats 999_999 as 999,999' do
160
+ output = Formatter.new.with_thousands_separator(999_999)
161
+ expect(output).to eq('999,999')
162
+ end
163
+
164
+ it 'formats 1_000_000 as 1,000,000' do
165
+ output = Formatter.new.with_thousands_separator(1_000_000)
166
+ expect(output).to eq('1,000,000')
167
+ end
168
+
169
+ it 'formats 999_999_999 as 999,999,999' do
170
+ output = Formatter.new.with_thousands_separator(999_999_999)
171
+ expect(output).to eq('999,999,999')
172
+ end
173
+ end
@@ -0,0 +1,105 @@
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
+ require 'spec_helper'
21
+
22
+ SAMPLE_POLL_ARRAY = ['foo=bar', 'baz=qux', '==', 'a=1', 'b=2']
23
+ SAMPLE_METADATA = { 'Area' => 'UT' }
24
+ SAMPLE_RESULTS = { 'Red' => 1, 'Green' => 2, 'Blue' => 3, 'Other' => 1 }
25
+ MAX_ERROR = 0.05
26
+ MAX_VALUE_ERROR = 50_000
27
+
28
+ def sample_poll
29
+ Sapor::Poll.new(SAMPLE_METADATA.dup, SAMPLE_RESULTS.dup)
30
+ end
31
+
32
+ describe Sapor::Poll, '#as_hashes' do
33
+ it 'converts an array of lines into an array with two hashes' do
34
+ hashes = Sapor::Poll.as_hashes(SAMPLE_POLL_ARRAY)
35
+ expect(hashes.size).to eq(2)
36
+ end
37
+
38
+ it 'extracts the metadata into the first array' do
39
+ hashes = Sapor::Poll.as_hashes(SAMPLE_POLL_ARRAY)
40
+ expect(hashes[0]).to eq('foo' => 'bar', 'baz' => 'qux')
41
+ end
42
+
43
+ it 'extracts the results into the second array' do
44
+ hashes = Sapor::Poll.as_hashes(SAMPLE_POLL_ARRAY)
45
+ expect(hashes[1]).to eq('a' => '1', 'b' => '2')
46
+ end
47
+ end
48
+
49
+ describe Sapor::Poll, '#new' do
50
+ it 'sets the area' do
51
+ expect(sample_poll.area.area_code).to eq('UT')
52
+ end
53
+
54
+ it 'sets the results' do
55
+ expect(sample_poll.result('Red')).to eq(1)
56
+ expect(sample_poll.result('Green')).to eq(2)
57
+ expect(sample_poll.result('Blue')).to eq(3)
58
+ end
59
+ end
60
+
61
+ describe Sapor::Poll, '#most_probable_value' do
62
+ it 'returns nil if no analysis has been run' do
63
+ expect(sample_poll.most_probable_value('Blue')).to be_nil
64
+ end
65
+
66
+ it 'gets the most probable value after analysis (50%)' do
67
+ poll = sample_poll
68
+ poll.analyze(MAX_ERROR)
69
+ expect(poll.most_probable_value('Blue')).to \
70
+ be_within(MAX_VALUE_ERROR).of(428_571)
71
+ end
72
+
73
+ it 'gets the most probable value after analysis (33%)' do
74
+ poll = sample_poll
75
+ poll.analyze(MAX_ERROR)
76
+ expect(poll.most_probable_value('Green')).to \
77
+ be_within(MAX_VALUE_ERROR).of(285_714)
78
+ end
79
+ end
80
+
81
+ describe Sapor::Poll, '#most_probable_fraction' do
82
+ it 'returns nil if no analysis has been run' do
83
+ expect(sample_poll.most_probable_fraction('Blue')).to be_nil
84
+ end
85
+
86
+ it 'gets the most probable fraction after analysis (50%)' do
87
+ poll = sample_poll
88
+ poll.analyze(MAX_ERROR)
89
+ expect(poll.most_probable_fraction('Blue')).to \
90
+ be_within(MAX_ERROR).of(3.to_f / 7.to_f)
91
+ end
92
+
93
+ it 'gets the most probable fraction after analysis (33%)' do
94
+ poll = sample_poll
95
+ poll.analyze(MAX_ERROR)
96
+ expect(poll.most_probable_fraction('Green')).to \
97
+ be_within(MAX_ERROR).of(2.to_f / 7.to_f)
98
+ end
99
+ end
100
+
101
+ describe Sapor::Poll, '#confidence_interval' do
102
+ it 'returns nil if no analysis has been run' do
103
+ expect(sample_poll.confidence_interval('Blue')).to be_nil
104
+ end
105
+ end
@@ -0,0 +1,332 @@
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
+ require 'spec_helper'
21
+
22
+ class TestArea
23
+ include Singleton
24
+
25
+ LAST_ELECTION_RESULT = { 'Red' => 91_811, 'Green' => 190_934,
26
+ 'Blue' => 290_647, 'Yellow' => 356_473 }
27
+
28
+ LAST_DETAILED_ELECTION_RESULT = { 'North' => { 'Red' => 50, 'Green' => 70
29
+ },
30
+ 'South' => { 'Red' => 70, 'Green' => 50,
31
+ 'Blue' => 100 },
32
+ 'East' => { 'Red' => 90, 'Green' => 70,
33
+ 'Blue' => 90 },
34
+ 'West' => { 'Red' => 110, 'Green' => 50,
35
+ 'Yellow' => 120 } }
36
+
37
+ ELECTORAL_SYSTEM = Sapor::FirstPastThePost.new(LAST_ELECTION_RESULT,
38
+ LAST_DETAILED_ELECTION_RESULT)
39
+
40
+ def area_code
41
+ 'TA'
42
+ end
43
+
44
+ def coalitions
45
+ [['Red', 'Green'], ['Red', 'Blue']]
46
+ end
47
+
48
+ def population_size
49
+ 1_000
50
+ end
51
+
52
+ def no_of_seats
53
+ LAST_DETAILED_ELECTION_RESULT.size
54
+ end
55
+
56
+ def seats(simulation)
57
+ ELECTORAL_SYSTEM.project(simulation)
58
+ end
59
+ end
60
+
61
+ TEST_AREA = TestArea.instance
62
+
63
+ def pentachotomy(max_error = 0.01)
64
+ results = { 'Red' => 1, 'Green' => 2, 'Blue' => 3, 'Yellow' => 6,
65
+ 'Other' => 1 }
66
+ dichotomies = Sapor::Dichotomies.new(results, 1000)
67
+ dichotomies.refine
68
+ dichotomies.refine
69
+ dichotomies.refine
70
+ Sapor::Polychotomy.new(results, TEST_AREA, dichotomies, max_error)
71
+ end
72
+
73
+ describe Sapor::Polychotomy, '#new' do
74
+ it 'extracts the 99.99% confidence intervals as ranges for max 1% error' do
75
+ expect(pentachotomy.range('Red').size).to eq(17)
76
+ expect(pentachotomy.range('Green').size).to eq(19)
77
+ expect(pentachotomy.range('Blue').size).to eq(20)
78
+ expect(pentachotomy.range('Yellow').size).to eq(22)
79
+ end
80
+
81
+ it 'extracts the 99.9999% confidence intervals as ranges for max 0.1%' \
82
+ ' error' do
83
+ expect(pentachotomy(0.001).range('Red').size).to eq(20)
84
+ expect(pentachotomy(0.001).range('Green').size).to eq(22)
85
+ expect(pentachotomy(0.001).range('Blue').size).to eq(23)
86
+ expect(pentachotomy(0.001).range('Yellow').size).to eq(25)
87
+ end
88
+
89
+ # TODO: A test case where the construction of the enumerator fails
90
+ end
91
+
92
+ describe Sapor::Polychotomy, '#error_estimate' do
93
+ it 'is 1 by default (no refinement)' do
94
+ polychotomy = pentachotomy
95
+ expect(polychotomy.error_estimate).to eq(1.0)
96
+ end
97
+
98
+ it 'is 1 after the first refinement' do
99
+ polychotomy = pentachotomy
100
+ polychotomy.refine
101
+ expect(polychotomy.error_estimate).to eq(1.0)
102
+ end
103
+
104
+ it 'is at least the difference between the most probable fractions (two' \
105
+ ' refinements)' do
106
+ polychotomy = pentachotomy
107
+ polychotomy.refine
108
+ polychotomy.refine
109
+ expect(polychotomy.error_estimate.round(3)).to eq(0.333)
110
+ end
111
+
112
+ it 'is at least the difference between the most probable fractions (three' \
113
+ ' refinements)' do
114
+ polychotomy = pentachotomy
115
+ polychotomy.refine
116
+ polychotomy.refine
117
+ polychotomy.refine
118
+ expect(polychotomy.error_estimate.round(3)).to eq(0.259)
119
+ end
120
+ end
121
+
122
+ describe Sapor::Polychotomy, '#most_probable_fraction' do
123
+ it 'is nil by default (no refinement)' do
124
+ polychotomy = pentachotomy
125
+ expect(polychotomy.most_probable_fraction('Red')).to be_nil
126
+ expect(polychotomy.most_probable_fraction('Green')).to be_nil
127
+ expect(polychotomy.most_probable_fraction('Blue')).to be_nil
128
+ expect(polychotomy.most_probable_fraction('Yellow')).to be_nil
129
+ end
130
+
131
+ it 'returns a value after the first refinement' do
132
+ polychotomy = pentachotomy
133
+ polychotomy.refine
134
+ expect(polychotomy.most_probable_fraction('Red').round(2)).to eq(0.02)
135
+ expect(polychotomy.most_probable_fraction('Green').round(2)).to eq(0.02)
136
+ expect(polychotomy.most_probable_fraction('Blue').round(2)).to eq(0.02)
137
+ expect(polychotomy.most_probable_fraction('Yellow').round(2)).to eq(0.09)
138
+ end
139
+
140
+ it 'returns an updated value after the second refinement' do
141
+ polychotomy = pentachotomy
142
+ polychotomy.refine
143
+ polychotomy.refine
144
+ expect(polychotomy.most_probable_fraction('Red').round(2)).to eq(0.28)
145
+ expect(polychotomy.most_probable_fraction('Green').round(2)).to eq(0.09)
146
+ expect(polychotomy.most_probable_fraction('Blue').round(2)).to eq(0.35)
147
+ expect(polychotomy.most_probable_fraction('Yellow').round(2)).to eq(0.20)
148
+ end
149
+
150
+ it 'returns an updated value after the third refinement' do
151
+ polychotomy = pentachotomy
152
+ polychotomy.refine
153
+ polychotomy.refine
154
+ polychotomy.refine
155
+ expect(polychotomy.most_probable_fraction('Red').round(2)).to eq(0.35)
156
+ expect(polychotomy.most_probable_fraction('Green').round(2)).to eq(0.09)
157
+ expect(polychotomy.most_probable_fraction('Blue').round(2)).to eq(0.09)
158
+ expect(polychotomy.most_probable_fraction('Yellow').round(2)).to eq(0.39)
159
+ end
160
+ end
161
+
162
+ describe Sapor::Polychotomy, '#no_of_data_points' do
163
+ it 'is 0 by default (no refinement)' do
164
+ polychotomy = pentachotomy
165
+ expect(polychotomy.no_of_data_points).to eq(0)
166
+ end
167
+
168
+ it 'is 1 after 1 refinement' do
169
+ polychotomy = pentachotomy
170
+ polychotomy.refine
171
+ expect(polychotomy.no_of_data_points).to eq(1)
172
+ end
173
+
174
+ it 'is 19 after 2 refinements' do
175
+ polychotomy = pentachotomy
176
+ polychotomy.refine
177
+ polychotomy.refine
178
+ expect(polychotomy.no_of_data_points).to eq(8)
179
+ end
180
+
181
+ it 'is 38 after 3 refinements' do
182
+ polychotomy = pentachotomy
183
+ polychotomy.refine
184
+ polychotomy.refine
185
+ polychotomy.refine
186
+ expect(polychotomy.no_of_data_points).to eq(27)
187
+ end
188
+
189
+ # TODO: A test case that runs all the simulations
190
+ end
191
+
192
+ describe Sapor::Polychotomy, '#no_of_simulations' do
193
+ it 'is 0 by default (no refinement)' do
194
+ polychotomy = pentachotomy
195
+ expect(polychotomy.no_of_simulations).to eq(0)
196
+ end
197
+
198
+ it 'is 1 after 1 refinement' do
199
+ polychotomy = pentachotomy
200
+ polychotomy.refine
201
+ expect(polychotomy.no_of_simulations).to eq(1)
202
+ end
203
+
204
+ it 'is 2 after 2 refinements' do
205
+ polychotomy = pentachotomy
206
+ polychotomy.refine
207
+ polychotomy.refine
208
+ expect(polychotomy.no_of_simulations).to eq(2)
209
+ end
210
+
211
+ it 'is 4 after 3 refinements' do
212
+ polychotomy = pentachotomy
213
+ polychotomy.refine
214
+ polychotomy.refine
215
+ polychotomy.refine
216
+ expect(polychotomy.no_of_simulations).to eq(4)
217
+ end
218
+
219
+ # TODO: A test case that runs until the end of the enumerator
220
+ end
221
+
222
+ describe Sapor::Polychotomy, '#progress_report' do
223
+ it 'reports progress with the number of simulations and data points,' \
224
+ ' together with the search space size and the fraction that already has' \
225
+ ' been searched' do
226
+ expected_report = '1 simulations out of 1 data points, 1 / 142,120 of' \
227
+ ' search space size (142,120).'
228
+ polychotomy = pentachotomy
229
+ polychotomy.refine
230
+ expect(polychotomy.progress_report).to eq(expected_report)
231
+ end
232
+
233
+ it 'reports no of simulations and data points with thousands separator' do
234
+ expected_report = '1,024 simulations out of 9,287 data points, 1 / 15' \
235
+ ' of search space size (142,120).'
236
+ polychotomy = pentachotomy
237
+ 11.times { polychotomy.refine }
238
+ expect(polychotomy.progress_report).to eq(expected_report)
239
+ end
240
+
241
+ it 'rounds the fraction of the space size searched with 1 decimal if' \
242
+ ' larger than one tenth' do
243
+ expected_report = '2,048 simulations out of 19,433 data points, 1 / 7.0' \
244
+ ' of search space size (142,120).'
245
+ polychotomy = pentachotomy
246
+ 12.times { polychotomy.refine }
247
+ expect(polychotomy.progress_report).to eq(expected_report)
248
+ end
249
+ end
250
+
251
+ describe Sapor::Polychotomy, '#report' do
252
+ it 'produces a report after first refinement for short choice labels' do
253
+ expected_report = 'Most probable rounded fractions, fractions and 95%' \
254
+ " confidence intervals:\n" \
255
+ "Choice Result MPRF MPF CI(95%) P(>↓) Seats\n" \
256
+ "Yellow 46.2% 5.6% 9.3% 9.3%– 11.1% 100.0% 1–1\n" + # TODO: Shouldn't MPRF be 5.6% and CI 9.3%–9.3%?
257
+ "Blue 23.1% 1.9% 1.9% 1.9%– 3.7% 0.0% 0–0\n" + # TODO: Shouldn't CI be 1.9%–1.9%?
258
+ "Green 15.4% 1.9% 1.9% 1.9%– 3.7% 0.0% 0–0\n" + # TODO: Shouldn't CI be 1.9%–1.9%?
259
+ "Red 7.7% 1.9% 1.9% 1.9%– 3.7% 3–3\n" + # TODO: Shouldn't CI be 1.9%–1.9%?
260
+ "Coalition Result MPRF MPF CI(95%) P(>50%) Seats P(>50%)\n" +
261
+ "Blue + Red 30.8% 50.0% 3.8% 3.8%– 3.8% 0.0% 3–3 100.0%\n" +
262
+ 'Green + Red 23.1% 50.0% 3.8% 3.8%– 3.8% 0.0% 3–3 100.0%'
263
+ polychotomy = pentachotomy
264
+ polychotomy.refine
265
+ expect(polychotomy.report).to eq(expected_report)
266
+ end
267
+
268
+ it 'produces a report by default for long choice labels' do
269
+ results = { 'Dark Red' => 1, 'Light Green' => 2, 'Medium Blue' => 3,
270
+ 'Other' => 1 }
271
+ dichotomies = Sapor::Dichotomies.new(results, 1000)
272
+ dichotomies.refine
273
+ dichotomies.refine
274
+ dichotomies.refine
275
+ polychotomy = Sapor::Polychotomy.new(results, TEST_AREA, dichotomies, 0.01)
276
+ polychotomy.refine
277
+ expected_report = 'Most probable rounded fractions, fractions and 95%' \
278
+ " confidence intervals:\n" \
279
+ "Choice Result MPRF MPF CI(95%) P(>↓) Seats\n" \
280
+ "Medium Blue 42.9% 1.9% 1.9% 1.9%– 3.7% 0.0% 0–0\n" + # TODO: Shouldn't CI be 1.9%–1.9%?
281
+ "Light Green 28.6% 1.9% 1.9% 1.9%– 3.7% 0.0% 0–0\n" + # TODO: Shouldn't CI be 1.9%–1.9%?
282
+ "Dark Red 14.3% 1.9% 1.9% 1.9%– 3.7% 0–0\n" + # TODO: Shouldn't CI be 1.9%–1.9%?
283
+ # TODO: "Red 3–3\n" \
284
+ # TODO: 'Yellow 1–1'
285
+ "Coalition Result MPRF MPF CI(95%) P(>50%) Seats P(>50%)\n" +
286
+ "Blue + Red 0.0% 50.0% 0.0% 0.0%– 0.0% 0.0% 0–0 0.0%\n" +
287
+ 'Green + Red 0.0% 50.0% 0.0% 0.0%– 0.0% 0.0% 0–0 0.0%'
288
+ expect(polychotomy.report).to eq(expected_report)
289
+ end
290
+
291
+ it 'produces a report by default for short choice labels' do
292
+ results = { 'Red' => 1, 'Green' => 2, 'Blue' => 3, 'Other' => 1 }
293
+ dichotomies = Sapor::Dichotomies.new(results, 1000)
294
+ dichotomies.refine
295
+ dichotomies.refine
296
+ dichotomies.refine
297
+ polychotomy = Sapor::Polychotomy.new(results, TEST_AREA, dichotomies, 0.01)
298
+ polychotomy.refine
299
+ expected_report = 'Most probable rounded fractions, fractions and 95%' \
300
+ " confidence intervals:\n" \
301
+ "Choice Result MPRF MPF CI(95%) P(>↓) Seats\n" \
302
+ "Blue 42.9% 1.9% 1.9% 1.9%– 3.7% 0.0% 0–0\n" + # TODO: Shouldn't CI be 1.9%–1.9%?
303
+ "Green 28.6% 1.9% 1.9% 1.9%– 3.7% 0.0% 0–0\n" + # TODO: Shouldn't CI be 1.9%–1.9%?
304
+ "Red 14.3% 1.9% 1.9% 1.9%– 3.7% 3–3\n" + # TODO: Shouldn't CI be 1.9%–1.9%?
305
+ # TODO: 'Yellow 1–1'
306
+ "Coalition Result MPRF MPF CI(95%) P(>50%) Seats P(>50%)\n" +
307
+ "Blue + Red 57.1% 50.0% 3.8% 3.8%– 3.8% 0.0% 3–3 100.0%\n" +
308
+ 'Green + Red 42.9% 50.0% 3.8% 3.8%– 3.8% 0.0% 3–3 100.0%'
309
+ expect(polychotomy.report).to eq(expected_report)
310
+ end
311
+
312
+ it 'sorts line with the same results alphabetically' do
313
+ results = { 'Red' => 1, 'Green' => 1, 'Blue' => 1, 'Other' => 1 }
314
+ dichotomies = Sapor::Dichotomies.new(results, 1000)
315
+ dichotomies.refine
316
+ dichotomies.refine
317
+ dichotomies.refine
318
+ polychotomy = Sapor::Polychotomy.new(results, TEST_AREA, dichotomies, 0.01)
319
+ polychotomy.refine
320
+ expected_report = 'Most probable rounded fractions, fractions and 95%' \
321
+ " confidence intervals:\n" \
322
+ "Choice Result MPRF MPF CI(95%) P(>↓) Seats\n" \
323
+ "Blue 25.0% 1.9% 1.9% 1.9%– 3.7% 0.0% 0–0\n" + # TODO: Shouldn't CI be 1.9%–1.9%?
324
+ "Green 25.0% 1.9% 1.9% 1.9%– 3.7% 0.0% 0–0\n" + # TODO: Shouldn't CI be 1.9%–1.9%?
325
+ "Red 25.0% 1.9% 1.9% 1.9%– 3.7% 3–3\n" + # TODO: Shouldn't CI be 1.9%–1.9%?
326
+ # TODO: 'Yellow 1–1'
327
+ "Coalition Result MPRF MPF CI(95%) P(>50%) Seats P(>50%)\n" +
328
+ "Blue + Red 50.0% 50.0% 3.8% 3.8%– 3.8% 0.0% 3–3 100.0%\n" +
329
+ 'Green + Red 50.0% 50.0% 3.8% 3.8%– 3.8% 0.0% 3–3 100.0%'
330
+ expect(polychotomy.report).to eq(expected_report)
331
+ end
332
+ end