diff options
Diffstat (limited to 'analyze.py')
-rw-r--r-- | analyze.py | 170 |
1 files changed, 64 insertions, 106 deletions
@@ -1,10 +1,12 @@ -# Purpose: to compute course objective fulfillment percentage.
+# Purpose: 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: python3 analyze.py
+# Usage: python analyze.py
+# OR
+# python3 analyze.py
#
# Required files:
#
@@ -20,7 +22,7 @@ #
#
# 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
@@ -40,13 +42,11 @@ # 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 Lan Hui
+# Copyright (C) 2019, 2020, 2021, 2023, 2024 Lan Hui
#
# Contact the author if you encounter any problems: Lan Hui <lanhui@zjnu.edu.cn>
-import json
-import os
-import sys
+import json, os, sys
# Solve UnicodeDecodeError - https://blog.csdn.net/blmoistawinde/article/details/87717065
import _locale
@@ -55,55 +55,29 @@ _locale._getdefaultlocale = (lambda *args: ['zh_CN', 'utf8']) def get_task_information(fname):
- '''
- Return a dictionary in the following form:
-
- {
- "course.objectives": [
- "co1",
- "co2",
- "co3"
- ],
- "tasks": {
- "lab": {
- "co1": 30
- },
- "project": {
- "co2": 20
- },
- "exam": {
- "co3": 50
- }
- }
- }
- '''
with open(fname) as json_data:
d = json.load(json_data)
return d
def get_student_information(fname):
- '''
- Return a list of tuples where each tuple contains (student_number, student_name)
- '''
result = []
- lineno = 1
+
with open(fname) as f:
for line in f:
line = line.strip()
lst = line.split('\t')
- if len(lst) == 2:
- result.append((lst[0], lst[1]))
- else:
- print('Warning: Line %d in file %s does not have two columns. Skipped!' % (lineno, fname))
- lineno += 1
+ 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 (co's) associated to that task.
+ course objectives.
'''
result = 0
for k in d:
@@ -112,7 +86,6 @@ def get_max_score(d): def get_student_number(fname):
- '''Return a dictionary where the keys are student numbers, and values are the scores for that task'''
d = {}
# If a file has BOM (Byte Order Marker) charater, stop.
@@ -121,14 +94,14 @@ def get_student_number(fname): 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
+ d[sno] = lst[2] # score
f.close()
return d
@@ -136,7 +109,6 @@ def get_student_number(fname): def make_individual_grade_files(grade_dir, task_dict, student_lst):
if not os.path.exists(grade_dir):
os.mkdir(grade_dir)
- # Create a grade file for each task. If the task is exam, then exam.txt will be created in folder grade_dir
for task in task_dict['tasks']:
fname = task + '.txt'
max_score = get_max_score(task_dict['tasks'][task])
@@ -162,18 +134,18 @@ def make_individual_grade_files(grade_dir, task_dict, 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))
+ 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 = 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')
@@ -182,17 +154,10 @@ def make_individual_grade_files(grade_dir, task_dict, student_lst): 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()
+ f.close()
def make_score_dict(fname):
- '''Return a dictionary in the following form:
- {
- "201930220235": "30",
- "201930220320": "15"
- },
- where the key is a student number, and the value is the student's score for that task
- '''
d = {}
f = open(fname)
lines = f.readlines()
@@ -208,13 +173,6 @@ def make_score_dict(fname): def get_scores_for_each_student(grade_dir, task_dict):
- '''
- Return a dictionary where each key is a task, and each value is
- a dictionary of students and their scores for each task. Example:
- {'lab': {'201930220235': '30', '201930220320': '15'},
- 'project': {'201930220235': '20', '201930220320': '10'},
- 'exam': {'201930220235': '50', '201930220320': '25'}}
- '''
d = {}
for task in task_dict['tasks']:
fname = task + '.txt'
@@ -229,7 +187,7 @@ def get_scores_for_each_student(grade_dir, task_dict): def get_objective_total(d):
- ''' For each objective, get its total by summing over all tasks.'''
+ ''' For each objective, get its total by summing all tasks.'''
objective_lst = d['course.objectives']
result = []
check_sum = 0
@@ -242,12 +200,12 @@ def get_objective_total(d): 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 tasks correctly.' % (check_sum))
+ 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), ...]
+ return result # [(objective1, value1), (objective2, value2), ...]
-def check_file_availability(fname):
+def check_availability(fname):
if not os.path.exists(fname):
print('The required file %s does not exist.' % (fname))
sys.exit()
@@ -275,19 +233,18 @@ def all_gone(student_lst, lst): return True
+
# main
GRADE_DIR = 'grade'
-GRADE_FILE = 'grade_file.xls' # output
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'
-check_file_availability(TASK_FILE)
-check_file_availability(STUDENT_FILE)
-
-# Make software banner
-software_information = 'Course Objective Fulfillment Calculator\nCopyright (C) 2019,2020,2022 Lan Hui (lanhui@zjnu.edu.cn)'
+GRADE_FILE = 'grade_file.csv' # output
+software_information = 'Course Objective Fulfillment Calculator\nCopyright (C) 2019, 2020, 2021, 2023 Lan Hui (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)
+banner = '%s\n%s\n%s' % ('-'*n, software_information, '-'*n)
print(banner)
task_dict = get_task_information(TASK_FILE)
@@ -295,19 +252,20 @@ 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
+# 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' # for a summary grade file
+file_content = ', '.join(head_lst) + ', Total\n'
score_dict = get_scores_for_each_student(GRADE_DIR, task_dict)
-print(json.dumps(task_dict, indent=4))
-print(json.dumps(score_dict, indent=4))
+course_object_cumulative_score = {}
+for co in task_dict['course.objectives']:
+ course_object_cumulative_score[co] = 0
-# Exclude some students from the student list student_lst. The excluded students are specified in a file called exclude.txt.
+# 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))
@@ -318,51 +276,51 @@ if os.path.exists(EXCLUDE_FILE): if sno != '':
lst.append(sno)
for s in lst:
- print('Ignore student %s' % (s))
+ print('Do not count %s' % (s))
remove_a_student(student_lst, s)
- assert all_gone(student_lst, lst)
+ 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
-course_object_cumulative_score = {}
-for co in task_dict['course.objectives']:
- course_object_cumulative_score[co] = 0
-
for s in student_lst:
sno = s[0]
sname = s[1]
- file_content += '%s\t%s' % (sno, sname)
- total = 0 # total score for this student
+ result = '%s\t%s\n' % (sno, sname)
+ file_content += '%s, %s' % (sno, sname)
+ total = 0
for task in task_lst:
score = score_dict[task][sno]
- max_task_score = get_max_score(task_dict['tasks'][task])
- if float(score) > max_task_score:
- print('Error: student %s\'s score greater than maximum score in task %s' % (sno + '_' + sname, task))
+ 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:
- file_content += '\t%s' % (score)
+ result += ' %s:%s\t' % (task, score)
+ file_content += ', %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] / max_task_score)
+ my_share = 1.0*float(score) * task_dict['tasks'][task][co] / total_score
course_object_cumulative_score[co] += my_share
- file_content += '\t%4.1f\n' % (total)
-
-
-# Make the summary grade file
-with open(GRADE_FILE, 'w', encoding='utf-8') as f:
- f.write(file_content)
- print('Check the spreadsheet %s.' % (GRADE_FILE))
+ result += ' [%4.1f] ' % ( my_share )
+ else:
+ result += ' [%4.1f] ' % (0)
+ result += '\n'
+ result += ' ---\n Total:%4.1f\n' % (total)
+ file_content += ', %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] # name of the course objective
- value = x[1] # the associated total value of that course objective
- try:
- percentage = 100 * course_object_cumulative_score[co] / (value * num_student)
- print('Course objective %s is %.0f%% satisfied.' % (co, percentage))
- except:
- print('Error: value = %4.1f, num_student = %4.1f' % (value, num_student))
+ 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))
|