Richard Lawrence avatar Richard Lawrence committed ebea145

* reports.py, calculator_helpers.py: first stab at histograms (UNTESTED!)

Comments (0)

Files changed (2)

schoolutils/grading/calculator_helpers.py

                                 letter_func=letter_grade_average,
                                 filter_nan=filter_nan)
 
+def freqs_for_letters(values):
+    """Returns frequencies for a list of letter grade values.
+       Frequencies are represented as a dictionary mapping letter grade strings to
+       integers."""
+    freqs = dict((p[0], 0) for p in POINTS) # ensure we get all grade values 
+    for v in values:
+        freqs[v] += 1
+    return freqs
+
+def freqs_for_numbers(values, bins):
+    """Returns frequencies for a list of numeric grade values.
+       Values are binned according to the ranges defined by the bins:
+         each item in bins should be a tuple t such that
+          t[0] is an exclusive max, and
+          t[1] is an inclusive min
+         defining the range for a bin.
+       Frequencies are represented as a dictionary mapping bins to integers."""
+    freqs = dict((b, 0) for b in bins)
+    for v in values:
+        for b in bins:
+            if b[0] > v >= b[1]:
+                freqs[b] += 1
+                break
+        else:
+            raise ValueError("Value %s did not fit in any bin!" % v)
+
+    return freqs
+
 # Munging input data:
 def unpack_entered_grades(rows):
     """Extract grade values, weights, types and assignment names from a sequence

schoolutils/reporting/reports.py

             missing = [g['student_id'] for g in grades if g['grade_id'] is None]
             try:
                 mn, mx, avg = self.calculate_stats(grades)
+                hist = self.histogram(grades)
                 stats.append({
                         'assignment_id': a['id'],
                         'assignment_name': a['name'],
                         'min': mn,
                         'max': mx,
                         'mean': avg,
+                        'hist': hist,
                         'missing_students': missing,
                         })
             except (ValueError, TypeError) as e:
         if math.isnan(avg):
             avg = None
         if avg and grade_type == 'letter':
-            # letter grade is more useful here
+            # knowing letter grade is more useful here
             avg = ch.points_to_letter(avg)
        
         return mn, mx, avg
 
     def histogram(self, grades):
-        """Produce a simple text histogram indicating an assignment's distribution of grades.
-           CURRENTLY ONLY IMPLEMENTED FOR ASSIGNMENTS WITH LETTER GRADE TYPE."""
+        "Produce a simple text histogram indicating an assignment's distribution of grades."
         values, weights, types, _ = ch.unpack_entered_grades(grades)
-        raise NotImplementedError
+
+        if types[0] == 'letter': # values are for single assignment and grade type
+            bins = [p[0] for p in ch.POINTS] # grade values in descending order
+            freqs = ch.freqs_for_letters(values)
+        else:
+            if types[0] == '4points':
+                scale = ch.POINTS
+            elif types[0] == 'percent':
+                scale = ch.PERCENTS
+            else:
+                raise ValueError("Can't calculate histogram bins for assignment type %s" %
+                                 types[0])
+            bins = [p[2], p[3] for p in scale]
+            bins.pop(-1) # remove "dummy" limits bin with inf/-inf bounds 
+            freqs = ch.freqs_for_numbers(values, bins)
+
+        def bin_str(b):
+            if isinstance(b, tuple):
+                return "{1:<.2f} to {0:<.2f}".format(*b)
+            else:
+                return b # letter grade "bins" are already strings
+            
+        line_template = "{bin: >10}: {bars}\n"
+        lines = [line_template.format(bin=bin_str(b),
+                                      bars="".join("|" for i in range(freqs[b])))
+                 for b in bins] # ensure grade values are printed in order
+
+        return "".join(lines)
     
     def as_text(self, compact=False):
         """Return a textual representation of this report as a string.
         stats_template = ("{assignment_name: <25s}\n"
                           "Grade type: {grade_type: <8s} Weight: {weight: <8}\n"
                           "Average: {mean: <8} Minimum: {min: <8} "
-                          "Maximum: {max: <8}\n")
+                          "Maximum: {max: <8}\n"
+                          "Distribution:\n{hist}\n")
         no_stats_msg = ("{assignment_name: <25s}\n  No statistics available for "
                         "this assignment, because:\n  {unavailable}\n")
         missing_template = ("{num_missing} students do not have a grade for this "
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.