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.
118 lines
3.4 KiB
Rust
118 lines
3.4 KiB
Rust
//! Forward-only (filtering) estimates: what the model knew at the time,
|
|
//! as opposed to the smoothed posteriors `learning_curve` reports.
|
|
|
|
use smallvec::smallvec;
|
|
use trueskill_tt::{Event, History, Member, Outcome, Team};
|
|
|
|
/// `games` one-on-one matches at successive times, won by "a" every time.
|
|
///
|
|
/// This is the fixture from issue #19, where `online(true)` reported
|
|
/// `games * ln(0.5)`.
|
|
fn repeated_winner(games: i64) -> History {
|
|
let mut history = History::builder().build();
|
|
|
|
for time in 1..=games {
|
|
history
|
|
.add_events([Event {
|
|
time,
|
|
teams: smallvec![
|
|
Team::with_members([Member::new("a")]),
|
|
Team::with_members([Member::new("b")]),
|
|
],
|
|
outcome: Outcome::winner(0, 2),
|
|
}])
|
|
.unwrap();
|
|
}
|
|
|
|
history
|
|
}
|
|
|
|
#[test]
|
|
fn filtered_evidence_sits_between_coin_flip_and_batch() {
|
|
let mut history = repeated_winner(5);
|
|
|
|
history.converge().unwrap();
|
|
|
|
let coin_flip = 5.0 * 0.5f64.ln();
|
|
let batch = history.log_evidence();
|
|
let filtered = history.filtered_log_evidence();
|
|
|
|
assert!(
|
|
filtered > coin_flip,
|
|
"filtered evidence {filtered} is at or below {coin_flip}, the all-coin-flip \
|
|
value the inert online flag reported; game one is a coin flip but games two \
|
|
through five are not"
|
|
);
|
|
|
|
assert!(
|
|
filtered < batch,
|
|
"filtered evidence {filtered} is not below the smoothed {batch}; filtering \
|
|
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"
|
|
);
|
|
}
|