From cd2723525e97e78dadcc1d3cf68aec2395b4863b Mon Sep 17 00:00:00 2001 From: Lan Hui Date: Mon, 29 Jan 2024 19:14:29 +0800 Subject: Use CSV format than TSV format. Let the result file name end with .csv rather than .xls, so it is easier to open. --- analyze.py | 170 +++++++++++++++++++++++-------------------------------------- 1 file changed, 64 insertions(+), 106 deletions(-) diff --git a/analyze.py b/analyze.py index e657ce5..9956e1a 100644 --- a/analyze.py +++ b/analyze.py @@ -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 -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)) -- cgit v1.2.1