# schoolutils

committed ebea145

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

# Files changed (2)

`                                 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.