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:
+47
-3
@@ -307,9 +307,6 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Learning curves for all competitors, keyed by their user-facing key.
|
/// Learning curves for all competitors, keyed by their user-facing key.
|
||||||
///
|
|
||||||
/// Note: `key(idx)` is O(n) per lookup; this method is therefore O(n²)
|
|
||||||
/// in the number of competitors. Acceptable for T2; T3 may optimize.
|
|
||||||
pub fn learning_curves(&self) -> HashMap<K, Vec<(T, Gaussian)>> {
|
pub fn learning_curves(&self) -> HashMap<K, Vec<(T, Gaussian)>> {
|
||||||
#[cfg(feature = "rayon")]
|
#[cfg(feature = "rayon")]
|
||||||
{
|
{
|
||||||
@@ -380,6 +377,53 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Filtered learning curves for all competitors, keyed by user-facing key.
|
||||||
|
///
|
||||||
|
/// Each point is the posterior using only events up to and including that
|
||||||
|
/// time — "what we knew then". Contrast `learning_curves`, whose points
|
||||||
|
/// are smoothed and so incorporate rounds played later.
|
||||||
|
///
|
||||||
|
/// Runs a full forward pass per call and caches nothing.
|
||||||
|
pub fn filtered_learning_curves(&self) -> HashMap<K, Vec<(T, Gaussian)>> {
|
||||||
|
let mut data: HashMap<K, Vec<(T, Gaussian)>> = HashMap::new();
|
||||||
|
|
||||||
|
for (time, step) in self.filtered_pass() {
|
||||||
|
for (agent, posterior) in step.posteriors {
|
||||||
|
if let Some(key) = self.keys.key(agent).cloned() {
|
||||||
|
data.entry(key).or_default().push((time, posterior));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filtered learning curve for a single key: (time, posterior) pairs in
|
||||||
|
/// time order.
|
||||||
|
///
|
||||||
|
/// Runs the same full pass as `filtered_learning_curves` and keeps one
|
||||||
|
/// key, so asking for several keys individually costs a pass each — use
|
||||||
|
/// the plural form for that.
|
||||||
|
pub fn filtered_learning_curve<Q>(&self, key: &Q) -> Vec<(T, Gaussian)>
|
||||||
|
where
|
||||||
|
K: Borrow<Q>,
|
||||||
|
Q: Hash + Eq + ?Sized,
|
||||||
|
{
|
||||||
|
let Some(idx) = self.keys.get(key) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
|
||||||
|
self.filtered_pass()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(time, step)| {
|
||||||
|
step.posteriors
|
||||||
|
.iter()
|
||||||
|
.find(|(agent, _)| *agent == idx)
|
||||||
|
.map(|&(_, posterior)| (time, posterior))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn log_evidence_internal(&mut self, forward: bool, targets: &[Index]) -> f64 {
|
pub(crate) fn log_evidence_internal(&mut self, forward: bool, targets: &[Index]) -> f64 {
|
||||||
#[cfg(feature = "rayon")]
|
#[cfg(feature = "rayon")]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,3 +50,68 @@ fn filtered_evidence_sits_between_coin_flip_and_batch() {
|
|||||||
scores each game on strictly less information than smoothing does"
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user