## Grading Page

There are two independent ways to evaluate performance in the course:

- Use syllabus. I have decided to keep the original targets of passing performance specified in the syllabus for all but the first exam. So, passing the first exam needed a score of 35; passing the second and third exams needed scores of 70. The makeup exam substituted for a non-passing score on any previous exams not passed. The project needed a combined score of 90. I had planned to lower this number, but instead of that, I used a curve.
- Use a curve. By combining all points from all exams and all homeworks, each student has a point total from the course. The distribution of point totals is shown below, along with some statistics about the totals. The curve can be used to define categories for different GPA's (letter grades).

The grading policy is to take the higher of the two methods. In fact, it appears there are eight students who will get a higher grade than what the curve would define. This is because those students did better according to the syllabus, in passing exams, than what the performance by the curve would suggest.

As an experiment, I also tried changing the definition of passing exams (on Exam 2 and Exam 3) from 70 to 68, to see if a few points would have made a difference. It made no difference. When I tried using 65 instead as the passing grade, it did create ten, instead of eight, cases where a syllabus-like policy would have changed grades. (Such an experiment was useful, to respond to requests from several students who got exam scores right on the borderline of passing, either 68 or 69 rather than 70.)

### GPA by Curve

These are the lines separating different grades. The left column shows the point total, from all exams (possibly including the makeup final to substitute for a non-passing exam score) and from all project homeworks.

> 351 A > 335 A- > 314 B+ > 258 B > 237 B- > 221 C+ > 170 C > 50 C-

There were eight exceptions to this table, where students got a higher grade because of the number of exams passed and/or having a project score of at least 90.

### Statistics

Fifty-nine (59) students benefited from the makeup exam, because it substituted for the score of Exam 2 or the score of Exam 3 in the grade computation (see program below). The mean and variance statistics below include this substitution:

Max = 395.0 Median = 270.211653511 Standard Deviation = 74.9538548056 Mean + 2*StDev = 407.480645055 Mean + StDev = 332.526790249 Mean = 257.572935443 Mean - StDev = 182.619080638 Mean - 2*2*StDev = 107.665225832 Distribution of Scores: 395 391 373 372 371 368 365 364 362 361 360 352 352 352 351 350 349 349 348 347 347 344 341 341 339 338 336 332 329 329 329 328 326 325 325 324 324 322 321 320 320 319 319 317 316 316 316 316 315 313 312 312 312 311 310 310 310 309 306 306 305 305 304 304 302 301 301 301 300 299 299 299 298 297 295 295 295 295 295 295 294 294 291 291 290 290 287 284 281 279 279 278 275 274 273 272 271 271 270 268 268 266 266 265 264 262 260 259 257 257 257 256 255 254 252 251 246 245 245 244 243 243 243 243 242 239 238 237 235 235 235 235 234 232 230 228 226 226 222 219 218 218 215 214 214 213 212 205 205 203 202 200 199 199 192 190 190 188 185 183 181 181 179 178 174 173 172 171 171 171 163 159 156 154 150 148 147 147 145 145 142 142 138 135 131 129 128 122 110 95 94 80 60 59 50 32 meanGPA = 2.73505102041 meanGPA (with October roster) = 2.49334883721 Syll meanGPA = 1.98525510204 Syll meanGPA (with October roster) = 1.80981395349

The October roster has more students than there are enrolled at the end of the course, due to a number of drops. Including those students would change the denominator for computing the mean.

### The Program

Here is the program used to calculate scores and grades, still being checked for bugs.

