Commits

Richard Lawrence committed 258014f Merge

Merge branch 'cleanup'

Comments (0)

Files changed (1)

grading/grader.py

 """
 grader.py
 
-Enter students' grades individually on a command line, then calculate
-weighted averages and write to CSV.
+Utilities for calculating and reporting grades
 """
-# The beginning of a Python program usually involves importing modules
-# from the standard library.  Here I import the csv module so I can
-# write a csv file, below, and a couple of modules nearly every Python
-# program imports: os and sys.
-import csv, os, sys, math
-
-# A couple of global variables needed by the functions below; you
-# should customize these for the particular class you're grading.
-INPUT_FIELDS = ["Name", "Paper 1 grade", "Paper 2 grade"]
-CALCULATED_FIELDS = ["Final percentage", "Final letter grade"]
-
-# You probably don't need to change this function:        
-def main():
-    "The program's entry point, when run interactively."
-
-    print "AUSTIN'S GREAT GRADING PROGRAM!"
-    print "Enter each student's name and grade."
-    print "To stop entry, leave the name field blank.\n"
-
-    all_grades = []
-    while True:
-        student_grade_or_stop = get_next_student_grades()
-        if student_grade_or_stop:
-            all_grades.push(student_grade_or_stop)
-        else:
-            break
-
-    write_csv(INPUT_FIELDS + CALCULATED_FIELDS, all_grades)
-
-    exit(0) 
-    
-def main_with_csv(in_name, out_name, sort_field=''):
-    "Main entry point, when run against a CSV file"
-    fieldnames, rows = read_csv(in_name)
-
-    calculated_fields = ['Grade average', 'Final grade']
+import csv, os, sys, math, optparse
+
+#
+# Top-level interfaces
+#
+# Each interface accepts an input file handle, an output file
+# handle, and an options structure
+def csv_to_csv(in_file, out_file, options):
+    "Interface for reading and writing CSV files"        
+    fieldnames, rows = read_csv(in_file)
+
+    calculated_rows = [calculate_grade(row) for row in rows]
     
-    for row in rows:
-        for field in calculated_fields:
-            row[field] = calculate_grade(field, row)
+    if options.sort_field:
+        calculated_rows = sort_grade_list(calculated_rows, options.sort_field)
+        
+    write_csv(out_file, calculated_rows[0].keys(), calculated_rows)
 
-    # sort rows before output based on sort_field if requested:
+#
+# Grade-list operations and reporting functions
+#
+def sort_grade_list(rows, sort_field):
+    "Sort a list of grade dicionaries by sort_field"
     def cmp_overall(x,y):
         if x[sort_field] < y[sort_field] or math.isnan(x[sort_field]):
             return 1 # puts highest score at the top
         else:
             return 0
         
-    if sort_field:
-        rows.sort(cmp=cmp_overall)
-
-    # output to CSV:
-    write_csv(out_name, fieldnames + calculated_fields, rows)
-            
-# You'll need to modify this function to suit your needs:
-def get_next_student_grades():
-    """Ask the user for each of a student's assignment grades, then
-    calculate their final percentage and letter grade"""
-    
-    student_grades = {}
-    for f in INPUT_FIELDS:
-        # ask the user for the value to put in each field
-        i = raw_input("Enter %s: " % f)
-        if not i and f == "Name":
-            # stop when a name field is left blank
-            return False
-        else:
-            # no attempt made here to convert input strings to numbers
-            # or other types of value; you'll have to do that at some
-            # point!  (Hint: you'll probably need the float()
-            # function.)
-            student_grades[f] = i
-
-    for f in CALCULATED_FIELDS:
-        student_grades[f] = calculate_grade(f, student_grades)
-
-    return student_grades
-
-# The functions below all deal with calculating grades.  You need to
-# fill in the implementation here.
-def calculate_grade_spring2012(f, sgs):
+    rows.sort(cmp=cmp_overall)
+    return rows
+
+#
+# Grade calculation functions for specific courses
+#
+# Every grade calculation function consumes a dictionary containing
+# entered grades for one student and returns a dictionary containing
+# both the entered grades and the calculated grades.
+def calculate_grade_spring2012(sgs):
     raise NotImplementedError("Implementation of calculate_grade_spring2012 is gone")
 
