refactor: improve cohort report

This commit is contained in:
2026-09-19 22:19:25 -04:00
parent c06d5caa8c
commit 92c8a8fc75
6 changed files with 2194 additions and 311 deletions
+177
View File
@@ -549,6 +549,105 @@ pub fn cohort_value(diagnostic: &CohortDiagnostic, config: &RenderConfig) -> Val
),
);
out.insert(
"grades",
Value::Array(
diagnostic
.grades
.iter()
.map(|grade| {
let mut value = Value::dict();
value.insert("letter", Value::str(&grade.letter));
value.insert("low", Value::Float(grade.low));
value.insert("high", Value::Float(grade.high));
value.insert_some("gpa", grade.gpa.map(Value::Float));
value.insert_some("attainment", grade.attainment.as_ref().map(Value::str));
value.insert("group", Value::str(&grade.group));
value.insert("count", Value::Int(grade.count as i64));
value.insert("share", Value::Float(grade.share));
value.insert("at-or-above", Value::Int(grade.at_or_above as i64));
value
})
.collect(),
),
);
out.insert(
"lectures",
Value::Array(
diagnostic
.lectures
.iter()
.map(|lecture| {
let mut value = Value::dict();
value.insert("lecture", Value::str(&lecture.lecture));
value.insert("title", Value::str(&lecture.title));
value.insert("items", Value::Int(lecture.n_items as i64));
value.insert("objectives", Value::Int(lecture.n_objectives as i64));
value.insert(
"objectives-below",
Value::Int(lecture.n_objectives_below as i64),
);
value.insert("rate", Value::Float(lecture.rate));
value.insert(
"questions",
Value::Array(
lecture
.questions
.iter()
.map(|n| Value::Int(*n as i64))
.collect(),
),
);
value.insert_some(
"worst-objective",
lecture
.worst_objective
.as_ref()
.map(|text| markup_value(text, content)),
);
value
})
.collect(),
),
);
let triage_rows = |rows: &[crate::diagnostic::TriageRow]| -> Value {
Value::Array(rows.iter().map(|row| triage_value(row, content)).collect())
};
let mut triage = Value::dict();
triage.insert("discard", triage_rows(&diagnostic.triage.discard));
triage.insert("rekey", triage_rows(&diagnostic.triage.rekey));
triage.insert("revise", triage_rows(&diagnostic.triage.revise));
triage.insert("reteach", triage_rows(&diagnostic.triage.reteach));
triage.insert("bounded", triage_rows(&diagnostic.triage.bounded));
triage.insert("clean", Value::Int(diagnostic.triage.clean as i64));
out.insert("triage", triage);
let predictions = &diagnostic.predictions;
let mut prediction = Value::dict();
prediction.insert("predicted", Value::Int(predictions.n_predicted as i64));
prediction.insert("calibrated", Value::Int(predictions.n_calibrated as i64));
prediction.insert_some(
"mean-signed-error",
predictions.mean_signed_error.map(Value::Float),
);
prediction.insert_some(
"mean-abs-error",
predictions.mean_abs_error.map(Value::Float),
);
prediction.insert("within", Value::Int(predictions.n_within as i64));
prediction.insert("band", Value::Int(predictions.n_band as i64));
prediction.insert("band-hit", Value::Int(predictions.n_band_hit as i64));
if let Some((number, expected, observed)) = predictions.biggest_surprise {
let mut surprise = Value::dict();
surprise.insert("number", Value::Int(number as i64));
surprise.insert("expected", Value::Float(expected));
surprise.insert("observed", Value::Float(observed));
prediction.insert("biggest-surprise", surprise);
}
out.insert("predictions", prediction);
out.insert(
"forms",
Value::Array(
@@ -601,6 +700,46 @@ pub fn cohort_value(diagnostic: &CohortDiagnostic, config: &RenderConfig) -> Val
out
}
/// One triage row as a Typst value.
fn triage_value(row: &crate::diagnostic::TriageRow, content: bool) -> Value {
let mut value = Value::dict();
value.insert("number", Value::Int(row.number as i64));
value.insert_some("item", row.item.as_ref().map(Value::str));
value.insert_some("level", row.level.map(|l| Value::Int(l as i64)));
value.insert("p", Value::Float(row.p_value));
value.insert_some("point-biserial", row.point_biserial.map(Value::Float));
value.insert_some("discrimination", row.discrimination.map(Value::Float));
value.insert(
"objectives",
Value::Array(
row.objectives
.iter()
.map(|text| markup_value(text, content))
.collect(),
),
);
value.insert(
"taught-in",
Value::Array(row.taught_in.iter().map(|t| Value::str(t)).collect()),
);
value.insert_some("option", row.option.as_ref().map(Value::str));
value.insert_some("option-share", row.option_share.map(Value::Float));
value.insert_some(
"option-point-biserial",
row.option_point_biserial.map(Value::Float),
);
value.insert(
"reasons",
Value::Array(
row.reasons
.iter()
.map(|reason| markup_value(reason, content))
.collect(),
),
);
value
}
/// One histogram bin as a Typst value.
fn bin_value(bin: &Bin) -> Value {
let mut value = Value::dict();
@@ -635,6 +774,29 @@ fn cohort_question_value(question: &CohortQuestionRow, content: bool) -> Value {
"objectives",
Value::Array(question.objectives.iter().map(|o| Value::str(o)).collect()),
);
value.insert(
"objective-texts",
Value::Array(
question
.objective_texts
.iter()
.map(|text| markup_value(text, content))
.collect(),
),
);
value.insert(
"taught-in",
Value::Array(question.taught_in.iter().map(|t| Value::str(t)).collect()),
);
value.insert(
"lectures",
Value::Array(question.lectures.iter().map(|l| Value::str(l)).collect()),
);
value.insert("difficulty-band", Value::str(&question.difficulty_band));
value.insert(
"discrimination-band",
Value::str(&question.discrimination_band),
);
value.insert("p", Value::Float(question.p_value));
value.insert_some("point-biserial", question.point_biserial.map(Value::Float));
value.insert_some("discrimination", question.discrimination.map(Value::Float));
@@ -676,6 +838,17 @@ fn cohort_question_value(question: &CohortQuestionRow, content: bool) -> Value {
.collect(),
),
);
value.insert(
"prediction-notes",
Value::Array(
question
.prediction_notes
.iter()
.map(|n| markup_value(n, content))
.collect(),
),
);
value.insert("calibrated", Value::Bool(question.calibrated));
let mut by_form = Value::dict();
for (form, p) in &question.by_form {
by_form.insert(form.clone(), Value::Float(*p));
@@ -953,7 +1126,11 @@ mod tests {
levels: Vec::new(),
objectives: Vec::new(),
gaps: Vec::new(),
grades: Vec::new(),
lectures: Vec::new(),
questions: Vec::new(),
triage: crate::diagnostic::Triage::default(),
predictions: crate::diagnostic::PredictionSummary::default(),
revise: Vec::new(),
forms: Vec::new(),
blueprint: Vec::new(),