4237b293ced2bb225e66fb1766f5e5c4e6142312
[pygrader.git] / pygrader / tabulate.py
1 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of pygrader.
4 #
5 # pygrader is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # pygrader is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # pygrader.  If not, see <http://www.gnu.org/licenses/>.
16
17 import math as _math  # for numpy workarounds and testing
18 import sys as _sys
19
20 _numpy_import_error = None
21 try:
22     import numpy as _numpy
23 except ImportError as e:
24     _numpy = None
25     _numpy_import_error = e
26
27 from . import LOG as _LOG
28 from .color import standard_colors as _standard_colors
29 from .color import write_color as _write_color
30
31
32 def _mean(iterable):  # missing-numpy workaround
33     """Return the mean of a list of items.
34
35     >>> print(_mean([0,1,2,3,4,5,6]))
36     3.0
37     """
38     length = len(iterable)
39     return sum(iterable) / float(length)
40
41 def _std(iterable):  # missing-numpy workaround
42     """Return the standard deviation of a list of items.
43
44     >>> print(_std([0,1,2,3,4,5,6]))
45     2.0
46     """
47     length = len(iterable)
48     m = _mean(iterable)
49     return _math.sqrt(sum((x-m)**2 for x in iterable) / length)
50
51 if _numpy is None:
52     _statistics_container = list
53 else:
54     _statistics_container = _numpy.array
55
56 def _statistic(iterabale, statistic):
57     """Calculate statistics on an list of numbers
58     """
59     global _numpy_import_error
60     if _numpy_import_error:
61         assert _numpy_import_error is not None
62         _LOG.warning('error importing numpy, falling back to workarounds')
63         _LOG.warning(str(_numpy_import_error))
64         _numpy_import_error = None
65     if statistic == 'Mean':
66         if _numpy is None:  # work around missing numpy
67             return _mean(iterable)
68         else:
69             return gs.mean()
70     elif statistic == 'Std. Dev.':
71         if _numpy is None:  # work around missing numpy
72             sval = _std(iterable)
73         else:
74             return gs.std()
75     else:
76         raise NotImplementedError(statistic)
77
78 def tabulate(course, statistics=False, stream=None, use_color=None, **kwargs):
79     """Return a table of student's grades to date
80     """
81     if stream is None:
82         stream = _sys.stdout
83     highlight,lowlight,good,bad = _standard_colors(use_color=use_color)
84     colors = [highlight, lowlight]
85     assignments = sorted(set(
86             grade.assignment for grade in course.grades))
87     students = sorted(set(grade.student for grade in course.grades))
88     _write_color(string='Student', color=colors[0], stream=stream)
89     for i,assignment in enumerate(assignments):
90         string = '\t{}'.format(assignment.name)
91         color = colors[(i+1)%len(colors)]
92         _write_color(string=string, color=color, stream=stream)
93     if len(assignments) == len(course.assignments):
94         string = '\t{}'.format('Total')
95         color = colors[(i+2)%len(colors)]
96         _write_color(string=string, color=color, stream=stream)
97     _write_color(string='\n', stream=stream)
98     for student in students:
99         _write_color(string=student.name, color=colors[0], stream=stream)
100         for i,assignment in enumerate(assignments):
101             try:
102                 grade = course.grade(student=student, assignment=assignment)
103                 gs = str(grade.points)
104             except ValueError:
105                 gs = '-'
106             string = '\t{}'.format(gs)
107             color = colors[(i+1)%len(colors)]
108             _write_color(string=string, color=color, stream=stream)
109         if len(assignments) == len(course.assignments):
110             string = '\t{}'.format(course.total(student))
111             color = colors[(i+2)%len(colors)]
112             _write_color(string=string, color=color, stream=stream)
113         _write_color(string='\n', stream=stream)
114     if statistics:
115         _write_color(string='--\n', stream=stream)
116         for stat in ['Mean', 'Std. Dev.']:
117             _write_color(string=stat, color=colors[0], stream=stream)
118             for i,assignment in enumerate(assignments):
119                 color = colors[(i+1)%len(colors)]
120                 grades = [g for g in course.grades
121                           if g.assignment == assignment]
122                 gs = _statistics_container([g.points for g in grades])
123                 sval = _statistic(gs, statistic=stat)
124                 string = '\t{:.2f}'.format(sval)
125                 _write_color(string=string, color=color, stream=stream)
126             if len(assignments) == len(course.assignments):
127                 gs = _statistics_container([course.total(s) for s in students])
128                 sval = _statistic(gs, statistic=stat)
129                 string = '\t{}'.format(sval)
130                 color = colors[(i+2)%len(colors)]
131                 _write_color(string=string, color=color, stream=stream)
132             _write_color(string='\n', stream=stream)