-def calculate_grade_summer2012(f, sgs):
+def calculate_grade_summer2012(sgs):
     raise NotImplementedError("Implementation of calculate_grade_summer2012 is gone")
 
-def calculate_grade_fall2012(f, sgs):
-    "Calculate a fall 2012 (25A) grade on the basis of entered grades."
-    if f == "Grade average":
-        return letter_grade_avg(sgs, ["Paper 1", "Paper 2", "Paper 3", "Exam grade"])
-    elif f == "Final grade":
-        return points_to_letter(sgs["Grade average"])
-    else:
-        raise NotImplementedError(
-            "Man, I haven't even *heard* of a '%s' grade. " % f +\
-            "Don't even try that shit with me.") 
+def calculate_grade_fall2012(sgs):
+    entered_grades = ["Paper 1", "Paper 2", "Paper 3", "Exam grade"]
+
+    gavg = letter_grade_average(sgs, entered_grades)
+    sgs["Grade average"] = gavg
+    sgs["Final grade"] = points_to_letter(gavg)
+
+    return sgs
 
 # update this every semester
 calculate_grade = calculate_grade_fall2012
 
-# represents ranges for grades on a GPA scale
-# format: (letter grade, point_value, exclusive_max, inclusive_min)
-# values less than -1.0 or greater than 5.0 are assumed not to be on this scale    
+#
+# Utility functions for grade calculations
+#
 POINTS = [
-    ('A+', 4.2, 5.0, 4.0), 
+    # format: (letter grade, point_value, exclusive_max, inclusive_min)
+    ('A+', 4.2, 5.0, 4.2), 
     ('A', 4.0, 4.2, 3.85),
     ('A-', 3.7, 3.85, 3.5),
     ('B+', 3.3, 3.5, 3.15),
     ('I', float("Nan"), float("-inf"), float("inf"))
 ]
 
-def letter_to_points(lgrd):
-    "Convert a letter grade to 4.0-scale points"
-    points_dict = dict([(p[0], p[1]) for p in POINTS])
-    try:
-        return points_dict[lgrd]
-    except KeyError:
-        # assume something went missing
-        return float('Nan')
-    
-def points_to_letter(pts):
+PERCENTS = [
+    # format: (letter grade, point_value, exclusive_max, inclusive_min)
+    ('A+', 100, 200, 97), 
+    ('A', 95, 97, 94),
+    ('A-', 92, 94, 90),
+    ('B+', 88, 90, 87),
+    ('B', 85, 87, 84),
+    ('B-', 82, 84, 80),
+    ('C+', 78, 80, 77),
+    ('C', 75, 77, 74),
+    ('C-', 72, 74, 70),
+    ('D+', 68, 70, 67),
+    ('D', 65, 67, 64),
+    ('D-', 62, 64, 60),
+    ('F', 58, 60, 0),
+    # dummy limits ensure nothing falls in this range:
+    ('I', float("Nan"), float("-inf"), float("inf"))
+]
+
+# Grade type conversions:
+def letter_to_points(letter_grade):
+    "Convert a letter grade to 4.0-scale grade"
+    return letter_to_number(letter_grade, POINTS)
+
+def letter_to_percentage(letter_grade):
+    "Convert a letter grade to a percentage"
+    return letter_to_number(letter_grade, PERCENTS)
+
+def points_to_letter(p):
     "Convert a 4.0-scale grade to a letter grade"
+    return number_to_letter(p, POINTS)
+
+def percentage_to_letter(p):
+    "Convert a percentage to a letter grade"
+    return number_to_letter(p, PERCENTS)
+
+def letter_to_number(letter_grade, scale):
+    """Convert a letter grade to a number using a given scale.
+       Returns float('Nan') for grades not in the scale."""
+    for grade, val, mx, mn in scale:
+        if letter_grade == grade:
+            return val
+    else:
+        return float('Nan')
+
+def number_to_letter(n, scale):
+    """Convert a number grade n to a letter using a given scale.
+       Returns 'I' for float('Nan') grades"""
     # any missing grades should default to Incomplete:
