1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
|
#!/usr/bin/env python3
import argparse
import csv
from functools import partial
import matplotlib.pyplot as plt
import numpy
import re
def readcsv(filename):
"""Read a (BlackBoard) CSV file into a header and data"""
with open(filename, 'r') as csvfile:
reader = csv.reader(csvfile, delimiter=',', quotechar='"')
data = []
header = next(reader)
for row in reader:
data.append(row)
return header, numpy.array(data)
def parse_float(f):
"""Parse a string into a float and return 0. when that fails"""
try:
return float(f)
except ValueError:
return 0.
def parse_floats(matrix):
"""Apply parse_float on all elements of a matrix"""
m_new = []
for row in matrix:
m_new.append([parse_float(v) for v in row])
return m_new
def remove_zeros(matrix):
"""Remove zeros from a matrix
Typically, BlackBoard grade lists will contain many zeros that we don't
want to distort the boxplots. This function removes zeros on a per-column
basis."""
m_new = []
for col in numpy.transpose(matrix):
m_new.append([v for v in col if v != 0.0])
return numpy.transpose(m_new)
def normalise(matrix):
"""Normalise grades to a 0-100 range
Some grades are given in a 0-10 range, others in a 0-100. This function
normalises *but these two* into a 0-100 range"""
m_new = []
for row in matrix:
m_new.append(row if max(row) > 10.0 else [10 * i for i in row])
return m_new
def strip_header(h):
"""Strip common BlackBoard additions in the CSV header"""
return h.split('instructor.download.column.total')[0]
def remove_empty_lists(headers, data):
"""Remove empty columns and the corresponding headers from a matrix"""
new_headers, new_data = [], []
for h, d in zip(headers, data):
if d != []:
new_headers.append(h)
new_data.append(d)
return new_headers, new_data
def regex_callback(header, regex='', invert=False):
"""Check whether a regex occurs in a header
This is an example of a possible callback function."""
match = re.compile(regex).search(header) != None
return match != invert
def check_callback(headers, data, header_callback):
"""For each header, check that we want to show it, and remove data if not"""
new_headers, new_data = [], []
for h, d in zip(headers, data):
if header_callback(h):
new_headers.append(h)
new_data.append(d)
return new_headers, new_data
def plotgrades(headers, data, header_callback=lambda x:True):
"""Plot grades corresponding to headers in a boxplot"""
data = parse_floats(data)
data = remove_zeros(data)
headers, data = remove_empty_lists(headers, data)
headers, data = check_callback(headers, data, header_callback)
data = normalise(data)
headers = [strip_header(h) for h in headers]
if len(data) > 0:
plt.boxplot(data)
ax = plt.gca()
plt.xticks(range(0, len(data) + 1), [''] + headers, rotation=90)
ax.set_ylim([0,100])
plt.show()
def parse_args():
"""Parse command line arguments"""
pars = argparse.ArgumentParser(description='Plot BlackBoard grades')
pars.add_argument('-s', '--skip', metavar='n', type=int, default=0,
help='Skip the first n columns')
pars.add_argument('-w', '--where', metavar='regex', default='',
help='Restrict what grades are shown with a regex')
pars.add_argument('-i', '--invert', action='store_true',
help='Invert --where regex')
pars.add_argument('filename', metavar='file',
help='The CSV file as exported by BlackBoard')
return pars.parse_args()
def main():
"""Plot BlackBoard grades from a CSV file in boxplots"""
args = parse_args()
headers, data = readcsv(args.filename)
plotgrades(headers[args.skip:], data[:,args.skip:],
partial(regex_callback, regex=args.where, invert=args.invert))
if __name__ == '__main__':
main()
|