diff options
| -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))
 | 
