From 58c3198448aec1245f5480fed8b3e6308a0bdf6b Mon Sep 17 00:00:00 2001 From: Hui Lan Date: Tue, 10 Mar 2020 12:22:52 +0800 Subject: Intial commit add two files --- analyze.py | 266 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 analyze.py (limited to 'analyze.py') 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 + +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)) -- cgit v1.2.1