forked from AllenDowney/ThinkBayes
-
Notifications
You must be signed in to change notification settings - Fork 0
/
sat.py
465 lines (339 loc) · 12 KB
/
sat.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
"""This file contains code used in "Think Bayes",
by Allen B. Downey, available from greenteapress.com
Copyright 2012 Allen B. Downey
License: GNU GPLv3 http://www.gnu.org/licenses/gpl.html
"""
import csv
import math
import numpy
import sys
import matplotlib
import matplotlib.pyplot as pyplot
import thinkbayes
import thinkplot
def ReadScale(filename='sat_scale.csv', col=2):
"""Reads a CSV file of SAT scales (maps from raw score to standard score).
Args:
filename: string filename
col: which column to start with (0=Reading, 2=Math, 4=Writing)
Returns: thinkbayes.Interpolator object
"""
def ParseRange(s):
"""Parse a range of values in the form 123-456
s: string
"""
t = [int(x) for x in s.split('-')]
return 1.0 * sum(t) / len(t)
fp = open(filename)
reader = csv.reader(fp)
raws = []
scores = []
for t in reader:
try:
raw = int(t[col])
raws.append(raw)
score = ParseRange(t[col+1])
scores.append(score)
except ValueError:
pass
raws.sort()
scores.sort()
return thinkbayes.Interpolator(raws, scores)
def ReadRanks(filename='sat_ranks.csv'):
"""Reads a CSV file of SAT scores.
Args:
filename: string filename
Returns:
list of (score, freq) pairs
"""
fp = open(filename)
reader = csv.reader(fp)
res = []
for t in reader:
try:
score = int(t[0])
freq = int(t[1])
res.append((score, freq))
except ValueError:
pass
return res
def DivideValues(pmf, denom):
"""Divides the values in a Pmf by denom.
Returns a new Pmf.
"""
new = thinkbayes.Pmf()
denom = float(denom)
for val, prob in pmf.Items():
x = val / denom
new.Set(x, prob)
return new
class Exam(object):
"""Encapsulates information about an exam.
Contains the distribution of scaled scores and an
Interpolator that maps between scaled and raw scores.
"""
def __init__(self):
self.scale = ReadScale()
scores = ReadRanks()
score_pmf = thinkbayes.MakePmfFromDict(dict(scores))
self.raw = self.ReverseScale(score_pmf)
self.max_score = max(self.raw.Values())
self.prior = DivideValues(self.raw, denom=self.max_score)
center = -0.05
width = 1.8
self.difficulties = MakeDifficulties(center, width, self.max_score)
def CompareScores(self, a_score, b_score, constructor):
"""Computes posteriors for two test scores and the likelihood ratio.
a_score, b_score: scales SAT scores
constructor: function that instantiates an Sat or Sat2 object
"""
a_sat = constructor(self, a_score)
b_sat = constructor(self, b_score)
a_sat.PlotPosteriors(b_sat)
if constructor is Sat:
PlotJointDist(a_sat, b_sat)
top = TopLevel('AB')
top.Update((a_sat, b_sat))
top.Print()
ratio = top.Prob('A') / top.Prob('B')
print 'Likelihood ratio', ratio
posterior = ratio / (ratio + 1)
print 'Posterior', posterior
if constructor is Sat2:
ComparePosteriorPredictive(a_sat, b_sat)
def MakeRawScoreDist(self, efficacies):
"""Makes the distribution of raw scores for given difficulty.
efficacies: Pmf of efficacy
"""
pmfs = thinkbayes.Pmf()
for efficacy, prob in efficacies.Items():
scores = self.PmfCorrect(efficacy)
pmfs.Set(scores, prob)
mix = thinkbayes.MakeMixture(pmfs)
return mix
def CalibrateDifficulty(self):
"""Make a plot showing the model distribution of raw scores."""
thinkplot.Clf()
thinkplot.PrePlot(num=2)
cdf = thinkbayes.MakeCdfFromPmf(self.raw, name='data')
thinkplot.Cdf(cdf)
efficacies = thinkbayes.MakeGaussianPmf(0, 1.5, 3)
pmf = self.MakeRawScoreDist(efficacies)
cdf = thinkbayes.MakeCdfFromPmf(pmf, name='model')
thinkplot.Cdf(cdf)
thinkplot.Save(root='sat_calibrate',
xlabel='raw score',
ylabel='CDF',
formats=['pdf', 'eps'])
def PmfCorrect(self, efficacy):
"""Returns the PMF of number of correct responses.
efficacy: float
"""
pmf = PmfCorrect(efficacy, self.difficulties)
return pmf
def Lookup(self, raw):
"""Looks up a raw score and returns a scaled score."""
return self.scale.Lookup(raw)
def Reverse(self, score):
"""Looks up a scaled score and returns a raw score.
Since we ignore the penalty, negative scores round up to zero.
"""
raw = self.scale.Reverse(score)
return raw if raw > 0 else 0
def ReverseScale(self, pmf):
"""Applies the reverse scale to the values of a PMF.
Args:
pmf: Pmf object
scale: Interpolator object
Returns:
new Pmf
"""
new = thinkbayes.Pmf()
for val, prob in pmf.Items():
raw = self.Reverse(val)
new.Incr(raw, prob)
return new
class Sat(thinkbayes.Suite):
"""Represents the distribution of p_correct for a test-taker."""
def __init__(self, exam, score):
self.exam = exam
self.score = score
# start with the prior distribution
thinkbayes.Suite.__init__(self, exam.prior)
# update based on an exam score
self.Update(score)
def Likelihood(self, data, hypo):
"""Computes the likelihood of a test score, given efficacy."""
p_correct = hypo
score = data
k = self.exam.Reverse(score)
n = self.exam.max_score
like = thinkbayes.EvalBinomialPmf(k, n, p_correct)
return like
def PlotPosteriors(self, other):
"""Plots posterior distributions of efficacy.
self, other: Sat objects.
"""
thinkplot.Clf()
thinkplot.PrePlot(num=2)
cdf1 = thinkbayes.MakeCdfFromPmf(self, 'posterior %d' % self.score)
cdf2 = thinkbayes.MakeCdfFromPmf(other, 'posterior %d' % other.score)
thinkplot.Cdfs([cdf1, cdf2])
thinkplot.Save(xlabel='p_correct',
ylabel='CDF',
axis=[0.7, 1.0, 0.0, 1.0],
root='sat_posteriors_p_corr',
formats=['pdf', 'eps'])
class Sat2(thinkbayes.Suite):
"""Represents the distribution of efficacy for a test-taker."""
def __init__(self, exam, score):
self.exam = exam
self.score = score
# start with the Gaussian prior
efficacies = thinkbayes.MakeGaussianPmf(0, 1.5, 3)
thinkbayes.Suite.__init__(self, efficacies)
# update based on an exam score
self.Update(score)
def Likelihood(self, data, hypo):
"""Computes the likelihood of a test score, given efficacy."""
efficacy = hypo
score = data
raw = self.exam.Reverse(score)
pmf = self.exam.PmfCorrect(efficacy)
like = pmf.Prob(raw)
return like
def MakePredictiveDist(self):
"""Returns the distribution of raw scores expected on a re-test."""
raw_pmf = self.exam.MakeRawScoreDist(self)
return raw_pmf
def PlotPosteriors(self, other):
"""Plots posterior distributions of efficacy.
self, other: Sat objects.
"""
thinkplot.Clf()
thinkplot.PrePlot(num=2)
cdf1 = thinkbayes.MakeCdfFromPmf(self, 'posterior %d' % self.score)
cdf2 = thinkbayes.MakeCdfFromPmf(other, 'posterior %d' % other.score)
thinkplot.Cdfs([cdf1, cdf2])
thinkplot.Save(xlabel='efficacy',
ylabel='CDF',
axis=[0, 4.6, 0.0, 1.0],
root='sat_posteriors_eff',
formats=['pdf', 'eps'])
def PlotJointDist(pmf1, pmf2, thresh=0.8):
"""Plot the joint distribution of p_correct.
pmf1, pmf2: posterior distributions
thresh: lower bound of the range to be plotted
"""
def Clean(pmf):
"""Removes values below thresh."""
vals = [val for val in pmf.Values() if val < thresh]
[pmf.Remove(val) for val in vals]
Clean(pmf1)
Clean(pmf2)
pmf = thinkbayes.MakeJoint(pmf1, pmf2)
thinkplot.Figure(figsize=(6, 6))
thinkplot.Contour(pmf, contour=False, pcolor=True)
thinkplot.Plot([thresh, 1.0], [thresh, 1.0],
color='gray', alpha=0.2, linewidth=4)
thinkplot.Save(root='sat_joint',
xlabel='p_correct Alice',
ylabel='p_correct Bob',
axis=[thresh, 1.0, thresh, 1.0],
formats=['pdf', 'eps'])
def ComparePosteriorPredictive(a_sat, b_sat):
"""Compares the predictive distributions of raw scores.
a_sat: posterior distribution
b_sat:
"""
a_pred = a_sat.MakePredictiveDist()
b_pred = b_sat.MakePredictiveDist()
#thinkplot.Clf()
#thinkplot.Pmfs([a_pred, b_pred])
#thinkplot.Show()
a_like = thinkbayes.PmfProbGreater(a_pred, b_pred)
b_like = thinkbayes.PmfProbLess(a_pred, b_pred)
c_like = thinkbayes.PmfProbEqual(a_pred, b_pred)
print 'Posterior predictive'
print 'A', a_like
print 'B', b_like
print 'C', c_like
def PlotPriorDist(pmf):
"""Plot the prior distribution of p_correct.
pmf: prior
"""
thinkplot.Clf()
thinkplot.PrePlot(num=1)
cdf1 = thinkbayes.MakeCdfFromPmf(pmf, 'prior')
thinkplot.Cdf(cdf1)
thinkplot.Save(root='sat_prior',
xlabel='p_correct',
ylabel='CDF',
formats=['pdf', 'eps'])
class TopLevel(thinkbayes.Suite):
"""Evaluates the top-level hypotheses about Alice and Bob.
Uses the bottom-level posterior distribution about p_correct
(or efficacy).
"""
def Update(self, data):
a_sat, b_sat = data
a_like = thinkbayes.PmfProbGreater(a_sat, b_sat)
b_like = thinkbayes.PmfProbLess(a_sat, b_sat)
c_like = thinkbayes.PmfProbEqual(a_sat, b_sat)
a_like += c_like / 2
b_like += c_like / 2
self.Mult('A', a_like)
self.Mult('B', b_like)
self.Normalize()
def ProbCorrect(efficacy, difficulty, a=1):
"""Returns the probability that a person gets a question right.
efficacy: personal ability to answer questions
difficulty: how hard the question is
Returns: float prob
"""
return 1 / (1 + math.exp(-a * (efficacy - difficulty)))
def BinaryPmf(p):
"""Makes a Pmf with values 1 and 0.
p: probability given to 1
Returns: Pmf object
"""
pmf = thinkbayes.Pmf()
pmf.Set(1, p)
pmf.Set(0, 1-p)
return pmf
def PmfCorrect(efficacy, difficulties):
"""Computes the distribution of correct responses.
efficacy: personal ability to answer questions
difficulties: list of difficulties, one for each question
Returns: new Pmf object
"""
pmf0 = thinkbayes.Pmf([0])
ps = [ProbCorrect(efficacy, difficulty) for difficulty in difficulties]
pmfs = [BinaryPmf(p) for p in ps]
dist = sum(pmfs, pmf0)
return dist
def MakeDifficulties(center, width, n):
"""Makes a list of n difficulties with a given center and width.
Returns: list of n floats between center-width and center+width
"""
low, high = center-width, center+width
return numpy.linspace(low, high, n)
def ProbCorrectTable():
"""Makes a table of p_correct for a range of efficacy and difficulty."""
efficacies = [3, 1.5, 0, -1.5, -3]
difficulties = [-1.85, -0.05, 1.75]
for eff in efficacies:
print '%0.2f & ' % eff,
for diff in difficulties:
p = ProbCorrect(eff, diff)
print '%0.2f & ' % p,
print r'\\'
def main(script):
ProbCorrectTable()
exam = Exam()
PlotPriorDist(exam.prior)
exam.CalibrateDifficulty()
exam.CompareScores(780, 740, constructor=Sat)
exam.CompareScores(780, 740, constructor=Sat2)
if __name__ == '__main__':
main(*sys.argv)