# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python [conda env:mypy36]
#     language: python
#     name: conda-env-mypy36-py
# ---

# # Automatic Lab Evaluator
#
# ## Assessment based on student-provided results
#
# * <NAME>
# * <NAME>

# Version History:
#
#     Version 0.1 (Dec. 2016)
#         - Firts python 2 version and python 3 adaptation
#     Version 0.2 (Dec. 2017) 
#         - All configurable parameters in the first and second code cell.
#         - Managing multiple mat files in students' zip files.
#         - Corrected bug in readdatafiles (new student variables were not properly added to the dataframe)
#         - Managing multiple class lists in Spanish and English.
#         - External evaluation functions
#         - New format of students report.
#         - Uses ExamProject class.
#         - Integrated student groups

# +
import numpy as np
import pandas as pd
import os
from os.path import isfile, join
import scipy.io as sio
import scipy
import zipfile as zp
import shutil
import difflib
import csv
import glob

# Evaluation libraries
from lib.dbEvaluatorB12 import *
from lib.dbSolverB12 import *
# -

# ## 1. Configurable parameters:

# +
# #########
# Libraries
# #########

# Use the inherited examProject class corresponding to the exam to be evaluated.
import lib.examProjectB3 as ex
import lib.examProjectB3 as ex

# #################
# Files and folders
# #################

# Project path
# project_path = '../LabEvaluationProjects/ProjectB3_1718_GSCGT/'
# project_path = '../LabEvaluationProjects/ProjectB3_1718/'
project_path = 'prb12'

# Exam_name. An exam evaluation project main contain several exams. Write here which one of them you want to evaluate.
# exam_label = 'ExLabB12_0'
exam_label = 'ExLabB12_1'

# Expected name of the students' results file. 
# This is used to disambiguate situations where the student uploaded multiple mat files
# (e.g. the input data file provided with the exam statement, or .mat files in .DS_STORE folders)
results_fname = 'results.mat'

# Output file name with
finalnotes_fname = 'student_notes.xlsx'

# ####
# Exam
# ####

# Penalties:
p_nocode = 0.75
p_noresults = 0.75
p_delay = 0.25      # score reduction per minute.

exam = ex.ExamProjectB3(project_path)
exam.load()

# Paths to input and output files
class_list_path = exam.f_struct['class_list']
all_students_path = exam.f_struct['all_students']
data4st_path = exam.f_struct['data4students']
results_path = exam.f_struct['student_results'] + exam_label + '/'
output_path = exam.f_struct['eval_results'] + exam_label + '/'
csv_questions_list = exam.f_struct['exam_statement'] + exam_label + '/' + exam_label + '.csv'

# +
# List of exam questions from the database
print(csv_questions_list)
with open(csv_questions_list, 'r') as f:
    reader = csv.reader(f)
    questions = list(reader)[0]

# If the fils os not available, you can write the list os questions by hand 
# questions = ['F0_estimate_06', 'F1_model_01', 'F2_predict_03', 'F4_lms_02']
print("Questions in the exam: {0}".format(questions))


# -

# ## 2. Read datafiles for all students
#
# Student datafiles can be in any of the following formats:
#
#    * `'.zip'`: When uncompressed, the zip may contain one or several matlab files. All matlab files are read and incorporated to a pandas Dataframe where each student is a column, and each index is a variable available for the exam solution
#    * `'.mat'`: All data variables for the students are given in a single matlab file

# +
def getFileName(fpath):
    return fpath.split('/')[-1]

def readData4st(datafiles_path):
    '''
    This function is used for reading the matlab data files provided to students 
    '''

    # Read matlab files in the input directory tree
    datafiles = glob.glob(datafiles_path + '**/*.mat', recursive=True)

    df = pd.DataFrame()
    
    # Read files
    print('Processing {0} files in {1} ...'.format(len(datafiles), datafiles_path))
    for dtfile in sorted(datafiles):
        
        # The tag can be the NIA, the student's name or just the begining of some other file
        tag = getFileName(dtfile).split('.')[0]

        # Load matlab data file
        data = sio.loadmat(dtfile, squeeze_me=True)
            
        # Read all variable names and the corresponding data values
        idx = []
        val = []
        for var in [el for el in data.keys() if not el.startswith('_')]:
            idx.append(var)
            val.append(data[var])
          
        # Add to dataframe
        df2 = pd.DataFrame()
        df2[tag] = pd.Series(val, index = idx)
        df = pd.concat([df, df2], axis=1)
        df.sort_index(axis=1, inplace=True)
    return df