```
1 import sys, os, csv, codecs, subprocess, tempfile
2 from operator import itemgetter
3 from pprint import *
4
5 if sys.version_info[0] > 2 or sys.version_info[0] < 2:
6 print("Only works with version 2 of Python")
7 sys.exit(1)
8 if sys.version_info[1] < 7:
9 print "Need at least version 2.7 of Python"
10 sys.exit(1)
11
12 GPATable = {4.33:"A+", 4.0:"A", 3.67:"A-",
13 3.33:"B+", 3.0:"B", 2.67:"B-",
14 2.33:"C+", 2.0:"C", 1.67:"C-",
15 1.33:"D+", 1.0:"D", 0.67:"D-"}
16
17 def BuildCategories():
18 "return a categories translation table"
19 F = open("categories.txt")
20 D, L = dict(), list()
21 for line in F:
22 if line[0] != ' ':
23 for item in L:
24 D[item] = line.strip()
25 L = list() # anticipate doing another set
26 else:
27 L.append(eval(line.strip()))
28 return D
29
30 Categories = BuildCategories()
31
32 def ReadCSV(csvfile):
33 "return (file,DictReader) of csv file"
34 F = open(csvfile,'r')
35 S = F.read()
36 F.close()
37 # For convenience (rather than writing a wrapper), just
38 # manually eliminate the UTF-8 and silly #-markers for EOL
39 S = S.replace(codecs.BOM_UTF8,'')
40 S = S.replace("#",'')
41 O = tempfile.SpooledTemporaryFile(max_size=2*1024*1024,mode='w')
42 O.write(S)
43 O.flush()
44 O.seek(0)
45 R = csv.DictReader(O)
46 return (O,R)
47
48 def GetGradeFiles():
49 "return (file,dictreader) list for each csv"
50 names = [ m for m in os.listdir(".") if m.endswith(".csv") ]
51 gradebooks = [] # will be (file,dictreader) for each csv
52 for item in names:
53 gradebooks.append(ReadCSV(item))
54 return gradebooks
55
56 def NormalizeNames(D):
57 "revise names in dictionary D & remove excess attributes"
58 # start by normalizing student name
59 assert 'Last Name' in D and 'First Name' in D
60 assert 'Name' not in D
61 D['Name'] = (D['First Name'],D['Last Name'])
62 del D['First Name'], D['Last Name']
63 # now, for each field in Categories, translate
64 for key in Categories:
65 if key in D:
66 D[Categories[key]] = D[key]
67 # finally, remove excess attributes from D
68 for key in D.keys():
69 if key not in Categories.values() + ["Name"]:
70 del D[key]
71
72 def ConvReal(s):
73 "convert string s into a float"
74 return 0.0 if s == '' else float(s)
75
76 def BuildCombined():
77 "return table of Name -> (E1,E2,E3,E4,H1,H2,H3,H4,H5)"
78 books, table = GetGradeFiles(), dict()
79 for tempfile,reader in books:
80 for dictionary in reader:
81 NormalizeNames(dictionary)
82 assert dictionary["Name"] not in table
83 table[dictionary["Name"]] = tuple(
84 [ConvReal(dictionary[i]) for i
85 in "E1 E2 E3 E4 H1 H2 H3 H4 H5".split()])
86 return table
87
88 def ScaleToMax(score,maxscore,weight):
89 "linearly scale a score to the maximum score"
90 r = score/maxscore
91 return r*weight
92
93 def WeighScores(T):
94 "revise BuildCombined table to weight each score"
95 weights = [("E1",100),("E2",100),("E3",100),("E4",100),
96 ("H1",15),("H2",15),("H3",20),("H4",20),("H5",30)]
97 # get list of maximum for each column in T
98 maxpercolumn = list()
99 for i in range(len(T.values()[0])):
100 maxpercolumn.append(max( item[i] for item in T.values() ))
101 # phase I: scale each column to its maximum & weight
102 for name in T:
103 newvec = list()
104 for i in range(len(maxpercolumn)):
105 newvec.append(ScaleToMax(T[name][i],
106 maxpercolumn[i],
107 weights[i][1]))
108 T[name] = newvec # note, type is now a list
109 assert len(newvec) == len(weights)
110
111 # phase II: use E4 as makeup on E1, E2, or E3
112 for name in T:
113 E1, E2, E3, makeup = [T[name][i] for i in range(4)]
114 if E3 < 70 and makeup > E3:
115 E3 = makeup
116 elif E2 < 70 and makeup > E2:
117 E2 = makeup
118 elif E1 < 35 and makeup > E1:
119 E1 = makeup
120 T[name][:3] = E1, E2, E3
121
122 def TotalScores(T):
123 "add total column to result of WeighScores"
124 for name in T:
125 E1,E2,E3,E4,H1,H2,H3,H4,H5 = T[name]
126 # total would be 400.0 at maximum
127 T[name].append(E1+E2+E3+H1+H2+H3+H4+H5)
128
129 # These three calls build a normalized table,
130 # with the total points in the final column
131 T = BuildCombined()
132 WeighScores(T)
133 TotalScores(T)
134
135 distribution = [T[name][-1] for name in T]
136 distribution.sort(reverse=True)
137 mean = sum(distribution)/len(distribution)
138 vari = sum( (e-mean)**2 for e in distribution )/len(distribution)
139 sdev = vari ** 0.5
140 median = distribution[len(distribution)/2]
141 print "**** Distribution of Total Points ****\n"
142 print "\tMax =", max(distribution)
143 print "\tMedian =", median
144 print "\tStandard Deviation =", sdev
145 print "\tMean + 2*StDev =", mean+2*sdev
146 print "\tMean + StDev =", mean+sdev
147 print "\tMean =", mean
148 print "\tMean - StDev =", mean-sdev
149 print "\tMean - 2*2*StDev =", mean-2*sdev
150 showdist = [ int(distribution[i]+0.5) for i
151 in range(len(distribution)) ]
152 print "\tDistribution of Scores:\n"
153 for i in range(0,len(showdist),16):
154 print "\t" ,
155 for score in showdist[i:i+16]:
156 print score ,
157 print '' # end of line
158
159 def DistGPA(T,middle,sdev):
160 "Add Column for Distribution-based GPA"
161 for name in T:
162 total = T[name][-1]
163 intervals = { (2*total,middle+1.25*sdev):4.0,
164 (middle+1.25*sdev,middle+1.0*sdev):3.67,
165 (middle+1.0*sdev,middle+0.75*sdev):3.33,
166 (middle+0.75*sdev,middle):3.0,
167 (middle,middle-0.25*sdev):2.67,
168 (middle-0.25*sdev,middle-0.5*sdev):2.33,
169 (middle-0.5*sdev,middle-1.25*sdev):2.0,
170 (middle-1.25*sdev,middle-3.0*sdev):1.67}
171 gpa = 0.0
172 for top,bottom in intervals.keys():
173 if top>=total>=bottom:
174 gpa = intervals[(top,bottom)]
175 T[name].append(gpa)
176 # also show mean GPA (target is 2.63 by CLAS)
177 #meanGPA = sum( T[name][-1] for name in T ) / 215
178 meanGPA = sum( T[name][-1] for name in T ) / len(T)
179 print "\n\tmeanGPA =", meanGPA
180 meanGPA = sum( T[name][-1] for name in T ) / 215
181 print "\tmeanGPA (with October roster) =", meanGPA
182
183 DistGPA(T,mean,sdev) # add distribution GPA column
184
185 def SyllabusGPA(T):
186 "Add Column for Syllabus-based GPA"
187 for name in T:
188 passes = [ T[name][0]>=35,
189 T[name][1]>=70,
190 T[name][2]>=70,
191 sum(T[name][4:4+5])>=90 ]
192 gpa = 1.0
193 for i in range(2,5):
194 if passes.count(True) == i:
195 gpa = float(i) - 0.33 # propose A-, B-, C-
196 T[name].append(gpa)
197 meanGPA = sum( T[name][-1] for name in T ) / len(T)
198 print "\n\tSyll meanGPA =", meanGPA
199 meanGPA = sum( T[name][-1] for name in T ) / 215
200 print "\tSyll meanGPA (with October roster) =", meanGPA
201
202 SyllabusGPA(T)
203
204 # Warn if SyllabusGPA is greater than DistGPA
205 print "\n"
206 for name in T:
207 if T[name][-1]>T[name][-2]:
208 print "\tSyllabus improves", name, T[name][-1], T[name][-2]
209
210 # Build Final Table, sorted by scores
211 NameScoreTable = list()
212 GPAsum = 0
213 for name in T:
214 g = max(T[name][-1],T[name][-2]) # max GPA of curve or syllabus
215 row = ("{0}/{1}".format(*name), int(T[name][-3]+0.5), g )
216 GPAsum += g
217 NameScoreTable.append(row)
218
219
220 NameScoreTable = sorted( NameScoreTable,
221 reverse=True,
222 key=itemgetter(1) )
223
224 print "\n\n**** Score Table \n\n"
225 pprint(NameScoreTable)
226
227 print "\nTarget GPA according to College =", 2.63
228 print "Class GPA = ", GPAsum/len(NameScoreTable)
229 print '''If students enrolled early in course
230 (at the time of the homework one, 30 Sept) were
231 included in the calculation of the Class GPA,'''
232 print "Alternate Class GPA = ", GPAsum/215 # total with dropped
233 print "\n(delta) n be to get target 2.63 =",
234 print int(GPAsum/2.63)-len(NameScoreTable)
```