151 lines
6.5 KiB
Python
151 lines
6.5 KiB
Python
|
#!/usr/bin/env python3
|
||
|
# -*- coding: utf-8 -*-
|
||
|
|
||
|
# Script to ...
|
||
|
#
|
||
|
# Minimally tested. Seems to work. Use at your own risk.
|
||
|
#
|
||
|
# By James Eagan <james.eagan@telecom-paris.fr>
|
||
|
# https://james.eagan.fr
|
||
|
|
||
|
import warnings
|
||
|
warnings.simplefilter(action='ignore', category=FutureWarning)
|
||
|
|
||
|
import pandas as pd
|
||
|
import re
|
||
|
import subprocess
|
||
|
import sys
|
||
|
import time
|
||
|
|
||
|
skipCols = ['SID', 'Email', 'Submission ID', 'Submission Time',
|
||
|
'Lateness (H:M:S)', 'View Count', 'Submission Count',
|
||
|
]
|
||
|
|
||
|
def readCSV(fileName, sep=',', decimal_char='.', skipRows=0, skipFooter=0):
|
||
|
cols = list(pd.read_csv(fileName, nrows=1, sep=sep, decimal=decimal_char))
|
||
|
keepCols = [col for col in cols if col not in skipCols]
|
||
|
|
||
|
df = pd.read_csv(fileName, usecols=keepCols, sep=sep, decimal=decimal_char, skiprows=skipRows, skipfooter=skipFooter, engine='python')
|
||
|
# df = df.sort_values(by='Last Name')
|
||
|
return df
|
||
|
|
||
|
def massageHeaders(df, args):
|
||
|
if args.split_ects and 'ECTS' in df.columns:
|
||
|
df[['ECTS', 'ECTS attempted']] = df['ECTS'].str.replace(',', '.') \
|
||
|
.str.split('/', expand=True) \
|
||
|
.astype('float64')
|
||
|
|
||
|
if args.calc_avg and 'ECTS attempted' in df.columns and 'Note finale' in df.columns:
|
||
|
df['Weighted Grade'] = df[["ECTS attempted", "Note finale"]].product(axis=1)
|
||
|
attemptedECTS = df['ECTS attempted'].sum()
|
||
|
weightedAvg = df["Weighted Grade"].sum() / attemptedECTS
|
||
|
earnedECTS = df['ECTS'].sum()
|
||
|
mention = "passable" if 10 <= weightedAvg < 12 else \
|
||
|
"assez bien" if 12 <= weightedAvg < 14 else \
|
||
|
"bien" if 14 <= weightedAvg < 16 else \
|
||
|
"très bien" if weightedAvg >= 16 else ""
|
||
|
df = df.append({"Occurrence d'UE": "Overall",
|
||
|
'Note finale': round(weightedAvg, 2),
|
||
|
'ECTS': earnedECTS,
|
||
|
'Note finale transposée': mention
|
||
|
}, ignore_index=True)
|
||
|
|
||
|
return df
|
||
|
|
||
|
def writeExcel(df, fileName):
|
||
|
with pd.ExcelWriter(fileName) as writer:
|
||
|
df.to_excel(writer)
|
||
|
# with open(basename + "-out.csv", 'w') as writer:
|
||
|
# writer.write(df.to_csv())
|
||
|
|
||
|
def run(df, commandString, args):
|
||
|
for idx, row in df.iterrows():
|
||
|
columnValue = lambda match: str( # coerce numbers to strings for commandString
|
||
|
row[int( # coerce matches into ints so pandas treats as col number and not name
|
||
|
match.group(1))
|
||
|
- 1 # use 1-based indexing for columns (pandas uses 0-based, so we subtract)
|
||
|
])
|
||
|
replacedCommand = re.sub("\$(\d+)", columnValue, commandString)
|
||
|
if args.dry_run:
|
||
|
print(replacedCommand)
|
||
|
else:
|
||
|
result = subprocess.run(replacedCommand, capture_output=True, shell=True, text=True)
|
||
|
if result.stdout:
|
||
|
print(result.stdout)
|
||
|
if result.stderr:
|
||
|
print(result.stderr, file=sys.stderr)
|
||
|
result.check_returncode()
|
||
|
if args.delay:
|
||
|
time.sleep(args.delay)
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
import argparse
|
||
|
|
||
|
def parse_args():
|
||
|
parser = argparse.ArgumentParser(description="Massage Gradescope data")
|
||
|
parser.add_argument("csv", help="csv file as exported from Gradescope")
|
||
|
parser.add_argument("-o", "--out", help="file to write output")
|
||
|
group = parser.add_mutually_exclusive_group()
|
||
|
group.add_argument("-,", "--commas", help="use commas for decimal separator", action="store_true")
|
||
|
group.add_argument("-.", "--dots", help="use dots for decimal separator", action="store_true")
|
||
|
group.add_argument("-,.", "--commas2dots", help="convert decimal separator from , to .", action="store_true")
|
||
|
group.add_argument("-.,", "--dots2commas", help="convert decimal separator from . to ,", action="store_true")
|
||
|
group.add_argument("-d", "--decimal-separator", help="decimal separator for real numbers", default=",")
|
||
|
parser.add_argument("-s", "--sep", help="csv column separator (default: ',' when decimal separator is '.' and ';' for ',')")
|
||
|
parser.add_argument("--insep", help="input column separator", default=",")
|
||
|
# FIXME: These two aren't really general csv options and should be refactored elsewhere.
|
||
|
parser.add_argument("--split-ects", help="split ECTS column into two on / separator", action="store_true")
|
||
|
parser.add_argument("--calc-avg", help="calculate weighted average from 'Note finale' and 'ECTS'", action="store_true")
|
||
|
parser.add_argument("--calc-mentions", help="calculate mentions from avg")
|
||
|
parser.add_argument("--run", help="command to run for each row", default=None)
|
||
|
parser.add_argument("--dry-run", help="do not run anything (when used with --run)", action="store_true")
|
||
|
parser.add_argument("--delay", help="delay in s to add between calls (when used with --run)", default=0, type=float)
|
||
|
parser.add_argument("--head", help="limit to first N content lines", metavar="N", action="store", default=False, type=int)
|
||
|
parser.add_argument("--tail", help="limit to last N content lines", metavar="N", action="store", default=False, type=int)
|
||
|
|
||
|
args = parser.parse_args()
|
||
|
|
||
|
# FIXME : broken logic
|
||
|
if args.commas2dots:
|
||
|
args.indecimal_separator = ','
|
||
|
args.decimal_separator = '.'
|
||
|
elif args.dots2commas:
|
||
|
args.indecimal_separator = '.'
|
||
|
args.decimal_separator = ","
|
||
|
elif args.dots:
|
||
|
args.indecimal_separator = "."
|
||
|
args.decimal_separator = "."
|
||
|
elif args.commas:
|
||
|
args.indecimal_separator = "."
|
||
|
args.decimal_separator = ","
|
||
|
|
||
|
if not args.sep:
|
||
|
args.sep = ';' if args.decimal_separator == ',' else ','
|
||
|
|
||
|
return args
|
||
|
|
||
|
def writeOutput(df, args):
|
||
|
if args.out:
|
||
|
writeExcel(df, args.out)
|
||
|
elif args.run:
|
||
|
run(df, args.run, args)
|
||
|
else :
|
||
|
df.to_csv(sys.stdout, sep=args.sep, decimal=args.decimal_separator)
|
||
|
|
||
|
def nowDoIt():
|
||
|
args = parse_args()
|
||
|
skipFooter = args.head * -1 if args.head and args.head < 0 else 0
|
||
|
skipRows = args.tail * -1 if args.tail and args.tail < 0 else 0
|
||
|
df = readCSV(args.csv, args.insep, args.indecimal_separator, skipRows, skipFooter)
|
||
|
|
||
|
if args.head and args.head > 0:
|
||
|
df = df.iloc[:args.head]
|
||
|
if args.tail and args.tail > 0:
|
||
|
df = df.iloc[len(df) - args.tail:]
|
||
|
|
||
|
df = massageHeaders(df, args)
|
||
|
writeOutput(df, args)
|
||
|
|
||
|
nowDoIt()
|
||
|
|