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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
|
# Purpose: to compute course objective fulfillment percentage.
#
# Rationale: it is tedious to compute course objective fulfillment
# percentage, which is a measurement of students'
# performance in a course.
#
# Usage: python analyze.py
#
# Required files:
#
# - students.txt: a plain text file where each row contains student number and student name, separated by a TAB#
# - tasks.json: a plain text file in JSON format specifying the course objectives and tasks in this course.
# Each task is associated to at least one course objective (co). Each co in a task has an associated weight.
# Make sure that all weights across tasks sum to 100.
# Optional files:
#
# - exclude.txt: a plain text file specifying a list of student numbers (one per line) which we do not want to include
# while computing the course objective fulfillment percentage.
#
#
#
# Limitations:
#
# For simplicity, I recommend associating one and only one co to each
# task, whether the task is an assignment, a quiz, a lab, or a test
# question. In the case of having more than one co's in a task, a
# student's score in this task will be proportionally distributed
# among these co's according the co's weights. This treatment is a
# simplification, since in reality two students with the same task
# score may have a different score distribution among different co's.
# For example, a task is associated to two course objectives, co1 and
# co2, with weights 2 and 2, respectively. Student A achieved a score
# of 2 in this task, with 2 in co1 and 0 in co2. Student B achieved a
# same score in this task, but with 0 in co1 and 2 in co2. This
# software does not consider this difference. It will assign 1 to co1
# and 1 to co2, for both student A and student B. That is why I
# suggest using a single course objective for each task, to avoid this
# simplification. Having more than one course objective in a task
# might indicate that this task's goal is not specific enough. You
# could break down the task into several sub-tasks such that each
# sub-task corresponds to only one course objective.
#
# Copyright (C) 2019, 2020, 2021 Hui Lan
#
# Contact the author if you encounter any problems: Hui Lan <lanhui@zjnu.edu.cn>
import json, os, sys
# Solve UnicodeDecodeError - https://blog.csdn.net/blmoistawinde/article/details/87717065
import _locale
import codecs
_locale._getdefaultlocale = (lambda *args: ['zh_CN', 'utf8'])
def get_task_information(fname):
with open(fname) as json_data:
d = json.load(json_data)
return d
def get_student_information(fname):
result = []
with open(fname) as f:
for line in f:
line = line.strip()
lst = line.split('\t')
if len(lst) != 2:
print('File %s does not have two columns.' % (fname))
sys.exit()
result.append((lst[0], lst[1]))
return result
def get_max_score(d):
'''
Return the maximum allowable score in a task. The maximum score is the sum of all values across all
course objectives.
'''
result = 0
for k in d:
result += int(d[k])
return result
def get_student_number(fname):
d = {}
# If a file has BOM (Byte Order Marker) charater, stop.
with open(fname, 'r+b') as f:
s = f.read()
if s.startswith(codecs.BOM_UTF8):
print('\nERROR: The file %s contains BOM character. Remove that first.' % (fname))
sys.exit()
f = open(fname)
for line in f:
line = line.strip()
if not line.startswith('#'):
lst = line.split('\t')
sno = lst[0] # student number
d[sno] = lst[2] # score
f.close()
return d
def make_individual_grade_files(grade_dir, task_dict, student_lst):
if not os.path.exists(grade_dir):
os.mkdir(grade_dir)
for task in task_dict['tasks']:
fname = task + '.txt'
max_score = get_max_score(task_dict['tasks'][task])
new_file = os.path.join(grade_dir, fname)
if not os.path.exists(new_file):
print('Create file %s. You need to fill out students\'s scores in this file.' % (new_file))
f = open(new_file, 'w')
f.write('#' + task + '\n')
f.write('\t'.join(['#student.no', 'student.name', 'score']) + '\n')
for student in student_lst:
f.write('\t'.join([student[0], student[1], '0']) + '\n')
f.close()
else:
inconsistency = 0
d = get_student_number(new_file)
# students.txt could be updated late ... an undesired event. Check for this event.
for student in student_lst:
sno = student[0]
if not sno in d:
inconsistency = 1
print('Warning: %s is in the new student file, but is not in the old grade file.' % (sno))
student_numbers = [student[0] for student in student_lst]
for sno in d:
if not sno in student_numbers:
inconsistency = 1
print('Warning: %s is in the old grade file, but is not in the new student file.' % (sno))
if inconsistency == 1:
print('Warning: I am keeping the available scores.')
f = open(new_file)
s = f.read()
f.close()
f = open(new_file + '.old', 'w')
f.write(s)
f.close()
f = open(new_file, 'w')
f.write('#' + task + '\n')
f.write('\t'.join(['#student.no', 'student.name', 'score']) + '\n')
for student in student_lst:
if student[0] in d:
f.write('\t'.join([student[0], student[1], '%s' % d[student[0]]]) + '\n')
else:
f.write('\t'.join([student[0], student[1], '0']) + '\n')
f.close()
def make_score_dict(fname):
d = {}
f = open(fname)
lines = f.readlines()
f.close()
for line in lines:
line = line.strip()
if not line.startswith('#'):
lst = line.split('\t')
sno = lst[0]
score = lst[2]
d[sno] = score
return d
def get_scores_for_each_student(grade_dir, task_dict):
d = {}
for task in task_dict['tasks']:
fname = task + '.txt'
max_score = get_max_score(task_dict['tasks'][task])
new_file = os.path.join(grade_dir, fname)
if os.path.exists(new_file):
d[task] = make_score_dict(new_file)
else:
print('Warning: grade file %s not found.' % (new_file))
return d
def get_objective_total(d):
''' For each objective, get its total by summing all tasks.'''
objective_lst = d['course.objectives']
result = []
check_sum = 0
for o in objective_lst:
total = 0
for task in d['tasks']:
for co in d['tasks'][task]:
if co == o:
total += d['tasks'][task][co]
check_sum += total
result.append((o, total))
if check_sum != 100:
print('Objective total is not 100 (%d instead). Make sure you have divide the objective scores across task correctly.' % (check_sum))
sys.exit()
return result # [(objective1, value1), (objective2, value2), ...]
def check_availability(fname):
if not os.path.exists(fname):
print('The required file %s does not exist.' % (fname))
sys.exit()
def remove_a_student(student_lst, s):
''' student_lst is a list of tuples, each tuple in the form like (student_no, student_name).
s is a student number. Effect: student_lst shortened.
'''
index = 0
for x in student_lst:
sno = x[0]
if sno == s:
student_lst.pop(index)
return None
index += 1
def all_gone(student_lst, lst):
''' Return True iff none of the student number in lst is in student_lst'''
for x in student_lst:
sno = x[0]
if sno in lst:
return False
return True
# main
GRADE_DIR = 'grade'
TASK_FILE = 'tasks.json' # required file containing course objectives and tasks.
check_availability(TASK_FILE)
STUDENT_FILE = 'students.txt' # required file containing student numbers and student names.
check_availability(STUDENT_FILE)
EXCLUDE_FILE = 'exclude.txt'
GRADE_FILE = 'grade_file.xls' # output
software_information = 'Course Objective Fulfillment Calculator\nCopyright (C) 2019 Hui Lan (lanhui@zjnu.edu.cn)'
n = max([len(s) for s in software_information.split('\n')])
banner = '%s\n%s\n%s' % ('-'*n, software_information, '-'*n)
print(banner)
task_dict = get_task_information(TASK_FILE)
student_lst = get_student_information(STUDENT_FILE)
make_individual_grade_files(GRADE_DIR, task_dict, student_lst)
print('Seen %d students.' % (len(student_lst)))
# output results to terminal and file
head_lst = ['student.no', 'student.name']
task_lst = sorted(task_dict['tasks'])
head_lst.extend(task_lst)
file_content = '\t'.join(head_lst) + '\tTotal\n'
score_dict = get_scores_for_each_student(GRADE_DIR, task_dict)
course_object_cumulative_score = {}
for co in task_dict['course.objectives']:
course_object_cumulative_score[co] = 0
# Exclude some students from student list student_lst. The excluded students are specified in a file called exclude.txt.
if os.path.exists(EXCLUDE_FILE):
lst = []
print('Read excluded students from %s.' % (EXCLUDE_FILE))
with open(EXCLUDE_FILE) as f:
for line in f:
line = line.strip()
sno = line.split('\t')[0]
if sno != '':
lst.append(sno)
for s in lst:
print('Do not count %s' % (s))
remove_a_student(student_lst, s)
assert all_gone(student_lst, lst) == True
print('Still have %d students after excluding the undesired students specified in %s.' % (len(student_lst), EXCLUDE_FILE))
# Do statistics
for s in student_lst:
sno = s[0]
sname = s[1]
result = '%s\t%s\n' % (sno, sname)
file_content += '%s\t%s' % (sno, sname)
total = 0
for task in task_lst:
score = score_dict[task][sno]
total_score = get_max_score(task_dict['tasks'][task])
if float(score) > total_score:
print('Warning: student %s\'s score greater than maximum score in task %s' % (sno + '_' + sname, task))
sys.exit()
else:
result += ' %s:%s\t' % (task, score)
file_content += '\t%s' % (score)
total += float(score)
for co in task_dict['course.objectives']:
if co in task_dict['tasks'][task]:
my_share = 1.0*float(score) * task_dict['tasks'][task][co] / total_score
course_object_cumulative_score[co] += my_share
result += ' [%4.1f] ' % ( my_share )
else:
result += ' [%4.1f] ' % (0)
result += '\n'
result += ' ---\n Total:%4.1f\n' % (total)
file_content += '\t%4.1f\n' % (total)
#print(result)
f = open(GRADE_FILE, 'w')
f.write(file_content)
f.close()
print('Check spreadsheet %s.' % (GRADE_FILE))
objective_total = get_objective_total(task_dict)
num_student = len(student_lst)
for x in objective_total:
co = x[0]
value = x[1]
percentage = 100 * course_object_cumulative_score[co]/(value * num_student)
print('Course objective %s is %.0f%% satisfied.' % (co, percentage))
|