Grading Page

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

  1. 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.
  2. 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)  

Grading Page (last edited 2014-05-25 18:26:50 by localhost)