# +
# Read students' data.
print(data4st_path)
student_data = readData4st(data4st_path)

print('')
print('Number of students in dataframe:', str(student_data.shape[1]))
print('Number of variables read:', str(student_data.shape[0]))

print('Displaying data for first students ... ')
student_data[student_data.columns[:7]]

student_data['100318675']

# -

# ## 2. Read answers provided by students

def readdatafiles(datafiles_path, splitsymbol):
    '''
    This function is used for reading both the data files provided to students and the response
    files provided by students
    '''

    # Read file paths
    datafiles = glob.glob(datafiles_path + '**/*.*', recursive=True)
    # datafiles = [f for f in os.listdir(datafiles_path) if isfile(join(datafiles_path, f))]

    temporary_dir = './tmp'
    df = pd.DataFrame()
    
    # Read files
    print('Processing {0} files in {1} ...'.format(len(datafiles), datafiles_path))
    for dtfile in sorted(datafiles):
        idx = []
        val = []
        makedf = True      # This is a default flag. If it remains True, a new column will be added to the df

        # The tag can be the NIA, the student's name or just the begining of some other file
        tag = getFileName(dtfile).split(splitsymbol)[0]
        # tag = dtfile.split(splitsymbol)[0]

        if dtfile.endswith('.zip'):
            
            # Read names of .mat files
            zpobj = zp.ZipFile(dtfile)            
            # zpobj = zp.ZipFile(join(datafiles_path, dtfile))            
            mat_fnames = [f for f in zpobj.namelist() if f.endswith('mat')]
            
            # mat file selection. This is to disambiguate cases with multiple files
            n = len(mat_fnames)
            if n == 0:
                print ('    WARNING: {} has not delivered any mat file'.format(tag))
                fname = None
            else:
                if n > 1:
                    print('    WARNING: {} has provided multiple mat files:'.format(tag))
                    print('        {0}'.format(mat_fnames))                  

                # Define a nested set of criteria to select a single mat file form multiple options:
                criteria = [mat_fnames]
                criteria.append([f for f in criteria[0] if '.ipynb_checkpoints' not in f])
                criteria.append([f for f in criteria[1] if f[0].isalnum()])
                criteria.append([f for f in criteria[2] if getFileName(f)[0].isalnum()])
                criteria.append([f for f in criteria[3] if getFileName(f)[0].isalpha()])
                criteria.append([f for f in criteria[4] if f.endswith(results_fname)])

                # Selecte the file according to the most restrictive criterium with non empty members.
                for c in reversed(criteria):
                    if len(c) > 0:
                        # We take the first file in the list (an arbitrary choice)
                        fname = c[0]
                        break
                if n > 1:
                    print('        Selected file: {}'.format(fname))

            # Read the selected mat file, if any
            if fname is not None:
                # Matlab files are extracted to a temporal subfolder
                zpobj.extract(fname, temporary_dir)
                data = sio.loadmat(join(temporary_dir, fname), squeeze_me=True)
                
                # Read all variable names and the corresponding data values
                for var in [el for el in data.keys() if not el.startswith('_')]:
                    idx.append(var)
                    val.append(data[var])

            # Remove temporary directory, if it has been created
            if os.path.exists(temporary_dir):
                shutil.rmtree(temporary_dir)

        elif dtfile.endswith('.mat'):

            # This block of code was removed from the original notebook.
            # I have rescued it from another notebook
            # data = sio.loadmat(join(datafiles_path, dtfile), squeeze_me=True)
            data = sio.loadmat(dtfile, squeeze_me=True)
            
            # Read all variable names and the corresponding data values
            for var in [el for el in data.keys() if not el.startswith('_')]:
                idx.append(var)
                val.append(data[var])

        elif dtfile.endswith('m') or dtfile.endswith('py') or dtfile.endswith('.ipynb'):
            print('    WARNING: {} has provided a code file only:'.format(tag))
            print('        {0}'.format(dtfile))
        else:
            makedf = False
            if os.path.isfile(dtfile):
                print('    File ignored: {0}'.format(dtfile))
            
        if makedf:
            df2 = pd.DataFrame()
            df2[tag] = pd.Series(val, index = idx)
            df = pd.concat([df, df2], axis=1)
            df.sort_index(axis=1, inplace=True)
    return df
        


