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,241 @@
|
|
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::CombinationsDistribution, '#new' do
|
23
|
+
it 'creates an empty distribution' do
|
24
|
+
distribution = Sapor::CombinationsDistribution.new
|
25
|
+
expect(distribution).to be_empty
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe Sapor::CombinationsDistribution, '#[]=' do
|
30
|
+
it 'sets a value-combinations pair' do
|
31
|
+
distribution = Sapor::CombinationsDistribution.new
|
32
|
+
distribution[1] = 2
|
33
|
+
expect(distribution[1]).to eq(2)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe Sapor::CombinationsDistribution, '#+' do
|
38
|
+
it 'adds two combiation distributions together' do
|
39
|
+
a = Sapor::CombinationsDistribution.new
|
40
|
+
a[10] = 2
|
41
|
+
a[21] = 3
|
42
|
+
a[23] = 11
|
43
|
+
b = Sapor::CombinationsDistribution.new
|
44
|
+
b[10] = 5
|
45
|
+
b[21] = 7
|
46
|
+
b[29] = 13
|
47
|
+
sum = Sapor::CombinationsDistribution.new
|
48
|
+
sum[10] = 7
|
49
|
+
sum[21] = 10
|
50
|
+
sum[23] = 11
|
51
|
+
sum[29] = 13
|
52
|
+
expect((a + b)[10]).to eq(sum[10])
|
53
|
+
expect((a + b)[21]).to eq(sum[21])
|
54
|
+
expect((a + b)[23]).to eq(sum[23])
|
55
|
+
expect((a + b)[29]).to eq(sum[29])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe Sapor::CombinationsDistribution, '#confidence_interval' do
|
60
|
+
it 'returns the single value as boundaries (default)' do
|
61
|
+
distribution = Sapor::CombinationsDistribution.new
|
62
|
+
distribution[5] = 2
|
63
|
+
expect(distribution.confidence_interval(0.95)).to eq([5, 5])
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'returns the full interval as boundaries if single value (extensive)' do
|
67
|
+
distribution = Sapor::CombinationsDistribution.new
|
68
|
+
distribution[5] = 2
|
69
|
+
expect(distribution.confidence_interval(0.95, 10)).to eq([0, 10])
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'returns the boundaries (default)' do
|
73
|
+
distribution = Sapor::CombinationsDistribution.new
|
74
|
+
distribution[3] = 2
|
75
|
+
distribution[7] = 2
|
76
|
+
expect(distribution.confidence_interval(0.95)).to eq([3, 7])
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'returns the full interval as boundaries (extensive)' do
|
80
|
+
distribution = Sapor::CombinationsDistribution.new
|
81
|
+
distribution[3] = 2
|
82
|
+
distribution[7] = 2
|
83
|
+
expect(distribution.confidence_interval(0.95, 10)).to eq([0, 10])
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'returns the full interval if level too high' do
|
87
|
+
distribution = Sapor::CombinationsDistribution.new
|
88
|
+
distribution[5] = 1
|
89
|
+
distribution[10] = 98
|
90
|
+
distribution[15] = 1
|
91
|
+
expect(distribution.confidence_interval(0.99)).to eq([5, 15])
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'returns the values in between as boundaries (even)' do
|
95
|
+
distribution = Sapor::CombinationsDistribution.new
|
96
|
+
distribution[5] = 1
|
97
|
+
distribution[10] = 98
|
98
|
+
distribution[15] = 1
|
99
|
+
expect(distribution.confidence_interval(0.95)).to eq([8, 12])
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'returns the values in between as boundaries (odd)' do
|
103
|
+
distribution = Sapor::CombinationsDistribution.new
|
104
|
+
distribution[6] = 1.to_lf
|
105
|
+
distribution[10] = 98.to_lf
|
106
|
+
distribution[14] = 1.to_lf
|
107
|
+
expect(distribution.confidence_interval(0.95)).to eq([8, 12])
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe Sapor::CombinationsDistribution, '#confidence_interval_values' do
|
112
|
+
it 'extracts the single value as confidence interval value' do
|
113
|
+
distribution = Sapor::CombinationsDistribution.new
|
114
|
+
distribution[5] = 2.to_lf
|
115
|
+
expect(distribution.confidence_interval_values(0.95)).to eq([5])
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'extracts all values as confidence intervalue values' do
|
119
|
+
distribution = Sapor::CombinationsDistribution.new
|
120
|
+
distribution[3] = 2.to_lf
|
121
|
+
distribution[7] = 2.to_lf
|
122
|
+
expect(distribution.confidence_interval_values(0.95).sort).to eq([3, 7])
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'extracts the middle values as confidence interval values' do
|
126
|
+
distribution = Sapor::CombinationsDistribution.new
|
127
|
+
distribution[5] = 1.to_lf
|
128
|
+
distribution[10] = 98.to_lf
|
129
|
+
distribution[15] = 1.to_lf
|
130
|
+
expect(distribution.confidence_interval_values(0.95)).to eq([10])
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe Sapor::CombinationsDistribution, '#empty?' do
|
135
|
+
it 'returns true if no value-combinations pair has been added' do
|
136
|
+
distribution = Sapor::CombinationsDistribution.new
|
137
|
+
expect(distribution.empty?).to be true
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'returns false if a value-combinations pair has been added' do
|
141
|
+
distribution = Sapor::CombinationsDistribution.new
|
142
|
+
distribution[1] = 2.to_lf
|
143
|
+
expect(distribution.empty?).to be false
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe Sapor::CombinationsDistribution, '#most_probable_rounded_fraction' do
|
148
|
+
it "returns the median rounded fraction if there's more than one with" \
|
149
|
+
' maximal probability' do
|
150
|
+
distribution = Sapor::CombinationsDistribution.new
|
151
|
+
distribution[500] = 2.to_lf
|
152
|
+
expect(distribution.most_probable_rounded_fraction(1000)).to eq(0.5)
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'returns the rounded fraction with the maximal probability' do
|
156
|
+
distribution = Sapor::CombinationsDistribution.new
|
157
|
+
distribution[199] = 2.to_lf
|
158
|
+
distribution[200] = 3.to_lf
|
159
|
+
distribution[201] = 2.to_lf
|
160
|
+
expect(distribution.most_probable_rounded_fraction(1000)).to eq(0.2)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe Sapor::CombinationsDistribution, '#most_probable_value' do
|
165
|
+
it "returns the single value if there's only one value" do
|
166
|
+
distribution = Sapor::CombinationsDistribution.new
|
167
|
+
distribution[1] = 2
|
168
|
+
expect(distribution.most_probable_value).to eq(1)
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'returns the first of two values if that has the greatest number ' \
|
172
|
+
'of combinations' do
|
173
|
+
distribution = Sapor::CombinationsDistribution.new
|
174
|
+
distribution[1] = 2
|
175
|
+
distribution[2] = 1
|
176
|
+
expect(distribution.most_probable_value).to eq(1)
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'returns the second of two values if that has the greatest number ' \
|
180
|
+
'of combinations' do
|
181
|
+
distribution = Sapor::CombinationsDistribution.new
|
182
|
+
distribution[1] = 1
|
183
|
+
distribution[2] = 2
|
184
|
+
expect(distribution.most_probable_value).to eq(2)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
describe Sapor::CombinationsDistribution, '#size' do
|
189
|
+
it 'returns 0 for an empty distribution' do
|
190
|
+
distribution = Sapor::CombinationsDistribution.new
|
191
|
+
expect(distribution.size).to eq(0)
|
192
|
+
end
|
193
|
+
|
194
|
+
it 'returns 1 when a value-combinations pair has been added' do
|
195
|
+
distribution = Sapor::CombinationsDistribution.new
|
196
|
+
distribution[1] = 2
|
197
|
+
expect(distribution.size).to eq(1)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
describe Sapor::CombinationsDistribution, '#threshold_probability' do
|
202
|
+
it 'returns 1 if the single value is above the threshold' do
|
203
|
+
distribution = Sapor::CombinationsDistribution.new
|
204
|
+
distribution[5] = 2.to_lf
|
205
|
+
expect(distribution.threshold_probability(0.1, 10)).to eq(1)
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'returns 0 if the single value is below the threshold' do
|
209
|
+
distribution = Sapor::CombinationsDistribution.new
|
210
|
+
distribution[5] = 2.to_lf
|
211
|
+
expect(distribution.threshold_probability(0.6, 10)).to eq(0)
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'returns 0.8 if the threshold is between combinations 2 and 8' do
|
215
|
+
distribution = Sapor::CombinationsDistribution.new
|
216
|
+
distribution[2] = 2.to_lf
|
217
|
+
distribution[7] = 8.to_lf
|
218
|
+
expect(distribution.threshold_probability(0.5, 10)).to eq(0.8)
|
219
|
+
end
|
220
|
+
|
221
|
+
it 'includes the probability at the threshold' do
|
222
|
+
distribution = Sapor::CombinationsDistribution.new
|
223
|
+
distribution[2] = 2.to_lf
|
224
|
+
distribution[5] = 5.to_lf
|
225
|
+
distribution[7] = 3.to_lf
|
226
|
+
expect(distribution.threshold_probability(0.5, 10)).to eq(0.8)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
describe Sapor::CombinationsDistribution, '#values' do
|
231
|
+
it 'returns an empty array for an empty distribution' do
|
232
|
+
distribution = Sapor::CombinationsDistribution.new
|
233
|
+
expect(distribution.values).to eq([])
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'returns 1 when a value-combinations pair has been added' do
|
237
|
+
distribution = Sapor::CombinationsDistribution.new
|
238
|
+
distribution[1] = 2
|
239
|
+
expect(distribution.values).to eq([1])
|
240
|
+
end
|
241
|
+
end
|
@@ -0,0 +1,34 @@
|
|
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::DhondtDenominators, '#get' do
|
23
|
+
it 'returns [1, 2, 3, 4, 5] as the five first elements' do
|
24
|
+
denominators = Sapor::DhondtDenominators.get(5).each.to_a
|
25
|
+
expect(denominators).to eq([1, 2, 3, 4, 5])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe Sapor::SainteLague14Denominators, '#get' do
|
30
|
+
it 'returns [1.4, 3, 5, 7, 9] as the five first elements' do
|
31
|
+
denominators = Sapor::SainteLague14Denominators.get(5).each.to_a
|
32
|
+
expect(denominators).to eq([1.4, 3, 5, 7, 9])
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,154 @@
|
|
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_RESULTS = { 'Red' => 1, 'Green' => 2, 'Blue' => 3, 'Other' => 1 }
|
23
|
+
SAMPLE_POPULATION_SIZE = 1_000_000
|
24
|
+
|
25
|
+
describe Sapor::Dichotomies, '#confidence_interval_values' do
|
26
|
+
it 'returns the confidence interval values' do
|
27
|
+
dichotomies = Sapor::Dichotomies.new(SAMPLE_RESULTS,
|
28
|
+
SAMPLE_POPULATION_SIZE)
|
29
|
+
expect(dichotomies.confidence_interval_values('Red', 0.95)).to \
|
30
|
+
eq([500_000])
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'returns the non-default confidence interval values' do
|
34
|
+
dichotomies = Sapor::Dichotomies.new(SAMPLE_RESULTS,
|
35
|
+
SAMPLE_POPULATION_SIZE)
|
36
|
+
dichotomies.refine
|
37
|
+
expect(dichotomies.confidence_interval_values('Red', 0.9999).sort).to \
|
38
|
+
eq([166_667, 500_000, 833_333])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe Sapor::Dichotomies, '#error_estimate' do
|
43
|
+
it 'returns the largest error estimate of the underlying Dichotomy ' \
|
44
|
+
'objects' do
|
45
|
+
dichotomies = Sapor::Dichotomies.new(SAMPLE_RESULTS,
|
46
|
+
SAMPLE_POPULATION_SIZE)
|
47
|
+
expect(dichotomies.error_estimate).to eq(1)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe Sapor::Dichotomies, '#progress_report' do
|
52
|
+
it 'reports progress as 1 data point by default' do
|
53
|
+
dichotomies = Sapor::Dichotomies.new(SAMPLE_RESULTS,
|
54
|
+
SAMPLE_POPULATION_SIZE)
|
55
|
+
expect(dichotomies.progress_report).to eq('Number of data points: 1.')
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'reports progress as 3 data points after the first refinement' do
|
59
|
+
dichotomies = Sapor::Dichotomies.new(SAMPLE_RESULTS,
|
60
|
+
SAMPLE_POPULATION_SIZE)
|
61
|
+
dichotomies.refine
|
62
|
+
expect(dichotomies.progress_report).to eq('Number of data points: 3.')
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe Sapor::Dichotomies, '#report' do
|
67
|
+
it 'produces a report by default for short choice labels' do
|
68
|
+
dichotomies = Sapor::Dichotomies.new(SAMPLE_RESULTS,
|
69
|
+
SAMPLE_POPULATION_SIZE)
|
70
|
+
expected_report = 'Most probable fractions and 95% confidence ' \
|
71
|
+
"intervals:\n" \
|
72
|
+
"Choice MPF CI(95%)\n" \
|
73
|
+
"Blue 50.0% 0.0%–100.0%\n" \
|
74
|
+
"Green 50.0% 0.0%–100.0%\n" \
|
75
|
+
"Red 50.0% 0.0%–100.0%\n" \
|
76
|
+
'Other 50.0% 0.0%–100.0%'
|
77
|
+
expect(dichotomies.report).to eq(expected_report)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'produces a report by default for long choice labels' do
|
81
|
+
dichotomies = Sapor::Dichotomies.new({ 'Dark Red' => 1,
|
82
|
+
'Light Green' => 2,
|
83
|
+
'Medium Blue' => 3, 'Other' => 1 },
|
84
|
+
SAMPLE_POPULATION_SIZE)
|
85
|
+
expected_report = 'Most probable fractions and 95% confidence ' \
|
86
|
+
"intervals:\n" \
|
87
|
+
"Choice MPF CI(95%)\n" \
|
88
|
+
"Dark Red 50.0% 0.0%–100.0%\n" \
|
89
|
+
"Light Green 50.0% 0.0%–100.0%\n" \
|
90
|
+
"Medium Blue 50.0% 0.0%–100.0%\n" \
|
91
|
+
'Other 50.0% 0.0%–100.0%'
|
92
|
+
expect(dichotomies.report).to eq(expected_report)
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'produces a report after the first refinement' do
|
96
|
+
dichotomies = Sapor::Dichotomies.new(SAMPLE_RESULTS,
|
97
|
+
SAMPLE_POPULATION_SIZE)
|
98
|
+
dichotomies.refine
|
99
|
+
expected_report = 'Most probable fractions and 95% confidence ' \
|
100
|
+
"intervals:\n" \
|
101
|
+
"Choice MPF CI(95%)\n" \
|
102
|
+
"Blue 50.0% 0.0%–100.0%\n" \
|
103
|
+
"Green 16.7% 0.0%– 66.7%\n" \
|
104
|
+
"Red 16.7% 0.0%– 66.7%\n" \
|
105
|
+
'Other 16.7% 0.0%– 66.7%'
|
106
|
+
expect(dichotomies.report).to eq(expected_report)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'sorts the choices according to MPV' do
|
110
|
+
dichotomies = Sapor::Dichotomies.new({ 'Red' => 3, 'Green' => 2,
|
111
|
+
'Blue' => 1, 'Other' => 1 },
|
112
|
+
SAMPLE_POPULATION_SIZE)
|
113
|
+
dichotomies.refine
|
114
|
+
expected_report = 'Most probable fractions and 95% confidence ' \
|
115
|
+
"intervals:\n" \
|
116
|
+
"Choice MPF CI(95%)\n" \
|
117
|
+
"Red 50.0% 0.0%–100.0%\n" \
|
118
|
+
"Blue 16.7% 0.0%– 66.7%\n" \
|
119
|
+
"Green 16.7% 0.0%– 66.7%\n" \
|
120
|
+
'Other 16.7% 0.0%– 66.7%'
|
121
|
+
expect(dichotomies.report).to eq(expected_report)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "puts Other on the last line, even if it's listed first and largest" do
|
125
|
+
dichotomies = Sapor::Dichotomies.new({ 'Other' => 4, 'Red' => 1,
|
126
|
+
'Green' => 2, 'Blue' => 3 },
|
127
|
+
SAMPLE_POPULATION_SIZE)
|
128
|
+
dichotomies.refine
|
129
|
+
expected_report = 'Most probable fractions and 95% confidence ' \
|
130
|
+
"intervals:\n" \
|
131
|
+
"Choice MPF CI(95%)\n" \
|
132
|
+
"Blue 16.7% 0.0%– 66.7%\n" \
|
133
|
+
"Green 16.7% 0.0%– 66.7%\n" \
|
134
|
+
"Red 16.7% 0.0%– 66.7%\n" \
|
135
|
+
'Other 50.0% 0.0%– 66.7%'
|
136
|
+
expect(dichotomies.report).to eq(expected_report)
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'includes threshold probabilities when a threshold provided' do
|
140
|
+
dichotomies = Sapor::Dichotomies.new(SAMPLE_RESULTS,
|
141
|
+
SAMPLE_POPULATION_SIZE,
|
142
|
+
0.15)
|
143
|
+
dichotomies.refine
|
144
|
+
dichotomies.refine
|
145
|
+
expected_report = 'Most probable fractions and 95% confidence ' \
|
146
|
+
"intervals:\n" \
|
147
|
+
"Choice MPF CI(95%) P(≥15%)\n" \
|
148
|
+
"Blue 38.9% 11.1%– 77.8% 99.6%\n" \
|
149
|
+
"Green 27.8% 0.0%– 66.7% 95.7%\n" \
|
150
|
+
"Red 16.7% 0.0%– 55.6% 76.1%\n" \
|
151
|
+
'Other 16.7% 0.0%– 55.6% 76.1%'
|
152
|
+
expect(dichotomies.report).to eq(expected_report)
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,320 @@
|
|
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
|
+
def dichotomy_of_eight
|
23
|
+
Sapor::Dichotomy.new(2, 5, 8)
|
24
|
+
end
|
25
|
+
|
26
|
+
def dichotomy_of_nine
|
27
|
+
Sapor::Dichotomy.new(2, 5, 9)
|
28
|
+
end
|
29
|
+
|
30
|
+
def dichotomy_of_thousand
|
31
|
+
Sapor::Dichotomy.new(2, 5, 1000)
|
32
|
+
end
|
33
|
+
|
34
|
+
def dichotomy_of_nine_hundred_ninety_nine
|
35
|
+
Sapor::Dichotomy.new(2, 5, 999)
|
36
|
+
end
|
37
|
+
|
38
|
+
describe Sapor::Dichotomy, '#new' do
|
39
|
+
it 'creates an array with a value at 50% of the population size' do
|
40
|
+
expect(dichotomy_of_eight.values).to eq([4])
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'creates an array with a value at 50% of the population size' \
|
44
|
+
' (rounding)' do
|
45
|
+
expect(dichotomy_of_nine.values).to eq([4])
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'creates an array with a combination for 50% of the population size' do
|
49
|
+
expect(dichotomy_of_eight.combinations(4)).to eq(24.to_lf)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe Sapor::Dichotomy, '#confidence_interval' do
|
54
|
+
it 'returns [0%, 100%] after new for default level 95%' do
|
55
|
+
expect(dichotomy_of_eight.confidence_interval).to eq([0.0, 1.0])
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'returns the default 95% confidence interval after one refinement' do
|
59
|
+
dichotomy = Sapor::Dichotomy.new(20, 50, 80)
|
60
|
+
dichotomy.refine
|
61
|
+
expect(dichotomy.confidence_interval).to eq([0.3375, 0.6625])
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'returns the default 95% confidence interval after three refinements' do
|
65
|
+
dichotomy = dichotomy_of_thousand
|
66
|
+
dichotomy.refine
|
67
|
+
dichotomy.refine
|
68
|
+
dichotomy.refine
|
69
|
+
expect(dichotomy.confidence_interval).to eq([0.112, 0.777])
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'returns the 80% confidence interval after three refinements' do
|
73
|
+
dichotomy = dichotomy_of_thousand
|
74
|
+
dichotomy.refine
|
75
|
+
dichotomy.refine
|
76
|
+
dichotomy.refine
|
77
|
+
expect(dichotomy.confidence_interval(0.8)).to eq([0.186, 0.666])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe Sapor::Dichotomy, '#error_estimate' do
|
82
|
+
it 'is 0 when population size is reached' do
|
83
|
+
dichotomy = dichotomy_of_eight
|
84
|
+
dichotomy.refine
|
85
|
+
dichotomy.refine
|
86
|
+
dichotomy.refine
|
87
|
+
expect(dichotomy.error_estimate).to eq(0)
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'is at least the resolution (no refinement)' do
|
91
|
+
dichotomy = dichotomy_of_eight
|
92
|
+
expect(dichotomy.error_estimate).to eq(1.0)
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'is at least the resolution (one refinement)' do
|
96
|
+
dichotomy = dichotomy_of_eight
|
97
|
+
dichotomy.refine
|
98
|
+
expect(dichotomy.error_estimate).to eq(1.to_f / 3)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe Sapor::Dichotomy, '#most_probable_fraction' do
|
103
|
+
it 'returns the fraction of the single value after new' do
|
104
|
+
expect(dichotomy_of_eight.most_probable_fraction).to eq(0.5)
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'returns the one probable value after one refinement' do
|
108
|
+
dichotomy = dichotomy_of_eight
|
109
|
+
dichotomy.refine
|
110
|
+
expect(dichotomy.most_probable_fraction).to eq(0.5)
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'returns the new most probable value after two refinements' do
|
114
|
+
dichotomy = dichotomy_of_eight
|
115
|
+
dichotomy.refine
|
116
|
+
dichotomy.refine
|
117
|
+
expect(dichotomy.most_probable_fraction).to eq(0.375)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe Sapor::Dichotomy, '#most_probable_value' do
|
122
|
+
it 'returns the single value after new' do
|
123
|
+
expect(dichotomy_of_eight.most_probable_value).to eq(4)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'returns the one probable value after one refinement' do
|
127
|
+
dichotomy = dichotomy_of_eight
|
128
|
+
dichotomy.refine
|
129
|
+
expect(dichotomy.most_probable_value).to eq(4)
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'returns the new most probable value after two refinements' do
|
133
|
+
dichotomy = dichotomy_of_eight
|
134
|
+
dichotomy.refine
|
135
|
+
dichotomy.refine
|
136
|
+
expect(dichotomy.most_probable_value).to eq(3)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe Sapor::Dichotomy, '#refine' do
|
141
|
+
it 'adds values after one refinement (power of three)' do
|
142
|
+
dichotomy = dichotomy_of_eight
|
143
|
+
dichotomy.refine
|
144
|
+
expect(dichotomy.values.sort).to eq([1, 4, 7])
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'adds values after one refinement (small population)' do
|
148
|
+
dichotomy = dichotomy_of_nine
|
149
|
+
dichotomy.refine
|
150
|
+
expect(dichotomy.values.sort).to eq([1, 4, 7])
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'adds values after one refinement (large population)' do
|
154
|
+
dichotomy = dichotomy_of_thousand
|
155
|
+
dichotomy.refine
|
156
|
+
expect(dichotomy.values.sort).to eq([167, 500, 833])
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'adds values after two refinements (power of three)' do
|
160
|
+
dichotomy = dichotomy_of_eight
|
161
|
+
dichotomy.refine
|
162
|
+
dichotomy.refine
|
163
|
+
expect(dichotomy.values.sort).to eq([0, 1, 2, 3, 4, 5, 6, 7, 8])
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'adds values after two refinements (small population)' do
|
167
|
+
dichotomy = dichotomy_of_nine
|
168
|
+
dichotomy.refine
|
169
|
+
dichotomy.refine
|
170
|
+
expect(dichotomy.values.sort).to eq([0, 1, 2, 3, 4, 5, 6, 7, 8])
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'adds values after two refinements (small population)' do
|
174
|
+
dichotomy = Sapor::Dichotomy.new(2, 5, 60)
|
175
|
+
dichotomy.refine
|
176
|
+
dichotomy.refine
|
177
|
+
expect(dichotomy.values.sort).to eq([3, 10, 17, 23, 30, 37, 43, 50, 57])
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'adds values after two refinements (large population)' do
|
181
|
+
dichotomy = dichotomy_of_thousand
|
182
|
+
dichotomy.refine
|
183
|
+
dichotomy.refine
|
184
|
+
expect(dichotomy.values.sort).to eq([56, 167, 278, 389, 500, 611, 722, 833,
|
185
|
+
944])
|
186
|
+
end
|
187
|
+
|
188
|
+
it "doesn't add values when population size is reached" do
|
189
|
+
dichotomy = dichotomy_of_eight
|
190
|
+
dichotomy.refine
|
191
|
+
dichotomy.refine
|
192
|
+
dichotomy.refine
|
193
|
+
expect(dichotomy.values.sort).to eq([0, 1, 2, 3, 4, 5, 6, 7, 8])
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'fills up the values when population size is reached' do
|
197
|
+
dichotomy = dichotomy_of_nine
|
198
|
+
dichotomy.refine
|
199
|
+
dichotomy.refine
|
200
|
+
dichotomy.refine
|
201
|
+
expect(dichotomy.values.sort).to eq([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'fills up the values when population size is reached' do
|
205
|
+
dichotomy = Sapor::Dichotomy.new(2, 5, 11)
|
206
|
+
dichotomy.refine
|
207
|
+
dichotomy.refine
|
208
|
+
dichotomy.refine
|
209
|
+
expect(dichotomy.values.sort).to eq([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'adds combinations after a refinement' do
|
213
|
+
dichotomy = dichotomy_of_thousand
|
214
|
+
dichotomy.refine
|
215
|
+
expect(dichotomy.combinations(167)).to eq(1_330_493_216_416.to_lf)
|
216
|
+
expect(dichotomy.combinations(833)).to eq(264_177_353_440.to_lf)
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'adds combinations of zero for impossible values' do
|
220
|
+
dichotomy = dichotomy_of_eight
|
221
|
+
dichotomy.refine
|
222
|
+
expect(dichotomy.combinations(1)).to eq(0.to_lf)
|
223
|
+
expect(dichotomy.combinations(7)).to eq(0.to_lf)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
describe Sapor::Dichotomy, '#threshold_probability' do
|
228
|
+
it 'returns 100% after new for a 50% threshold' do
|
229
|
+
expect(dichotomy_of_thousand.threshold_probability(0.5)).to eq(1)
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'returns 0% after new for a 51% threshold' do
|
233
|
+
expect(dichotomy_of_thousand.threshold_probability(0.51)).to eq(0)
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'returns 100% for a 5% threshold after one refinement' do
|
237
|
+
dichotomy = dichotomy_of_thousand
|
238
|
+
dichotomy.refine
|
239
|
+
expect(dichotomy.threshold_probability(0.05)).to eq(1)
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'returns 68% (after rounding) for a 50% threshold after one refinement' do
|
243
|
+
dichotomy = dichotomy_of_thousand
|
244
|
+
dichotomy.refine
|
245
|
+
expect(dichotomy.threshold_probability(0.5).round(2)).to eq(0.68)
|
246
|
+
end
|
247
|
+
|
248
|
+
it 'returns 0% for a 95% threshold after one refinement' do
|
249
|
+
dichotomy = dichotomy_of_thousand
|
250
|
+
dichotomy.refine
|
251
|
+
expect(dichotomy.threshold_probability(0.95)).to eq(0)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
describe Sapor::Dichotomy, '#value_confidence_interval' do
|
256
|
+
it 'returns [0, population_size] values after new for default level 95%' do
|
257
|
+
expect(dichotomy_of_eight.value_confidence_interval).to eq([0, 8])
|
258
|
+
end
|
259
|
+
|
260
|
+
it 'returns [0, population_size] values after new for level 80%' do
|
261
|
+
expect(dichotomy_of_eight.value_confidence_interval(0.8)).to eq([0, 8])
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'returns the value confidence interval after one refinement' do
|
265
|
+
dichotomy = Sapor::Dichotomy.new(20, 50, 80)
|
266
|
+
dichotomy.refine
|
267
|
+
expect(dichotomy.value_confidence_interval).to eq([27, 53])
|
268
|
+
end
|
269
|
+
|
270
|
+
it 'returns the value confidence interval after full refinement' do
|
271
|
+
dichotomy = dichotomy_of_eight
|
272
|
+
dichotomy.refine
|
273
|
+
dichotomy.refine
|
274
|
+
expect(dichotomy.value_confidence_interval).to eq([2, 5])
|
275
|
+
end
|
276
|
+
|
277
|
+
it 'returns the 80% value confidence interval after full refinement' do
|
278
|
+
dichotomy = dichotomy_of_eight
|
279
|
+
dichotomy.refine
|
280
|
+
dichotomy.refine
|
281
|
+
expect(dichotomy.value_confidence_interval(0.8)).to eq([2, 5])
|
282
|
+
end
|
283
|
+
|
284
|
+
it 'returns the value confidence interval after two refinements' do
|
285
|
+
dichotomy = dichotomy_of_thousand
|
286
|
+
dichotomy.refine
|
287
|
+
dichotomy.refine
|
288
|
+
expect(dichotomy.value_confidence_interval).to eq([112, 777])
|
289
|
+
end
|
290
|
+
|
291
|
+
it 'returns the 80% value confidence interval after two refinements' do
|
292
|
+
dichotomy = dichotomy_of_thousand
|
293
|
+
dichotomy.refine
|
294
|
+
dichotomy.refine
|
295
|
+
expect(dichotomy.value_confidence_interval(0.8)).to eq([112, 666])
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'returns the value confidence interval after three refinements' do
|
299
|
+
dichotomy = dichotomy_of_thousand
|
300
|
+
dichotomy.refine
|
301
|
+
dichotomy.refine
|
302
|
+
dichotomy.refine
|
303
|
+
expect(dichotomy.value_confidence_interval).to eq([112, 777])
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'can return a confidence interval starting at 0 after refinements' do
|
307
|
+
dichotomy = Sapor::Dichotomy.new(0, 5, 1000)
|
308
|
+
dichotomy.refine
|
309
|
+
dichotomy.refine
|
310
|
+
expect(dichotomy.value_confidence_interval).to eq([0, 555])
|
311
|
+
end
|
312
|
+
|
313
|
+
it 'can return a confidence interval ending at population size after ' \
|
314
|
+
'refinements' do
|
315
|
+
dichotomy = Sapor::Dichotomy.new(5, 5, 1000)
|
316
|
+
dichotomy.refine
|
317
|
+
dichotomy.refine
|
318
|
+
expect(dichotomy.value_confidence_interval).to eq([445, 1000])
|
319
|
+
end
|
320
|
+
end
|