-    if math.isnan(pts):
+    if math.isnan(n):
         return 'I'
 
     # otherwise find the appropriate grade given ranges in the scale:
-    for grade, val, mx, mn in POINTS:
-        if mn <= pts < mx:
+    for grade, val, mx, mn in scale:
+        if mn <= n < mx:
             return grade
     else:
-        raise ValueError("Value not on 4-point scale: %s" % pts) 
-    
-
-def final_percentage(sgs):
-    "Calculate a final percentage based on a student's grades"
-    raise NotImplementedError()
-
-def final_letter_grade(sgs):
-    "Calculate a final letter grade based on a student's grades"
-    raise NotImplementedError()
-
-def homework_avg(sgs):
-    # mean of homework 1-9 scores, except the lowest is dropped
-    hw_fields = [k for k in sgs.keys() if k[0:2] == 'EX']
-    hw_fields.sort()
-
-    # convert strings to floats; non-numeric values get a zero
-    grades = extract_and_zero_grades(hw_fields, sgs)
-
-    # drop the lowest score:
-    grades.sort()
-    grades = grades[1:]
-
-    return sum(grades) / len(grades)
-
-def quiz_avg(sgs):
-    # mean of quizzes 1 through 3, times 10
-    quiz_fields = [k for k in sgs.keys() if k[0:4] == 'Quiz' and len(k) < 12]
-
-    grades = extract_and_zero_grades(quiz_fields, sgs)
-
-    # drop the lowest score:
-    grades.sort()
-    grades = grades[1:]
-
-    # multiply the average by 10, since quizzes were out of 10:
-    return 10.0 * sum(grades) / len(grades)
-
-def letter_grade_avg(sgs, fieldnames):
-    "4.0-scale average (unweighted) of letter grades found in fieldnames"
-    lgrades = [sgs[field] for field in fieldnames]
-    pgrades = map(letter_to_points, lgrades)
-    
-    return sum(pgrades) / len(pgrades)
-    
-def overall_avg(sgs):
-    # final percentage is:
-    # 40% final exam
-    # 20% midterm exam
-    # 25% homework average
-    # 15% quiz average
-    final, midterm = extract_and_zero_grades(
-        ['Final Exam [100]', 'Midterm Exam [100]'], sgs)
-    homework = sgs['Homework average'] # error if not calculated yet!
-    quiz = sgs['Quiz average'] # error if not calculated yet!
-    
-    avg = 0.40 * final + 0.20 * midterm + 0.25 * homework + 0.15 * quiz
-    return avg
-        
-
-# These functions are utility functions that you'll probably want to
-# use in defining final_percentage, final_letter_grade, etc.  You need
-# to provide the implementations.
+        raise ValueError("Value %s not on scale with max=%s and min=%s" %
+                         (n, scale[0][2], scale[-2][3])) 
+     
 def extract_and_zero_grades(fields, d):
     "Extract an array of grades from a dictionary; convert non-numeric values to 0.0"
     # convert strings to floats; non-numeric values get a zero
         grades.append(g)
 
     return grades
-                      
-def weighted_average(nums, weights):
-    "Calculate a weighted average"
-    raise NotImplementedError()
 
-def percentage_to_letter_grade(p):
-    "Convert a percentage to a letter grade"
-    raise NotImplementedError()
+# Aggregations and averages:   
+def letter_grade_average(sgs, fieldnames, weights=None):
+    """4.0-scale average of the letter grades found in sgs.
+       If given, weights[i] should be a weight for the letter grade in
+       sgs[fieldnames[i]].
+    """
+    lgrades = [sgs[field] for field in fieldnames]
+    pgrades = map(letter_to_points, lgrades)
 
