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