chore: add dropped question to reports
This commit is contained in:
@@ -172,6 +172,12 @@ pub struct StudentDiagnostic {
|
|||||||
/// One row per question, with no question in it.
|
/// One row per question, with no question in it.
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
pub questions: Vec<QuestionRow>,
|
pub questions: Vec<QuestionRow>,
|
||||||
|
/// Questions dropped from scoring, in number order.
|
||||||
|
///
|
||||||
|
/// Reported separately from the question rows so the document can say once,
|
||||||
|
/// in prose, why the denominator is smaller than the paper was.
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub dropped_questions: Vec<u32>,
|
||||||
/// Which lectures to go back to, the one that would repay the most time
|
/// Which lectures to go back to, the one that would repay the most time
|
||||||
/// first.
|
/// first.
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
@@ -320,6 +326,13 @@ pub struct QuestionRow {
|
|||||||
/// Whether it was a bonus question.
|
/// Whether it was a bonus question.
|
||||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||||
pub bonus: bool,
|
pub bonus: bool,
|
||||||
|
/// Whether it was dropped from scoring after the fact.
|
||||||
|
///
|
||||||
|
/// A dropped question still appears in the map, because the student has the
|
||||||
|
/// paper in front of them and will look for it. What it must not do is
|
||||||
|
/// appear as an error they made.
|
||||||
|
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||||
|
pub dropped: bool,
|
||||||
/// Whether the student left it blank.
|
/// Whether the student left it blank.
|
||||||
pub blank: bool,
|
pub blank: bool,
|
||||||
/// The share of the class that answered it correctly, when comparison is on.
|
/// The share of the class that answered it correctly, when comparison is on.
|
||||||
@@ -530,6 +543,14 @@ pub fn student(
|
|||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut dropped_questions: Vec<u32> = rows
|
||||||
|
.iter()
|
||||||
|
.filter(|r| r.dropped)
|
||||||
|
.map(|r| r.item_number)
|
||||||
|
.collect();
|
||||||
|
dropped_questions.sort_unstable();
|
||||||
|
dropped_questions.dedup();
|
||||||
|
|
||||||
// Built from the response rows rather than from `questions`, so a report with
|
// Built from the response rows rather than from `questions`, so a report with
|
||||||
// `--no-questions` still says where to go back to; it just does not name the
|
// `--no-questions` still says where to go back to; it just does not name the
|
||||||
// question numbers.
|
// question numbers.
|
||||||
@@ -573,6 +594,7 @@ pub fn student(
|
|||||||
strengths,
|
strengths,
|
||||||
focus,
|
focus,
|
||||||
questions,
|
questions,
|
||||||
|
dropped_questions,
|
||||||
review_lectures,
|
review_lectures,
|
||||||
study,
|
study,
|
||||||
}
|
}
|
||||||
@@ -591,7 +613,10 @@ fn question_row(
|
|||||||
opts: &Options,
|
opts: &Options,
|
||||||
) -> QuestionRow {
|
) -> QuestionRow {
|
||||||
let blank = row.selected.is_empty() && row.eliminated.is_empty();
|
let blank = row.selected.is_empty() && row.eliminated.is_empty();
|
||||||
let missed = row.credit < 0.999;
|
// A dropped question cannot be missed. Without `counts()` here, a question
|
||||||
|
// thrown out after the exam still collects per-option feedback explaining an
|
||||||
|
// error the student is no longer being charged for.
|
||||||
|
let missed = row.credit < 0.999 && row.counts();
|
||||||
let mut feedback = None;
|
let mut feedback = None;
|
||||||
let mut hint = None;
|
let mut hint = None;
|
||||||
let mut misconception = None;
|
let mut misconception = None;
|
||||||
@@ -673,6 +698,7 @@ fn question_row(
|
|||||||
correct: row.correct,
|
correct: row.correct,
|
||||||
credit: row.credit,
|
credit: row.credit,
|
||||||
bonus: row.bonus,
|
bonus: row.bonus,
|
||||||
|
dropped: row.dropped,
|
||||||
blank,
|
blank,
|
||||||
class_rate: opts
|
class_rate: opts
|
||||||
.comparison
|
.comparison
|
||||||
@@ -926,6 +952,11 @@ pub struct CohortDiagnostic {
|
|||||||
pub lectures: Vec<CohortLectureRow>,
|
pub lectures: Vec<CohortLectureRow>,
|
||||||
/// Per-question statistics.
|
/// Per-question statistics.
|
||||||
pub questions: Vec<CohortQuestionRow>,
|
pub questions: Vec<CohortQuestionRow>,
|
||||||
|
/// Questions dropped from scoring, which are therefore absent from every
|
||||||
|
/// table above. Recorded so the report says why rather than leaving a gap in
|
||||||
|
/// the numbering.
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub dropped_questions: Vec<u32>,
|
||||||
/// What to do about each question that raised something.
|
/// What to do about each question that raised something.
|
||||||
pub triage: Triage,
|
pub triage: Triage,
|
||||||
/// How the authored expectations did.
|
/// How the authored expectations did.
|
||||||
@@ -1478,6 +1509,13 @@ pub fn cohort(
|
|||||||
.filter_map(|item| questions.iter().find(|q| q.number == item.number).cloned())
|
.filter_map(|item| questions.iter().find(|q| q.number == item.number).cloned())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let mut dropped_questions: Vec<u32> = set
|
||||||
|
.all_items()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|n| set.item_dropped(*n))
|
||||||
|
.collect();
|
||||||
|
dropped_questions.sort_unstable();
|
||||||
|
|
||||||
let default_options = course.policy.options_per_item;
|
let default_options = course.policy.options_per_item;
|
||||||
let triage = triage(&questions, threshold, default_options);
|
let triage = triage(&questions, threshold, default_options);
|
||||||
let predictions = prediction_summary(analysis);
|
let predictions = prediction_summary(analysis);
|
||||||
@@ -1490,6 +1528,7 @@ pub fn cohort(
|
|||||||
distribution: distribution(&percents),
|
distribution: distribution(&percents),
|
||||||
grades,
|
grades,
|
||||||
lectures,
|
lectures,
|
||||||
|
dropped_questions,
|
||||||
triage,
|
triage,
|
||||||
predictions,
|
predictions,
|
||||||
reliability: ReliabilityRow {
|
reliability: ReliabilityRow {
|
||||||
@@ -2193,6 +2232,7 @@ mod tests {
|
|||||||
strengths: Vec::new(),
|
strengths: Vec::new(),
|
||||||
focus: Vec::new(),
|
focus: Vec::new(),
|
||||||
questions: Vec::new(),
|
questions: Vec::new(),
|
||||||
|
dropped_questions: Vec::new(),
|
||||||
review_lectures: Vec::new(),
|
review_lectures: Vec::new(),
|
||||||
study: Vec::new(),
|
study: Vec::new(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ pub fn student_value(diagnostic: &StudentDiagnostic, config: &RenderConfig) -> V
|
|||||||
value.insert_some("correct", question.correct.map(Value::Bool));
|
value.insert_some("correct", question.correct.map(Value::Bool));
|
||||||
value.insert("credit", Value::Float(question.credit));
|
value.insert("credit", Value::Float(question.credit));
|
||||||
value.insert("bonus", Value::Bool(question.bonus));
|
value.insert("bonus", Value::Bool(question.bonus));
|
||||||
|
value.insert("dropped", Value::Bool(question.dropped));
|
||||||
value.insert("blank", Value::Bool(question.blank));
|
value.insert("blank", Value::Bool(question.blank));
|
||||||
value.insert_some("class-rate", question.class_rate.map(Value::Float));
|
value.insert_some("class-rate", question.class_rate.map(Value::Float));
|
||||||
value.insert(
|
value.insert(
|
||||||
@@ -348,6 +349,17 @@ pub fn student_value(diagnostic: &StudentDiagnostic, config: &RenderConfig) -> V
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
out.insert(
|
||||||
|
"dropped-questions",
|
||||||
|
Value::Array(
|
||||||
|
diagnostic
|
||||||
|
.dropped_questions
|
||||||
|
.iter()
|
||||||
|
.map(|n| Value::Int(*n as i64))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
out.insert(
|
out.insert(
|
||||||
"review-lectures",
|
"review-lectures",
|
||||||
Value::Array(
|
Value::Array(
|
||||||
@@ -612,6 +624,17 @@ pub fn cohort_value(diagnostic: &CohortDiagnostic, config: &RenderConfig) -> Val
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
out.insert(
|
||||||
|
"dropped-questions",
|
||||||
|
Value::Array(
|
||||||
|
diagnostic
|
||||||
|
.dropped_questions
|
||||||
|
.iter()
|
||||||
|
.map(|n| Value::Int(*n as i64))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
let triage_rows = |rows: &[crate::diagnostic::TriageRow]| -> Value {
|
let triage_rows = |rows: &[crate::diagnostic::TriageRow]| -> Value {
|
||||||
Value::Array(rows.iter().map(|row| triage_value(row, content)).collect())
|
Value::Array(rows.iter().map(|row| triage_value(row, content)).collect())
|
||||||
};
|
};
|
||||||
@@ -1128,6 +1151,7 @@ mod tests {
|
|||||||
gaps: Vec::new(),
|
gaps: Vec::new(),
|
||||||
grades: Vec::new(),
|
grades: Vec::new(),
|
||||||
lectures: Vec::new(),
|
lectures: Vec::new(),
|
||||||
|
dropped_questions: Vec::new(),
|
||||||
questions: Vec::new(),
|
questions: Vec::new(),
|
||||||
triage: crate::diagnostic::Triage::default(),
|
triage: crate::diagnostic::Triage::default(),
|
||||||
predictions: crate::diagnostic::PredictionSummary::default(),
|
predictions: crate::diagnostic::PredictionSummary::default(),
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
band-hit: 11,
|
band-hit: 11,
|
||||||
biggest-surprise: (number: 14, expected: 0.45, observed: 0.86),
|
biggest-surprise: (number: 14, expected: 0.45, observed: 0.86),
|
||||||
),
|
),
|
||||||
|
dropped-questions: (),
|
||||||
revise: (),
|
revise: (),
|
||||||
forms: (
|
forms: (
|
||||||
(id: "A", students: 12, mean: 73.5, sd: 10.2),
|
(id: "A", students: 12, mean: 73.5, sd: 10.2),
|
||||||
@@ -989,6 +990,17 @@
|
|||||||
One row per question, in the order they were numbered. Option letters are the
|
One row per question, in the order they were numbered. Option letters are the
|
||||||
bank's, not any one form's. See the guide above for what p, r, and D mean; the
|
bank's, not any one form's. See the guide above for what p, r, and D mean; the
|
||||||
r column is colour-coded against the conventional cut points.
|
r column is colour-coded against the conventional cut points.
|
||||||
|
#{
|
||||||
|
let dropped = cb-data.at("dropped-questions", default: ())
|
||||||
|
if dropped.len() > 0 [
|
||||||
|
#plural(dropped.len(), "Question", "Questions")
|
||||||
|
#dropped.map(str).join(", ")
|
||||||
|
#plural(dropped.len(), "is", "are")
|
||||||
|
marked dropped in the assessment record, so
|
||||||
|
#plural(dropped.len(), "it is", "they are")
|
||||||
|
absent from this table and from every statistic above.
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
#table(
|
#table(
|
||||||
|
|||||||
@@ -121,6 +121,7 @@
|
|||||||
correct: true,
|
correct: true,
|
||||||
credit: 1.0,
|
credit: 1.0,
|
||||||
bonus: false,
|
bonus: false,
|
||||||
|
dropped: false,
|
||||||
blank: false,
|
blank: false,
|
||||||
class-rate: 0.91,
|
class-rate: 0.91,
|
||||||
taught-in: (),
|
taught-in: (),
|
||||||
@@ -451,6 +452,26 @@
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#let dropped-questions = cb-data.at("dropped-questions", default: ())
|
||||||
|
|
||||||
|
#if dropped-questions.len() > 0 [
|
||||||
|
#block(above: entry-gap)[
|
||||||
|
#pad(right: prose-pad)[
|
||||||
|
#text(size: size-lead)[
|
||||||
|
#plural(dropped-questions.len(), "Question", "Questions")
|
||||||
|
#dropped-questions.map(str).join(", ")
|
||||||
|
#plural(dropped-questions.len(), "was", "were")
|
||||||
|
dropped after the exam and #plural(dropped-questions.len(), "is", "are")
|
||||||
|
not part of anyone's score. Your percentage above is out of the
|
||||||
|
#plural(dropped-questions.len(), "remaining question", "remaining questions").
|
||||||
|
Nothing you wrote on
|
||||||
|
#plural(dropped-questions.len(), "it", "them")
|
||||||
|
counted for or against you.
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Levels
|
// Levels
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -750,7 +771,10 @@
|
|||||||
#let questions = cb-data.at("questions", default: ())
|
#let questions = cb-data.at("questions", default: ())
|
||||||
|
|
||||||
#let question-box(q) = {
|
#let question-box(q) = {
|
||||||
let color = if q.at("blank", default: false) { luma(160) } else if q.at("correct", default: false) == true {
|
let dropped = q.at("dropped", default: false)
|
||||||
|
let color = if dropped { luma(130) } else if q.at("blank", default: false) {
|
||||||
|
luma(160)
|
||||||
|
} else if q.at("correct", default: false) == true {
|
||||||
ok-color
|
ok-color
|
||||||
} else if q.at("credit", default: 0.0) > 0.0 { mid-color } else { bad-color }
|
} else if q.at("credit", default: 0.0) > 0.0 { mid-color } else { bad-color }
|
||||||
box(
|
box(
|
||||||
@@ -763,10 +787,15 @@
|
|||||||
#align(center)[
|
#align(center)[
|
||||||
#text(size: size-small, weight: "bold", fill: color.darken(18%))[#q.number]
|
#text(size: size-small, weight: "bold", fill: color.darken(18%))[#q.number]
|
||||||
#{
|
#{
|
||||||
let level = q.at("level", default: none)
|
if dropped {
|
||||||
if level != none {
|
|
||||||
linebreak()
|
linebreak()
|
||||||
text(size: size-tag, fill: luma(120))[L#level]
|
text(size: size-tag, fill: luma(110))[out]
|
||||||
|
} else {
|
||||||
|
let level = q.at("level", default: none)
|
||||||
|
if level != none {
|
||||||
|
linebreak()
|
||||||
|
text(size: size-tag, fill: luma(120))[L#level]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user