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,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