feat: add filtered learning curves

learning_curve returns post-convergence posteriors, so every point is
smoothed: the estimate at a given date incorporates rounds played years
later. On ustat's data that starts six players' curves already spread apart
at sigma 0.9-1.6 against a prior of 6.0, barely moving thereafter.

filtered_learning_curve plots the same competitor on forward-only
information, so everyone starts at the prior and fans out. It could not be
reconstructed from the public API before: a caller could only refit over
events[0..k] for every k, which is O(n^2) fits for something one forward
pass already computes.
This commit is contained in:
2026-08-27 16:30:19 +02:00
parent d4af048914
commit 50e11cfbfa
2 changed files with 112 additions and 3 deletions
+65
View File
@@ -50,3 +50,68 @@ fn filtered_evidence_sits_between_coin_flip_and_batch() {
scores each game on strictly less information than smoothing does"
);
}
#[test]
fn filtered_first_point_is_less_certain_than_smoothed() {
let mut history = repeated_winner(12);
history.converge().unwrap();
let smoothed = history.learning_curve("a");
let filtered = history.filtered_learning_curve("a");
assert_eq!(
smoothed.len(),
filtered.len(),
"both curves must cover the same time points"
);
let (smoothed_time, first_smoothed) = smoothed[0];
let (filtered_time, first_filtered) = filtered[0];
assert_eq!(smoothed_time, filtered_time);
assert!(
first_filtered.sigma() > first_smoothed.sigma(),
"filtered sigma {} at the first point is not above smoothed {}; the smoother \
collapses uncertainty before the first round is drawn, which is the whole \
reason this method exists",
first_filtered.sigma(),
first_smoothed.sigma()
);
assert!(
first_filtered.sigma() < trueskill_tt::SIGMA,
"filtered sigma {} at the first point is not below the prior {}; one game was \
played, so some uncertainty must have been resolved",
first_filtered.sigma(),
trueskill_tt::SIGMA
);
for pair in filtered.windows(2) {
assert!(
pair[1].1.mu() > pair[0].1.mu(),
"filtered mu must climb at every step for a competitor who wins every \
game: t={} mu={} then t={} mu={}",
pair[0].0,
pair[0].1.mu(),
pair[1].0,
pair[1].1.mu()
);
}
}
#[test]
fn filtered_curves_plural_agrees_with_singular() {
let mut history = repeated_winner(4);
history.converge().unwrap();
let curves = history.filtered_learning_curves();
assert_eq!(
curves["b"],
history.filtered_learning_curve("b"),
"the plural form must agree with the singular for the same key"
);
}