feat: allow drift to vary per competitor via Member::with_drift_scale
Drift was a property of the History, so every competitor drifted at the same rate and a fixed reference point could not share a graph with moving competitors. A bot at a known strength, a rating floor, a course difficulty — all of them drifted along with the players. Member::with_drift_scale(s) multiplies the drift *variance* a competitor accumulates, so s is in the same units as gamma: ConstantDrift(g) at scale s behaves exactly as ConstantDrift(g * s) would for that competitor. A scalar rather than a per-competitor Drift keeps History's single D type parameter untouched and stays Copy. 0.0 pins a competitor still. The scale lives on Rating, beside the drift it scales, and is applied only through Rating::drift_variance_delta / drift_variance_for_elapsed. Making those the sole entry points means a caller cannot reach the raw drift and silently skip a competitor's scale — the filtered pass was exactly that bug during development, caught because its test was written before the wiring. Like with_prior, the scale is competitor configuration captured at first appearance rather than a per-event override; a competitor that is static is static, and a scale that changed between events would make the skill trajectory hard to interpret. Member's docs claimed prior was a per-event override, which the code has never done — corrected here. A negative scale is rejected rather than squared into its absolute value, and a non-finite one rejected outright, both as InvalidParameter. None means 1.0, so no existing call site changes and no existing fit moves. Adding a public field to Member does break struct-literal construction downstream. Closes #34 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014b6wy2q8rnFK8U8GPJVQNU
This commit is contained in:
@@ -16,6 +16,9 @@ pub struct Rating<T: Time = i64, D: Drift<T> = ConstantDrift> {
|
||||
pub(crate) prior: Gaussian,
|
||||
pub(crate) beta: f64,
|
||||
pub(crate) drift: D,
|
||||
/// Multiplier on the drift *variance* this competitor accumulates; 1.0 is
|
||||
/// the neutral default. Set per competitor via `Member::with_drift_scale`.
|
||||
pub(crate) drift_scale: f64,
|
||||
pub(crate) _time: PhantomData<T>,
|
||||
}
|
||||
|
||||
@@ -25,10 +28,21 @@ impl<T: Time, D: Drift<T>> Rating<T, D> {
|
||||
prior,
|
||||
beta,
|
||||
drift,
|
||||
drift_scale: 1.0,
|
||||
_time: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Scale how fast this competitor drifts, relative to `drift`.
|
||||
///
|
||||
/// Multiplies the drift *variance*, so the scale is in the same units as
|
||||
/// `gamma`. `0.0` pins the competitor still.
|
||||
#[must_use]
|
||||
pub fn with_drift_scale(mut self, drift_scale: f64) -> Self {
|
||||
self.drift_scale = drift_scale;
|
||||
self
|
||||
}
|
||||
|
||||
/// The configured prior skill estimate.
|
||||
#[must_use]
|
||||
pub fn prior(&self) -> Gaussian {
|
||||
@@ -47,6 +61,28 @@ impl<T: Time, D: Drift<T>> Rating<T, D> {
|
||||
self.drift
|
||||
}
|
||||
|
||||
/// This competitor's multiplier on the drift variance; 1.0 is neutral.
|
||||
#[must_use]
|
||||
pub fn drift_scale(&self) -> f64 {
|
||||
self.drift_scale
|
||||
}
|
||||
|
||||
/// Drift variance accumulated over `from -> to`, scaled for this competitor.
|
||||
///
|
||||
/// The single place the scale is applied for a `Time`-typed span. Callers
|
||||
/// must go through this rather than `self.drift` directly, so a competitor's
|
||||
/// scale cannot be silently skipped.
|
||||
pub(crate) fn drift_variance_delta(&self, from: &T, to: &T) -> f64 {
|
||||
self.drift.variance_delta(from, to) * self.drift_scale * self.drift_scale
|
||||
}
|
||||
|
||||
/// Drift variance for a cached elapsed count, scaled for this competitor.
|
||||
///
|
||||
/// The counterpart of `drift_variance_delta` for the cached-elapsed paths.
|
||||
pub(crate) fn drift_variance_for_elapsed(&self, elapsed: i64) -> f64 {
|
||||
self.drift.variance_for_elapsed(elapsed) * self.drift_scale * self.drift_scale
|
||||
}
|
||||
|
||||
pub(crate) fn performance(&self) -> Gaussian {
|
||||
self.prior.forget(self.beta.powi(2))
|
||||
}
|
||||
@@ -58,6 +94,7 @@ impl Default for Rating<i64, ConstantDrift> {
|
||||
prior: Gaussian::default(),
|
||||
beta: BETA,
|
||||
drift: ConstantDrift(GAMMA),
|
||||
drift_scale: 1.0,
|
||||
_time: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user