# ### 2.1. Requested variable names.
#
# In order to get the names of the requested variables, we solve the exam with an arbitrary set of variables.

# +
data = student_data[student_data.columns[0]].to_dict()
print(questions)
solution, scoring_ex = solveExam(questions, data)
truenames = list(solution.keys())

print(truenames)
# -

# ### 2.2. Read student results into panda dataframe

# +
# Read student results
student_results = readdatafiles(results_path, splitsymbol='_')

# Build a set of indices containing the expected variable names and all other variables provided by students
newindex = truenames + [el for el in student_results.index.tolist() if el not in truenames]

student_results = student_results.reindex(newindex)

print('')
print('Number of students in dataframe:', str(student_results.shape[1]))
print('Number of variables read:', str(student_results.shape[0]))

print('Displaying data for first students ... ')
student_results[student_results.columns[0:7]]
# -

# ### 2.3. Common Mistakes on variable names
#
# In view of all variable names provided by all students, we may decide to allow alternative names for variables without any penalty

# +
# print(student_results)

# +
print('Number of students in dataframe:', str(student_results.shape[1]))

print('\nDisplaying number of missing data per variable name.')
print('Those with a large number are potential common mistakes for a variable name')

student_results.isnull().sum(axis=1)

# +
###########################################
# EXAM DEPENDENT VARIABLE

#Dictionary with accepted mistakes in the following format
#  Expected variable name : Accepted mistake
if exam_label == 'ExLabB12_0':
    Mistakes = {'xnVal': 'xnTest', 'wp': 'w', 'EAPval':'EAP'}
elif exam_label == 'ExLabB12_1':
    Mistakes = {'sEst': 'sTest',
                'xnTest': 'xnTest.mat', 'xnVal': 'xnTest',
                'we': 'w2', 'w5': 'we', 
                'w': 'w1',
                'PFAx1': 'PFAX1',
                'uNP': 'etaNPX1', 'uNP': 'etanNPx1', 'etaNPx1': 'uNP'}
elif exam_label == 'ExLabB12_2':
    Mistakes = {'xnTest': 'xnTest.mat', 'xmVal': 'xnTest',
                'xnTrain': 'xnTrain.mat', 'xmTrain': 'xnTest',
                'we': 'we3', 
                'w3': 'we4',
                'm0': 'mo'}
    
##########################################

# Fill and empty variable by the value of its accepted mistake.
for el in Mistakes:    
    print(el)
    # The following 'if is necessary because some of the mistakes in the dictionary may not happen.
    if Mistakes[el] in student_results.index.tolist():
        # print(student_results.loc[Mistakes[el]])
        student_results.loc[el] = student_results.loc[el].fillna(student_results.loc[Mistakes[el]])

# Remove rows with the wrong variables.
for el in student_results.index.tolist():
    if el not in truenames:
        student_results.drop(el, inplace=True)
        
student_results.head(40)
# -

# ### 2.4. Name to NIA dictionary
#
# Finally, since datafiles are created by NIA and results are available per student name, we need to create a dictionary connecting them.
#
# Student names are taken from one or several student lists. Using multiple list is useful when the same exam is stated to multiple groups, or in the frequent situation where students from one group carry out the exam of another group.

# +
# Select xls file names in the class list folder
print("Reading class lists...")
xls_files = [f for f in os.listdir(class_list_path) if f.endswith('.xls') or f.endswith('.xlsx')]
if len(xls_files) > 1:
    print("    There are {} excel files in the class_list folder.".format(len(xls_files)))
    print("    All students will be merged in a single list.")

# Load all xls files into dataframes
groups = []
for g in xls_files:
    df = pd.read_excel(class_list_path + g)
    # Translate column names form Spanish to English.
    # This is required to concatenate student lists in different languages.
    df.rename(columns={'Dirección de correo': 'Email address',
                       'Apellido(s)': 'Surname', 
                       'Nombre': 'First name'}, inplace=True)
    groups.append(df)

# Concatenate class lists (we do not expect duplicated NIU's in different lists)
student_NIA_names = pd.concat(groups)
print("Done. {0} students in the lists".format(len(student_NIA_names)))
student_NIA_names.sort_values('Surname')     #.head()

