Skip to content

Instantly share code, notes, and snippets.

@henryiii henryiii/setgrades.py
Last active Aug 29, 2015

Embed
What would you like to do?
This is a python based grading program. Requires Anaconda, or standard scientific python libraries. Uses TK graphics, so you don't need to have Qt.
#!/usr/bin/env python
'''
Set final grades
Henry Schreiner
Version 1.5
This requires Anaconda, or the following:
* Python 2.7 or 3.3+
* Numpy
* Scipy
* Matplotlib
* Pandas
* Six
Note: Most of the time, Python includes the
required graphical toolkit, Tkinter. However,
on some OS's, Python is separated and you may
need to add python-tkinter manually.
'''
from __future__ import division, print_function
import sys
import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
from matplotlib.transforms import offset_copy
from matplotlib.widgets import Button
import platform
import re
from six import string_types
import pandas as pd
from six.moves.tkinter import Tk
from six.moves.tkinter_tkfiledialog import askopenfilenames, askopenfilename, asksaveasfile
class Cutoffs(object):
def __init__(self, students):
self.gpacuts = np.array([60, 62, 68, 70, 72, 78, 80, 82, 88, 90, 92.])+.5
self.gpanames = np.array(['F','D-', 'D', 'D+', 'C-', 'C', 'C+', 'B-', 'B', 'B+', 'A-', 'A'])
self.gpavalues = np.array([0, 1-1/3, 1, 1+1/3, 2-1/3, 2, 2+1/3, 3-1/3, 3, 3+1/3, 4-1/3, 4])
self.student_list = students
self.student_list.sort(['Score'],ascending=False,inplace=True)
#self.totals = students['Score'].order(ascending=False)
# Remove the following line to remove cut prediction
self.gpacuts[:] = self.predict_cuts()
@property
def gpacutsfull(self):
'Add 0 and 100 to the cuts'
return np.append(0,np.append(self.gpacuts,100))
@property
def num_students(self):
return len(self.student_list)
def hist(self):
return sp.histogram(self.student_list['Score'],self.gpacutsfull)[0]
def check(self):
return np.sum(self.hist()*self.gpavalues)/self.num_students
def listhist(self):
return ['{}:{}'.format(nm,hist) for hist, nm in zip(self.hist()[::-1],self.gpanames[::-1]) if hist > 0]
def whatgrade(self,grade):
return self.gpanames[abovewhich(self.gpacutsfull,grade)]
def listcuts(self):
return ['{}:{}'.format(self.gpacuts[-n-1],self.gpanames[-n-1]) for n in range(len(self.gpacuts))]
def predict_cuts(self):
cuts = (predict_cuts(self.student_list['Score'])-.5).round()+.5
return np.fmax(np.fmin(cuts,99.5),9.5)
def itcuts(self):
return [(self.gpanames[-n-1],self.gpacuts[-n-1]) for n in range(len(self.gpacuts))]
def __repr__(self):
return '{} students, Average GPA:{:.2f}'.format(self.num_students, self.check())
def __str__(self):
return repr(self)
def gpastring(self):
return 'GPA:{:.2f}'.format(self.check())
def plot(self):
self.max = max(self.student_list['Score'])+2
self.min = min(x for x in self.student_list['Score'] if x > 5)-4
fig = plt.figure(1,figsize=(16,8))
ax = fig.add_subplot(111,
autoscale_on=False,
xlim=(0,self.num_students),
ylim=(self.min,self.max))
fig.subplots_adjust(right=0.76, left=.05)
# Set 1 line per %
ax.yaxis.set_major_locator(plt.MultipleLocator(10.0))
ax.yaxis.set_minor_locator(plt.MultipleLocator(1.0))
ax.yaxis.grid(which='major', linewidth=0.75, linestyle='-', color='0.75')
ax.yaxis.grid(which='minor', linewidth=0.25, linestyle='-', color='0.75')
# Remove xaxis stuff
ax.xaxis.set_ticks([])
#set lines
self.listlines = [plt.axhline(a,axes=ax,alpha=.3) for a in self.gpacuts]
gpacutsforav = np.append(self.min,np.append(self.gpacuts,self.max))
self.listlabels = [ax.annotate(let,
(self.num_students-.7,(gpacutsforav[n]+gpacutsforav[n+1])/2),
verticalalignment='center')
for n,let in enumerate(self.gpanames)]
self.labelpoints = [ax.annotate(self.whatgrade(a),(loc+.5,a+.5),color='r',weight='medium',horizontalalignment='center')
for loc,a in enumerate(self.student_list['Score'])]
self.label_state = 2
for n in range(self.num_students):
whatgrade = self.whatgrade(self.student_list.iloc[n]['Score'])
self.student_list.loc[self.student_list.index[n],'Grades'] = whatgrade
X = np.arange(self.num_students)
# Main plots
self.bar = ax.bar(X+.1,self.student_list['Score'].round(),alpha=.2,color='b')
ax.scatter(X+.5,self.student_list['Score'],color='r')
# Show average and stddev
totalmean = self.student_list['Score'].mean()
totalstd = self.student_list['Score'].std()
ofsetbig = offset_copy(ax.transData,x=-25,units='dots')
ofsetsm = offset_copy(ax.transData,x=-20,units='dots')
ax.annotate("",
xy=(0,totalmean),
xytext=(0,totalmean),
xycoords=ax.transData,
textcoords=ofsetbig,
annotation_clip=False,
arrowprops=dict(arrowstyle="->",color='g'),
)
ax.annotate("",
xy=(0,totalmean+totalstd),
xytext=(0,totalmean+totalstd),
xycoords=ax.transData,
textcoords=ofsetsm,
annotation_clip=False,
arrowprops=dict(arrowstyle="->",color='g',alpha=.8),
)
ax.annotate("",
xy=(0,totalmean-totalstd),
xytext=(0,totalmean-totalstd),
xycoords=ax.transData,
textcoords=ofsetsm,
annotation_clip=False,
arrowprops=dict(arrowstyle="->",color='g',alpha=.8),
)
# Title
ax.set_title('Mean: {:.2f}, Median: {:.2f}, Stddev: {:.2f}'.format(totalmean, np.median(self.student_list['Score']),totalstd))
# Label on the bottom
self.lowlab = ax.text(.5, -0.01,
str(self),
horizontalalignment='center',
verticalalignment='top',
transform = ax.transAxes)
self.lowlab.set_color(('g' if 2.95 < self.check() < 3.05 else 'r'))
changer = LineChanger(self)
changer.connect(fig, ax)
togglestepax = plt.axes([0.89, 0.05, 0.09, 0.075])
togglestep = Button(togglestepax, 'Toggle\nstepping')
togglestep.on_clicked(changer.togglestepping)
togglelabelax = plt.axes([0.89, 0.15, 0.09, 0.075])
togglelabel = Button(togglelabelax, 'Toggle\nlabels')
togglelabel.on_clicked(self._togglelabels)
savecutsax = plt.axes([0.89, 0.25, 0.09, 0.075])
savecuts = Button(savecutsax, 'Save Cuts')
savecuts.on_clicked(self._savecuts)
saveimportax = plt.axes([0.78, 0.35, 0.20, 0.075])
saveimport = Button(saveimportax, 'Save Registrar import file')
saveimport.on_clicked(self._saveimport)
loadcutsax = plt.axes([0.78, 0.25, 0.09, 0.075])
loadcuts = Button(loadcutsax, 'Load cuts')
loadcuts.on_clicked(self._loadcuts)
savegradessax = plt.axes([0.78, 0.15, 0.09, 0.075])
savegrades = Button(savegradessax, 'Save Grades')
savegrades.on_clicked(self._savegrades)
self.histax = plt.axes([0.78, 0.50, 0.2, 0.4])
self.sideplot = self.histax.bar(np.arange(len(self.gpanames))+.1,self.hist())
self.histax.set_xbound(lower=0, upper=len(self.gpanames))
self.histax.set_xticks(np.arange(1,len(self.gpanames)+1)-.5)
self.histax.set_xticklabels(self.gpanames)
self.histax.set_ybound(upper=max(self.hist())+.5)
self._colorbars()
plt.show(block=True)
def _savegrades(self, event=None):
sorted_students = self.student_list.sort(['Last','First'])
print(sorted_students)
root = Tk()
root.withdraw()
csvfile = asksaveasfile(mode='w',defaultextension='.csv',title='Choose a file to save grades to')
root.destroy()
if csvfile:
sorted_students.to_csv(csvfile)
def _savecuts(self, event=None):
root = Tk()
root.withdraw()
csvfile = asksaveasfile(mode='w',defaultextension='.csv',title='Choose a file to save cuts to')
root.destroy()
if csvfile:
pd.DataFrame(self.gpacuts).to_csv(csvfile)
def _saveimport(self, event=None):
root = Tk()
root.withdraw()
csvfile = asksaveasfile(mode='w',defaultextension='.csv',title='Choose a file to save registrar import to')
root.destroy()
if csvfile:
sorted_students = self.student_list.sort(['Last','First'])
finalgrades = pd.DataFrame()
finalgrades['Name'] = sorted_students['Last'] + ', ' + sorted_students['First']
finalgrades['Grade'] = sorted_students['Grades']
finalgrades['Absences'] = ''
finalgrades['Remarks'] = ''
finalgrades['Unique'] = sorted_students['Class']
finalgrades.to_csv(csvfile,sep='\t',index_label='EID',encoding='utf-8')
def _loadcuts(self, event=None):
filetypes = [
('Cuts file','*.csv'),
('Allfiles','*'),
]
if platform.system() != 'Windows':
filetypes.append(filetypes.pop(0)) # Reorder so the all gradefiles is selected first
root = Tk()
root.withdraw()
csvfile = askopenfilename(filetypes=filetypes, title='Choose a file to load cuts from')
root.destroy()
if csvfile:
cuts = pd.DataFrame.from_csv(csvfile).values.flatten()
for i in range(len(cuts)):
self._change(i,cuts[i])
def _togglelabels(self,event=None):
self.label_state = (self.label_state + 1) % 3
for n, label in enumerate(self.labelpoints):
label.set_visible(self.label_state != 0)
if self.label_state != 0:
label.set_text(self.student_list.iloc[n]['First'] + ' ' + self.student_list.iloc[n]['Last'][0] + '.'
if self.label_state == 1 else self.whatgrade(self.student_list.iloc[n]['Score']))
def _updatesideplot(self):
for bar,hi in zip(self.sideplot,self.hist()):
bar.set_height(hi)
self.histax.set_ybound(upper=max(self.hist())+.5)
def _colorbars(self):
for n,bar in enumerate(self.bar):
if self.whatgrade(self.student_list.iloc[n]['Score']) == self.whatgrade(self.student_list['Score'].round()[n]):
bar.set_color('b')
else:
bar.set_color('r')
def _change(self,linenum,changeto):
if self.gpacutsfull[linenum] < changeto < self.gpacutsfull[linenum+2] and self.gpacutsfull[linenum+1] != changeto:
self.gpacuts[linenum] = changeto
self.listlines[linenum].set_ydata([changeto, changeto])
for n,label in enumerate(self.labelpoints):
whatgrade = self.whatgrade(self.student_list.iloc[n]['Score'])
if self.label_state == 2:
label.set_text(whatgrade)
self.student_list.loc[self.student_list.index[n],'Grades'] = whatgrade
self.lowlab.set_text(str(self))
gpacutsforav = np.append(self.min,np.append(self.gpacuts,self.max))
self.listlabels[linenum].xyann = (self.num_students-.7,(gpacutsforav[linenum]+gpacutsforav[linenum+1])/2)
self.listlabels[linenum+1].xyann = (self.num_students-.7,(gpacutsforav[linenum+1]+gpacutsforav[linenum+2])/2)
self.lowlab.set_color(('g' if 2.95 < self.check() < 3.05 else 'r'))
self._colorbars()
self._updatesideplot()
return True
else:
return False
class LineChanger(object):
'This handles moving lines by dragging'
def __init__(self,cutinst):
self.cuts = cutinst
self.pressed = False
self.curline = None
self.stepping = True
def on_press(self,event):
try:
self.curline = closest(self.cuts.gpacuts,event.ydata)
self.pressed = True
except TypeError:
pass
def on_release(self,event):
self.pressed = False
self.curline = None
def on_motion(self,event):
if self.pressed and event.inaxes == self.ax:
newval = (np.round(event.ydata-.5)+.5 if self.stepping else event.ydata)
if self.cuts._change(self.curline,newval):
self.fig.canvas.draw()
def togglestepping(self, event):
self.stepping = not self.stepping
def connect(self,figure,ax):
'Connect to figure'
self.fig = figure
self.ax = ax
self.cidpress = figure.canvas.mpl_connect('button_press_event', self.on_press)
self.cidrelease = figure.canvas.mpl_connect('button_release_event', self.on_release)
self.cidmotion = figure.canvas.mpl_connect('motion_notify_event', self.on_motion)
def disconnect(self):
'Disconnect all the stored connection ids'
self.rect.figure.canvas.mpl_disconnect(self.cidpress)
self.rect.figure.canvas.mpl_disconnect(self.cidrelease)
self.rect.figure.canvas.mpl_disconnect(self.cidmotion)
def abovewhich(arr,val):
listoflocs = np.append(np.where(arr>val)[0]-1,-1)
return listoflocs[0]
def closest(arr,val):
dist = np.abs(arr-val)
return np.where(dist==min(dist))[0][0]
def readcsv(name):
'Canvas only'
students = pd.read_csv(name, index_col=3, skiprows=[1]) # Making some assumptions about the Canvas format
students['Student']
names = students['Student'].str.title().str.split(', ').apply(pd.Series)
student_list = pd.DataFrame()
student_list['First'] = names[1]
student_list['Last'] = names[0]
student_list['Score'] = students['Final Score']
classes = students['Section'].str.extract(r'(\d{5})')
student_list['Class'] = classes.apply(int) # This fails on older pandas version (works on 0.15)
student_list['Grades'] = ''
return student_list
def predict_cuts(arr):
av = arr.mean()
std = arr.std()
mini = std*.15
std = max(std,3)
mini = max(mini,1)
ab, bc, cd, df = av+std*.5, av-std*.5, av-std*1.5, av-std*2.5
return np.array([df,df+mini,cd-mini,cd,cd+mini,bc-mini,bc,bc+mini,ab-mini,ab,ab+mini])
def ask_filenames():
filetypes = [
('Allfiles','*'),
('Canvas grade file','*.csv'),
]
if platform.system() != 'Windows':
filetypes.append(filetypes.pop(0)) # Reorder so the all gradefiles is selected first
root = Tk()
root.withdraw()
filenames = askopenfilenames(filetypes=filetypes,
title='Select a file or files to open')
root.destroy()
# Fix for broken Windows Tk
if isinstance(filenames,string_types):
filenames_extra = re.findall('\{(.*?)\}',filenames)
filenames = re.sub('\{(.*?)\}','',filenames)
filenames = filenames.split() + filenames_extra
return filenames
if __name__ == '__main__':
if len(sys.argv) > 1:
filenames = sys.argv[1:]
else:
filenames = ask_filenames()
if filenames:
student_list = pd.concat(readcsv(name) for name in filenames)
cuts = Cutoffs(student_list)
cuts.plot()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.