-def letter_grade_to_percentage(lg):
-    "Convert a letter grade to a percentage"
-    raise NotImplementedError()
-
-# This function will output the final list of student grades to a CSV
-# file, so that you can look at the results in a text editor or in a
-# spreadsheet.  I've provided the implementation here.
-def write_csv(fname, fields, all_sgs):
-    "Write a list of dictionaries representing student grades to rows in a CSV file"
-    # don't try to write an empty list of grades:
+    if weights:
+        return weighted_average(pgrades, weights)
+    else:
+        return unweighted_average(pgrades)
+    
+def unweighted_average(grades):
+    "Calculate an unweighted average"
+    return float(sum(grades)) / len(grades)
+
+def weighted_average(grades, weights):
+    """Calculate a weighted average.
+    weights[i] should be the weight given to the grade in grades[i].
+    This function does not check that weights sum to 1 or normalize
+    the resulting grades.
+    """
+    return sum([n[0] * n[1] for n in zip(grades, weights)])
+
+#
+# I/O
+#
+def write_csv(f, fields, all_sgs):
+    "Write a list of student grade dictionaries to rows in a CSV file"
+    # simple sanity check: don't try to write an empty list of grades
     if not all_sgs:
-        return "Grade table empty; skipping csv write."
+        sys.stderr.write("Grade table empty; skipping csv write.\n")
+        return all_sgs
 
-    if os.path.exists(fname):
-        sys.stderr.write("Warning! %s already exists; overwriting!\n" % fname)
+    writer = csv.DictWriter(f, fields)
 
+    # writeheader() became available in Python 2.7:
     try:
-        f = open(fname, 'w')
-        writer = csv.DictWriter(f, fields)
+        writer.writeheader()
+    except AttributeError:
+        writer.writerow(dict(zip(fields,fields)))
 
-        # writeheader() became available in Python 2.7:
-        try:
-            writer.writeheader()
-        except AttributeError:
-            writer.writerow(dict(zip(fields,fields)))
+    for row in all_sgs:
+        writer.writerow(row)
 
-        # the actual data:    
-        for row in all_sgs:
-            writer.writerow(row)
-    finally:
-        f.close()
-
-    return "Success!"
+    return all_sgs
 
-def read_csv(fname, *args, **kwargs):
+def read_csv(f, *args, **kwargs):
     "Read a list of student grades as dictionaries from a CSV file"
-    f = open(fname)
     reader = csv.DictReader(f, *args, **kwargs)
     rows = [row for row in reader]
-    f.close()
 
     return reader.fieldnames, rows
-    
-# This is just a (somewhat strange-looking) Python idiom. It tells the
-# interpreter to run the main() function when you type "python
-# grader.py" at the command line.
+
+#
+# Main function when used as a script from CLI
+#
+def main():
+    parser = optparse.OptionParser()
+    parser.add_option("-i", "--csv-in", dest="in_file",
+                      metavar="FILE",
+                      help="Input data as CSV from FILE")
+    parser.add_option("-o", "--csv-out", dest="out_file",
+                      metavar="FILE",
+                      help="Output data as CSV to FILE")
+    parser.add_option("-s", "--sort", dest="sort_field",
+                      metavar="FIELD_NAME",
+                      help="Sort data on FIELD_NAME before output")
+    options, args = parser.parse_args()
+
+    if options.in_file:
+        in_file = open(options.in_file, 'r')
+    else:
+        in_file = sys.stdin
+        
+    if options.out_file:
+        out_file = open(options.out_file, 'w')
+    else:
+        out_file = sys.stdout
+
+    try:
+        csv_to_csv(in_file, out_file, options)
+        exit(0)
+    finally:
+        in_file.close()
+        out_file.close()
+
 if __name__ == '__main__':
-   PREFIX = '/home/rwl/Documents/philosophy/teaching/25A/grades'
-   main_with_csv(PREFIX + '/fall2012_grades.csv',
-                 PREFIX + '/fall2012_grades_calculated.csv',
-                 sort_field='Grade average')
+    main()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.