# +
# UTF-8 encoding of everything
# AFAIK, this is no longer needed in Python 3, but I left it just in case...
for fld in student_NIA_names.keys():
    if fld != 'NIU':
        student_NIA_names[fld] = student_NIA_names[fld].str.encode('utf8')

# Build dictionary NIA: name
NIA_name = {}
for el in student_results.columns.tolist():

    # Find the student name in student_NIA_names that is most similar to el
    sim_list = []
    for idx, NIA in enumerate(student_NIA_names['NIU'].values):
        std_name = str(student_NIA_names['First name'].values.tolist()[idx]) + ' ' + \
                   str(student_NIA_names['Surname'].values.tolist()[idx])
        sim_list.append(difflib.SequenceMatcher(a=el.lower(), b=std_name.lower()).ratio())

    max_sim = max(sim_list)
    max_idx = sim_list.index(max_sim)
    NIA_name[student_NIA_names['NIU'].values.tolist()[max_idx]] = el

# Build reverse dictionary name: NIA
name_NIA = {NIA_name[el]: el for el in NIA_name}
# -

# ### 2.5. Group of each student

# We will include the information about the group in the final dataframe of results so as to make the separation of evaluation reports easier.

NIA_group = pd.read_csv(all_students_path)[['NIA', 'group']]
NIA_group.sort_values(['NIA']).head()

# At this point we have:
#
#    * student_data: dataframe with data given to the students. Each index is a variable, and each column a NIA
#    * student_results: dataframe with student results. Each index is a variable, and each column a name
#    * NIA_name: NIA to name dictionary
#    * name_NIA: name to NIA dictionary
#    * NIA_group: dataframe

# ## 3. Exam evaluation
#
# To carry out the evaluation of the exam, we use the external evaluation libraries.
#
# Function evaluateExam computes the correct solutions for the given data and compares them with the responses provided by the students.

# +
df = pd.DataFrame()

print('Evaluating all students... ')
for NIA in NIA_name:

    name = NIA_name[NIA]
    print('Evaluating {0} {1} ...'.format(NIA, name))

    # Evaluate the exam from the data provided to the student and the student response
    dataex = student_data[str(NIA)].to_dict()
    response = student_results[name].to_dict()
    exam_report = evaluateExam(questions, dataex, response)

    # Convert exam_report, which is a nested dictionary, into a pandas dataframe
    # Note that all this conversion to and from dictionaries can be avoided if evaluateExam 
    # worked with dataframes. This is a pending task.
    ex = {}
    # Note that we take the last 2 characters of the group name only.
    ex[('', 'Group')] = NIA_group[NIA_group['NIA'] == NIA]['group'].tolist()[0][-2:]   
    for v  in exam_report:
        for w in exam_report[v]:
            ex[(v,w)] = exam_report[v][w]
    
    df[NIA_name[NIA]] = pd.Series(ex)

# Take the transpose to place students in rows, and restate the original variable ordering
# This is because pd.Series does not preserve the order.
cols = list(ex.keys())
df = df.T[cols]

# Pretty print results
df[df.columns[:]].head(100)


# -

# ### 3.1. Penalties
#
# In addition to the evaluation of the results file provided by the student, the final mark depends on other factors:
#
# 1. If the student uploaded the code files
# 2. Delays in delivering the files during the exam.
# 3. Errors in the delivering process (use of e-mail, incorrect file types, etc).
#
# The following function is used to identify the code uploaded by the student.

