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.
- checksums.yaml +7 -0
- data/Area Class Diagram.dia +0 -0
- data/Area Class Diagram.png +0 -0
- data/Class Diagram.dia +0 -0
- data/Class Diagram.png +0 -0
- data/Examples.md +361 -0
- data/LICENSE +674 -0
- data/README.md +70 -0
- data/Rakefile +18 -0
- data/Technical Documentation.md +14 -0
- data/bin/create_installation_package.sh +49 -0
- data/bin/install.sh +45 -0
- data/bin/sapor.rb +22 -0
- data/bin/sapor.sh +105 -0
- data/lib/sapor.rb +44 -0
- data/lib/sapor/binomials_cache.rb +45 -0
- data/lib/sapor/combinations_distribution.rb +180 -0
- data/lib/sapor/dichotomies.rb +98 -0
- data/lib/sapor/dichotomy.rb +138 -0
- data/lib/sapor/first_past_the_post.rb +78 -0
- data/lib/sapor/leveled_proportional.rb +64 -0
- data/lib/sapor/log4r_logger.rb +49 -0
- data/lib/sapor/log_facade.rb +40 -0
- data/lib/sapor/number_formatter.rb +45 -0
- data/lib/sapor/poll.rb +137 -0
- data/lib/sapor/polychotomy.rb +359 -0
- data/lib/sapor/proportional.rb +128 -0
- data/lib/sapor/pseudorandom_multirange_enumerator.rb +87 -0
- data/lib/sapor/regional_data/area.rb +80 -0
- data/lib/sapor/regional_data/catalonia-2012-2015.psv +100 -0
- data/lib/sapor/regional_data/catalonia-2012.psv +87 -0
- data/lib/sapor/regional_data/catalonia.rb +90 -0
- data/lib/sapor/regional_data/norway.rb +408 -0
- data/lib/sapor/regional_data/united_kingdom.rb +1075 -0
- data/lib/sapor/regional_data/utopia.rb +66 -0
- data/sapor.gemspec +35 -0
- data/spec/integration/area_spec.rb +28 -0
- data/spec/integration/poll_spec.rb +107 -0
- data/spec/integration/sample.poll +7 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/unit/area_spec.rb +115 -0
- data/spec/unit/binomials_cache_spec.rb +34 -0
- data/spec/unit/catalonia_spec.rb +82 -0
- data/spec/unit/combinations_distribution_spec.rb +241 -0
- data/spec/unit/denominators_spec.rb +34 -0
- data/spec/unit/dichotomies_spec.rb +154 -0
- data/spec/unit/dichotomy_spec.rb +320 -0
- data/spec/unit/first_past_the_post_spec.rb +53 -0
- data/spec/unit/leveled_proportional_spec.rb +51 -0
- data/spec/unit/norway_spec.rb +47 -0
- data/spec/unit/number_formatter_spec.rb +173 -0
- data/spec/unit/poll_spec.rb +105 -0
- data/spec/unit/polychotomy_spec.rb +332 -0
- data/spec/unit/proportional_spec.rb +86 -0
- data/spec/unit/pseudorandom_multirange_enumerator_spec.rb +82 -0
- 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
|