Skip to main content

rill_ml/drift/
detector.rs

1//! Core drift detector trait.
2//!
3//! All drift detectors in RillML implement [`DriftDetector`]. A detector
4//! receives a scalar value (typically a prediction error or a target value)
5//! and reports a [`DriftLevel`]: no drift, a warning, or a confirmed drift.
6//!
7//! Implementations must use bounded memory. They never store raw feature
8//! vectors or labels — only scalar statistics derived from the stream.
9
10use crate::error::RillError;
11
12/// The severity level reported by a drift detector.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[non_exhaustive]
16pub enum DriftLevel {
17    /// No drift detected; the stream appears stable.
18    #[default]
19    None,
20    /// A possible change has been detected but confidence is insufficient
21    /// for a confirmed drift. Callers may wish to reduce confidence or
22    /// increase monitoring.
23    Warning,
24    /// A confirmed drift has been detected. Callers should consider
25    /// taking corrective action via a [`DriftStrategy`](crate::drift::DriftStrategy).
26    Drift,
27}
28
29impl DriftLevel {
30    /// Returns a short, stable string identifier.
31    ///
32    /// Possible values: `"none"`, `"warning"`, `"drift"`.
33    pub const fn as_str(&self) -> &'static str {
34        match self {
35            DriftLevel::None => "none",
36            DriftLevel::Warning => "warning",
37            DriftLevel::Drift => "drift",
38        }
39    }
40
41    /// Returns `true` if the level indicates any kind of change
42    /// (either `Warning` or `Drift`).
43    pub const fn is_change(self) -> bool {
44        matches!(self, DriftLevel::Warning | DriftLevel::Drift)
45    }
46}
47
48/// Online drift detector trait.
49///
50/// Implementations track a scalar stream (prediction errors or target values)
51/// and report when the stream's distribution appears to have changed. All
52/// implementations must use bounded memory.
53///
54/// The detector is decoupled from any model: it only observes a scalar and
55/// reports a level. The decision of what to do about a drift is delegated to
56/// a [`DriftStrategy`](crate::drift::DriftStrategy).
57pub trait DriftDetector {
58    /// Update the detector with a new scalar observation.
59    ///
60    /// Returns the current [`DriftLevel`] after incorporating the value.
61    /// Returns an error if the value is not finite.
62    fn update(&mut self, value: f64) -> Result<DriftLevel, RillError>;
63
64    /// Returns `true` if the detector currently reports a confirmed drift.
65    fn detected(&self) -> bool;
66
67    /// Returns `true` if the detector currently reports a warning.
68    fn warning(&self) -> bool;
69
70    /// The current drift level.
71    fn level(&self) -> DriftLevel;
72
73    /// Number of observations incorporated so far.
74    fn samples_seen(&self) -> u64;
75
76    /// Reset the detector to its initial (no-data) state.
77    fn reset(&mut self);
78
79    /// The detector-specific statistic value from the last update.
80    ///
81    /// Useful for diagnostics and logging in
82    /// [`DriftEvent`](crate::drift::DriftEvent). For example, Page-Hinkley
83    /// returns the cumulative-sum statistic, KSWIN returns the p-value.
84    /// The default implementation returns `0.0`.
85    fn last_value(&self) -> f64 {
86        0.0
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn drift_level_as_str() {
96        assert_eq!(DriftLevel::None.as_str(), "none");
97        assert_eq!(DriftLevel::Warning.as_str(), "warning");
98        assert_eq!(DriftLevel::Drift.as_str(), "drift");
99    }
100
101    #[test]
102    fn drift_level_is_change() {
103        assert!(!DriftLevel::None.is_change());
104        assert!(DriftLevel::Warning.is_change());
105        assert!(DriftLevel::Drift.is_change());
106    }
107
108    #[test]
109    fn drift_level_default_is_none() {
110        assert_eq!(DriftLevel::default(), DriftLevel::None);
111    }
112}