def detectCode(datafiles_path, splitsymbol):
    '''
    This function is used to check if the student has uploaded a python or a matlab code file
    '''

    # Read file paths
    # datafiles = [f for f in os.listdir(datafiles_path) if isfile(join(datafiles_path, f))]
    datafiles = glob.glob(datafiles_path + '**/*.*', recursive=True)
    
    # Read files
    df = pd.DataFrame()
    print('Processing {0} files in {1} ...'.format(len(datafiles), datafiles_path))
    for dtfile in datafiles:
        
        # This is a flag. If it remains True, a new column will be added to the df
        makedf = True      

        # The tag can be the NIA, the student's name or just the begining of some other file
        # tag = dtfile.split(splitsymbol)[0]
        tag = getFileName(dtfile).split(splitsymbol)[0]

        if tag in name_NIA:
        
            if dtfile.endswith('.zip'):
             
                # Read names of .mat files
                # files_in_zip = zp.ZipFile(join(datafiles_path, dtfile)).namelist()            
                files_in_zip = zp.ZipFile(dtfile).namelist()            

                # mat file selection. This is to disambiguate cases with multiple files
                n_mat = len([f for f in files_in_zip if f.endswith('.m')])
                n_py = len([f for f in files_in_zip if f.endswith('.py') or f.endswith('.ipynb')])

                if n_py * n_mat > 0:
                    print('WARNING: {} has delivered both matlab and python code'.format(name))

                if n_py > 0:
                    code = 'Py'
                elif n_mat > 0:
                    code = 'Mat'
                else:
                    code = 'None'

            elif dtfile.endswith('.py') or  dtfile.endswith('.ipynb'):
                code = 'Py'            
            elif dtfile.endswith('.m'):  
                code = 'Mat'
            else:
                code = 'None'

            df2 = pd.DataFrame()
            df2[tag] = pd.Series(code, index = ['Code'])
            df = pd.concat([df, df2], axis=1)
        elif os.path.isfile(dtfile):
            print('    File ignored: {0}'.format(dtfile))
    return df.T


# +
# Identify the code delivered by the students
code_data = detectCode(results_path, splitsymbol='_')
code_data[code_data.columns][:].head()

# Add the code data to the evaluation dataframe
df['Delivery', 'Code'] = code_data
df['Delivery', 'Delay'] = 0.0
df['Delivery', 'Factor'] = 1.0

# Penalties for students that did not delivered any code.
df.loc[df['Delivery', 'Code'] == 'None', ('Delivery', 'Factor')] = 0.5 

# +
# This cell contains project specific instructions.

# PENALTIES:
if project_path == '../LabEvaluationProjects/ProjectB3_1718/':

    # STUDENTS THAT DID NOT DELIVER ANY RESULTS.
    #     <NAME>: (no e-mail) Delivers code only.
    #         Results generated with penalty
    df.at['<NAME>', ('Delivery', 'Factor')] = p_noresults

    #     <NAME>: (no e-mail) Does not deliver results file. However, code computes some variables.
    #         Results generated with penalty
    df.at['<NAME>', ('Delivery', 'Factor')] = p_noresults

    #     <NAME>: (e-mail) His computer get blocked and could not generate results file 
    #         savemat command incorrect. Code generated without penalty.
    df.at['<NAME>', ('Delivery', 'Factor')] = 1.0

    #     <NAME>: (no e-mail) entrega un fichero Lab12.7z, pero cambia el nombre por Lab12zip
    #         Results generated with penalty.
    df.at['<NAME>', ('Delivery', 'Factor')] = p_noresults

    #     <NAME>: (e-mail) Does not deliver results file. Code does not compute any of the variables 
    #     <NAME>: (no e-mail) Delivers multiple code versions.
    #     <NAME> (no e-mail) No results file. The code is completely wrong.
    #     <NAME>: compressed files with .7z. Changed without penalty.
    
elif project_path == '../LabEvaluationProjects/ProjectB3_1718_Gbil/':
    # NO INCIDENTS IN THIS GROUP
    pass


if project_path == 'prb12':
    
    # <NAME>: 
    #      (1) python does not recognize the delivered file as zip. However, I could decompress with
    #          the unarchiver. zip file re-generated without penalty
    #      (2) the mat file is actualy a .ipynb with the extension changed.
    # <NAME>: the .zip file cannot be read in any way. I have changed the extension to
    #          .unk.
    # <NAME>: delivers a .7z file. File .zip generated without penalty
    # <NAME> delivers a .7z file. File .zip generated without penalty
    pass

# if exam_label == 'ExLabB12_0':
#    df.drop('mTrain', axis=1, inplace=True)
# -

# Now we are ready to compute the final score

df['Final', 'Score'] = (df['Exam', 'Score'] - p_delay * df['Delivery', 'Delay']) * df['Delivery', 'Factor']
df[df.columns]    # .head()

# ## 4. Save results

# Save to excel file.
if not os.path.exists(output_path):
    os.makedirs(output_path)
df.to_excel(output_path + finalnotes_fname, columns=df.columns)



StarcoderdataJupyterScriptsDedupFiltered