diff options
| -rw-r--r-- | analyze.py | 266 | ||||
| -rw-r--r-- | tasks.json | 57 | 
2 files changed, 323 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))
 diff --git a/tasks.json b/tasks.json new file mode 100644 index 0000000..6ce45d5 --- /dev/null +++ b/tasks.json @@ -0,0 +1,57 @@ +{ +    "course.objectives":["co1", "co2", "co3", "co4", "co5"], +    "tasks":{ +	"quiz1":{ +	    "co4":2 +	}, +	"quiz2":{ +	    "co1":2 +	}, +	"quiz3":{ +	    "co2":1, +	    "co5":1	     +	}, +	"quiz4":{ +	    "co1":2 +	}, +	"quiz5":{ +	    "co5":2 +	}, +	"quiz6":{ +	    "co5":2 +	}, +	"quiz7":{ +	    "co3":3 +	}, +	"project.form.group":{ +	    "co5":3 +	}, +	"project.decide.areas":{ +	    "co1":2 +	}, +	"project.use.tool":{ +	    "co4":5 +	}, +	"project.submit.commits":{ +	    "co4":10 +	}, +	"project.submit.story":{ +	    "co4":5 +	},	 +	"final.exam.q1":{ +	    "co1":12 +	}, +	"final.exam.q2":{ +	    "co2":12	     +	}, +	"final.exam.q3":{ +	    "co3":12 +	}, +	"final.exam.q4":{ +	    "co4":12 +	}, +	"final.exam.q5":{ +	    "co5":12 +	}	 +    } +} | 
