grading
Version:
Grading of student submissions, in particular programming tests.
204 lines (176 loc) • 7.97 kB
text/typescript
import { type } from "os";
import { GradingSchema, gradingText } from "./gradingschema";
import { GradingResult, Results, StudentResult } from "./results";
export type ItemType = 'exam' | 'task' | 'grading' | 'test';
function subItemType(itemType: ItemType) {
const ITEM_TYPE_NAMES = ['exam', 'task', 'grading', 'test'];
const current = ITEM_TYPE_NAMES.indexOf(itemType);
if (current < 0) {
return "unknown";
}
if (current == ITEM_TYPE_NAMES.length - 1) {
return "subtest(?!)"
}
return ITEM_TYPE_NAMES[current + 1];
}
export function computeStatistics(gradingSchema: GradingSchema, results: Results) {
const examStatistics = new ItemStatistics(gradingSchema.exam, gradingSchema.exam, gradingSchema.points, 'exam');
const statistics = new Statistics(examStatistics);
statistics.appliedCorrections = results.appliedCorrections;
// Generate overall scheme
gradingSchema.tasks.forEach((task, taskIndex) => {
const taskLabel = (taskIndex + 1) + ". " + task.name;
const taskStatistic = examStatistics.addItemStatistics(new ItemStatistics(taskLabel, taskLabel, task.points, 'task'))
task.grading.forEach((grading, gradingIndex) => {
const gradingLabel = (taskIndex + 1) + "." + (gradingIndex + 1) + ".";
const gradingTitle = (taskIndex + 1) + "." + (gradingIndex + 1) + ". " + gradingText(grading);
const gradingStatistics = taskStatistic.addItemStatistics(new ItemStatistics(gradingLabel, gradingTitle, grading.points, 'grading'));
grading.tests.forEach((test, testIndex) => {
const testLabel = gradingLabel + (testIndex + 1) + ".";
const testTitle = gradingLabel + (testIndex + 1) + ". " + test;
gradingStatistics.addItemStatistics(new ItemStatistics(testLabel, testTitle, 1, 'test'))
})
})
})
// Fill data
results.studentResults.forEach((studentResult, studentIndex) => {
statistics.registerSubmission(studentResult.coverageResult.stmtCoverage, studentResult.userTestsCount);
examStatistics.registerSubmission(studentResult.pointsRaw, studentResult.skipped());
studentResult.taskResults.forEach((taskResult, taskIndex) => {
const taskLabel = (taskIndex + 1) + ". " + taskResult.name;
const taskStatistic = examStatistics.getItemStatistics(taskIndex, taskLabel, studentResult);
taskStatistic.registerSubmission(taskResult.points, taskResult.skipped());
taskResult.gradingResults.forEach((gradingResult, gradingIndex) => {
const gradingLabel = (taskIndex + 1) + "." + (gradingIndex + 1) + ".";
const gradingStatistics = taskStatistic.getItemStatistics(gradingIndex, gradingLabel);
gradingStatistics.registerSubmission(gradingResult.points, gradingResult.skipped());
gradingResult.testResults.forEach((testResult, testIndex) => {
const testLabel = gradingLabel + (testIndex + 1) + ".";
const testStatistic = gradingStatistics.getItemStatistics(testIndex, testLabel);
testStatistic.registerSubmission(testResult.testStatus === 'success' ? 1 : 0, testResult.testStatus === 'notFound');
});
});
});
});
return statistics;
}
export class Statistics {
readonly examStatistics: ItemStatistics | undefined;
/**
* All submissions, should be similar to number of submitters.
* Includes skipped items.
*/
submissions: number = 0;
sumCoverage: number = 0;
sumStudentTests: number = 0;
appliedCorrections: number = 0;
constructor(examStatistics: ItemStatistics) {
this.examStatistics = examStatistics;
}
registerSubmission(coverage: number, studentTests: number) {
this.submissions++;
this.sumCoverage += coverage;
this.sumStudentTests += studentTests;
}
get averageCoverage() {
if (this.submissions == 0) {
throw new Error("Cannot compute average coverage, no submissions registered");
}
return this.sumCoverage / this.submissions;
}
get averageStudentTests() {
if (this.submissions == 0) {
throw new Error("Cannot compute average student tests, no submissions registered");
}
return this.sumStudentTests / this.submissions;
}
*[Symbol.iterator]() {
const stack: ItemStatistics[] = [];
if (this.examStatistics) {
stack.unshift(this.examStatistics);
}
while (stack.length > 0) {
const current = stack.shift()!;
yield current;
if (current.itemStatistics.length != 0) {
stack.unshift(...current.itemStatistics);
}
}
}
}
export class ItemStatistics {
readonly label: string;
readonly title: string;
readonly pointsMax: number;
readonly itemType: ItemType;
/**
* Subitems, i.e. exam->tasks, tasks->tests, tests->none
*/
readonly itemStatistics: ItemStatistics[] = []
/**
* All submissions, should be similar to number of submitters.
* Includes skipped items.
*/
submissions: number = 0;
sumGainedPoints: number = 0;
skippedCount: number = 0;
constructor(label: string, title: string, pointsMax: number, itemType: ItemType) {
this.label = label;
this.title = title;
this.pointsMax = pointsMax;
this.itemType = itemType;
if (this.pointsMax < 0) {
throw new Error(`Inconsistency for ${this.description}, maximum points must be greater or equal 0.`)
}
}
addItemStatistics(itemStatistics: ItemStatistics) {
this.itemStatistics.push(itemStatistics);
return itemStatistics;
}
/**
* Returns the statistics for the given subitem by index.
* Since the item is added for the first student, errors here means that a student has something no one else had before.
*/
getItemStatistics(index: number, label: string, studentResult?: StudentResult) {
const itemStatistics = this.itemStatistics[index];
if (!itemStatistics) {
throw new Error(`Inconsistency in ${this.description} found: No ${subItemType(this.itemType)} at index ${index} ('${label}')${studentResult ? " for " + studentResult.userName : ""}.`)
}
if (itemStatistics.label !== label) {
throw new Error(`Inconsistency in ${this.description} found: ${itemStatistics.description} with different name (${label}) at index ${index}${studentResult ? " for " + studentResult.userName : ""}.`);
}
return itemStatistics;
}
registerSubmission(points: number, skipped: boolean) {
this.submissions++;
this.sumGainedPoints += points;
if (skipped) {
this.skippedCount++;
}
}
get successRate() {
if (this.submissions == 0) {
if (this.itemType === 'exam') {
throw new Error(`Cannot compute success rate for ${this.description}, no submissions registered`);
} else { // may happen if no student has submitted this item
return 0;
}
}
const averagePoints = this.sumGainedPoints / this.submissions;
const successRate = averagePoints / this.pointsMax;
return successRate;
}
get successRateNonSkipped() {
if (this.submissions == 0) {
throw new Error(`Cannot compute success rate (non skipped) for ${this.itemType} ${this.label}, no submissions registered`);
}
const nonSkipped = this.submissions - this.skippedCount;
if (nonSkipped == 0) {
return 0;
}
return this.sumGainedPoints / nonSkipped;
}
get description() {
return `${this.itemType} '${this.label}'`;
}
}