feat: improve dropped question support
This commit is contained in:
@@ -355,7 +355,12 @@ pub fn student_value(diagnostic: &StudentDiagnostic, config: &RenderConfig) -> V
|
||||
diagnostic
|
||||
.dropped_questions
|
||||
.iter()
|
||||
.map(|n| Value::Int(*n as i64))
|
||||
.map(|dropped| {
|
||||
let mut value = Value::dict();
|
||||
value.insert("number", Value::Int(dropped.number as i64));
|
||||
value.insert("full-credit", Value::Bool(dropped.full_credit));
|
||||
value
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
@@ -630,7 +635,12 @@ pub fn cohort_value(diagnostic: &CohortDiagnostic, config: &RenderConfig) -> Val
|
||||
diagnostic
|
||||
.dropped_questions
|
||||
.iter()
|
||||
.map(|n| Value::Int(*n as i64))
|
||||
.map(|dropped| {
|
||||
let mut value = Value::dict();
|
||||
value.insert("number", Value::Int(dropped.number as i64));
|
||||
value.insert("full-credit", Value::Bool(dropped.full_credit));
|
||||
value
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -994,11 +994,21 @@
|
||||
let dropped = cb-data.at("dropped-questions", default: ())
|
||||
if dropped.len() > 0 [
|
||||
#plural(dropped.len(), "Question", "Questions")
|
||||
#dropped.map(str).join(", ")
|
||||
#dropped.map(d => str(d.number)).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.
|
||||
#{
|
||||
let credited = dropped.filter(d => d.at("full-credit", default: false))
|
||||
if credited.len() > 0 [
|
||||
#plural(credited.len(), "Question", "Questions")
|
||||
#credited.map(d => str(d.number)).join(", ")
|
||||
#plural(credited.len(), "was", "were") credited to every student, so
|
||||
#plural(credited.len(), "it remains", "they remain") in the score
|
||||
denominator and the means above match the grade of record.
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1173,69 +1183,71 @@
|
||||
#let predictions = cb-data.at("predictions", default: (:))
|
||||
|
||||
#if show-predictions and predictions.at("predicted", default: 0) > 0 [
|
||||
= How your predictions did
|
||||
#block(breakable: false)[
|
||||
= How your predictions did
|
||||
|
||||
#let n = predictions.at("predicted", default: 0)
|
||||
#let calibrated = predictions.at("calibrated", default: 0)
|
||||
#let signed-error = predictions.at("mean-signed-error", default: none)
|
||||
#let abs-error = predictions.at("mean-abs-error", default: none)
|
||||
#let n = predictions.at("predicted", default: 0)
|
||||
#let calibrated = predictions.at("calibrated", default: 0)
|
||||
#let signed-error = predictions.at("mean-signed-error", default: none)
|
||||
#let abs-error = predictions.at("mean-abs-error", default: none)
|
||||
|
||||
#explain[
|
||||
#if calibrated == 0 [
|
||||
None of these #n #plural(n, "expectation", "expectations") rests on prior
|
||||
data, so they are predictions rather than calibrations. A prediction that
|
||||
misses is a fact about the prediction: it does not flag the item, and it is
|
||||
summarised here instead of appearing #n times in the tables above. Once
|
||||
`coursebank calibrate` has written statistics back into the bank, a
|
||||
subsequent miss means the cohort or the teaching moved, and it will be
|
||||
flagged.
|
||||
] else [
|
||||
#calibrated of #n #plural(n, "expectation", "expectations") rests on a
|
||||
prior calibration. Those are the ones whose misses are flagged on the item,
|
||||
because a calibrated item that moves is telling you about this cohort. The
|
||||
rest are predictions, and a miss corrects the prediction.
|
||||
#explain[
|
||||
#if calibrated == 0 [
|
||||
None of these #n #plural(n, "expectation", "expectations") rests on prior
|
||||
data, so they are predictions rather than calibrations. A prediction that
|
||||
misses is a fact about the prediction: it does not flag the item, and it is
|
||||
summarised here instead of appearing #n times in the tables above. Once
|
||||
`coursebank calibrate` has written statistics back into the bank, a
|
||||
subsequent miss means the cohort or the teaching moved, and it will be
|
||||
flagged.
|
||||
] else [
|
||||
#calibrated of #n #plural(n, "expectation", "expectations") rests on a
|
||||
prior calibration. Those are the ones whose misses are flagged on the item,
|
||||
because a calibrated item that moves is telling you about this cohort. The
|
||||
rest are predictions, and a miss corrects the prediction.
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
#grid(
|
||||
columns: (1fr, 1fr, 1fr),
|
||||
gutter: 9pt,
|
||||
stat-card(
|
||||
"difficulty bias",
|
||||
if signed-error != none {
|
||||
(if signed-error >= 0 { "+" } else { "" }) + str(calc.round(signed-error * 100)) + " pts"
|
||||
} else { "n/a" },
|
||||
note: if signed-error != none and signed-error > 0 {
|
||||
"items came out easier than you expected"
|
||||
} else if signed-error != none {
|
||||
"items came out harder than you expected"
|
||||
} else { none },
|
||||
),
|
||||
stat-card(
|
||||
"typical miss",
|
||||
if abs-error != none { str(calc.round(abs-error * 100)) + " pts" } else { "n/a" },
|
||||
note: str(predictions.at("within", default: 0)) + " of " + str(n) + " inside tolerance",
|
||||
),
|
||||
stat-card(
|
||||
"discrimination band",
|
||||
str(predictions.at("band-hit", default: 0)) + " / " + str(predictions.at("band", default: 0)),
|
||||
note: "landed in the band you expected",
|
||||
),
|
||||
)
|
||||
#grid(
|
||||
columns: (1fr, 1fr, 1fr),
|
||||
gutter: 9pt,
|
||||
stat-card(
|
||||
"difficulty bias",
|
||||
if signed-error != none {
|
||||
(if signed-error >= 0 { "+" } else { "" }) + str(calc.round(signed-error * 100)) + " pts"
|
||||
} else { "n/a" },
|
||||
note: if signed-error != none and signed-error > 0 {
|
||||
"items came out easier than you expected"
|
||||
} else if signed-error != none {
|
||||
"items came out harder than you expected"
|
||||
} else { none },
|
||||
),
|
||||
stat-card(
|
||||
"typical miss",
|
||||
if abs-error != none { str(calc.round(abs-error * 100)) + " pts" } else { "n/a" },
|
||||
note: str(predictions.at("within", default: 0)) + " of " + str(n) + " inside tolerance",
|
||||
),
|
||||
stat-card(
|
||||
"discrimination band",
|
||||
str(predictions.at("band-hit", default: 0)) + " / " + str(predictions.at("band", default: 0)),
|
||||
note: "landed in the band you expected",
|
||||
),
|
||||
)
|
||||
|
||||
#{
|
||||
let surprise = predictions.at("biggest-surprise", default: none)
|
||||
if surprise != none {
|
||||
block(above: entry-gap)[
|
||||
#pad(right: prose-pad)[
|
||||
#text(size: size-lead, fill: luma(95))[
|
||||
The largest single gap was question #surprise.number, predicted at
|
||||
#pct(surprise.expected) and observed at #pct(surprise.observed).
|
||||
#{
|
||||
let surprise = predictions.at("biggest-surprise", default: none)
|
||||
if surprise != none {
|
||||
block(above: entry-gap)[
|
||||
#pad(right: prose-pad)[
|
||||
#text(size: size-lead, fill: luma(95))[
|
||||
The largest single gap was question #surprise.number, predicted at
|
||||
#pct(surprise.expected) and observed at #pct(surprise.observed).
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
review: ((citation: "KKW §6.2", title: "Molecules and Medicine", url: "https://example.edu/6/2"),),
|
||||
),
|
||||
),
|
||||
dropped-questions: ((number: 35, full-credit: true),),
|
||||
review-lectures: (
|
||||
(
|
||||
lecture: "L1.1",
|
||||
@@ -458,15 +459,30 @@
|
||||
#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.
|
||||
#{
|
||||
// Two kinds of drop, and they need different sentences. A credited
|
||||
// question is still in the denominator, so telling a student it was
|
||||
// removed would not match the arithmetic they can do themselves.
|
||||
let credited = dropped-questions.filter(d => d.at("full-credit", default: false))
|
||||
let removed = dropped-questions.filter(d => not d.at("full-credit", default: false))
|
||||
let numbers = list => list.map(d => str(d.number)).join(", ")
|
||||
|
||||
if credited.len() > 0 [
|
||||
#plural(credited.len(), "Question", "Questions") #numbers(credited)
|
||||
#plural(credited.len(), "was", "were") thrown out after the exam.
|
||||
Everyone received full credit for
|
||||
#plural(credited.len(), "it", "them"), so
|
||||
#plural(credited.len(), "it is", "they are") still counted in the
|
||||
score above and whatever you chose made no difference.
|
||||
]
|
||||
if removed.len() > 0 [
|
||||
#plural(removed.len(), "Question", "Questions") #numbers(removed)
|
||||
#plural(removed.len(), "was", "were") thrown out and removed from
|
||||
scoring, so your percentage is out of the remaining questions.
|
||||
Nothing you wrote on #plural(removed.len(), "it", "them") counted
|
||||
either way.
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -822,7 +838,8 @@
|
||||
#box(width: 0.7em, height: 0.7em, fill: ok-color.lighten(70%), radius: 2pt) right ·
|
||||
#box(width: 0.7em, height: 0.7em, fill: mid-color.lighten(70%), radius: 2pt) part marks ·
|
||||
#box(width: 0.7em, height: 0.7em, fill: bad-color.lighten(70%), radius: 2pt) not right ·
|
||||
#box(width: 0.7em, height: 0.7em, fill: luma(210), radius: 2pt) left blank
|
||||
#box(width: 0.7em, height: 0.7em, fill: luma(210), radius: 2pt) left blank ·
|
||||
#box(width: 0.7em, height: 0.7em, fill: luma(150), radius: 2pt) dropped, not scored
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user