summaryrefslogtreecommitdiff
path: root/analyze.py
diff options
context:
space:
mode:
authorHui Lan <lanhui@zjnu.edu.cn>2020-03-10 12:22:52 +0800
committerHui Lan <lanhui@zjnu.edu.cn>2020-03-10 12:22:52 +0800
commit58c3198448aec1245f5480fed8b3e6308a0bdf6b (patch)
treefe17352fd1f8c3f60c8c8501d68e5a153c837282 /analyze.py
Intial commit add two files
Diffstat (limited to 'analyze.py')
-rw-r--r--analyze.py266
1 files changed, 266 insertions, 0 deletions
diff --git a/analyze.py b/analyze.py
new file mode 100644
index 0000000..ce1e7f3
--- /dev/null
+++ b/analyze.py
@@ -0,0 +1,266 @@
+# 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: 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.
+#
+#
+# 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 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
+_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 = {}
+ 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()
+
+
+# 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)
+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)
+
+# 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
+
+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))