Skip to main content

rill_ml/drift/
action.rs

1//! Drift action and event types.
2//!
3//! When a drift detector reports a change, a [`DriftAction`] describes what
4//! the system should do about it. Actions are intentionally decoupled from
5//! detectors: a detector only reports the level, and a
6//! [`DriftStrategy`](crate::drift::strategy::DriftStrategy) decides the action.
7
8use crate::drift::detector::DriftLevel;
9
10/// The action to take when drift is detected.
11///
12/// This enum is returned by a [`DriftStrategy`](crate::drift::strategy::DriftStrategy)
13/// and executed by [`DriftAwareModel`](crate::drift::aware_model::DriftAwareModel).
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub enum DriftAction {
17    /// Record the event but do not change model behavior. This is the
18    /// safest default and should be used when the cost of a wrong reset
19    /// exceeds the cost of a slow adaptation.
20    #[default]
21    NotifyOnly,
22    /// Lower the confidence associated with subsequent predictions. The
23    /// interpretation is left to the caller (e.g. widen prediction intervals
24    /// or flag predictions as uncertain).
25    ReduceConfidence,
26    /// Reset the wrapped model's parameters to their initial state. Use
27    /// when the concept drift is severe enough that relearning from scratch
28    /// is faster than incremental adaptation.
29    ResetModel,
30    /// Reset the preprocessor's running statistics (e.g. StandardScaler
31    /// mean and variance). Useful when feature distributions have shifted
32    /// but the target relationship remains similar.
33    ResetPreprocessor,
34    /// Replace the current model with a baseline model. The replacement
35    /// logic is handled by the caller; this action signals intent.
36    ReplaceWithBaseline,
37    /// Increase the model's adaptation rate (e.g. raise the learning rate)
38    /// so it can relearn faster on the new distribution. The exact mechanism
39    /// is model-dependent.
40    IncreaseAdaptationRate,
41}
42
43impl DriftAction {
44    /// Returns a short, stable string identifier.
45    ///
46    /// Possible values: `"notify_only"`, `"reduce_confidence"`,
47    /// `"reset_model"`, `"reset_preprocessor"`, `"replace_with_baseline"`,
48    /// `"increase_adaptation_rate"`.
49    pub const fn as_str(&self) -> &'static str {
50        match self {
51            DriftAction::NotifyOnly => "notify_only",
52            DriftAction::ReduceConfidence => "reduce_confidence",
53            DriftAction::ResetModel => "reset_model",
54            DriftAction::ResetPreprocessor => "reset_preprocessor",
55            DriftAction::ReplaceWithBaseline => "replace_with_baseline",
56            DriftAction::IncreaseAdaptationRate => "increase_adaptation_rate",
57        }
58    }
59
60    /// Returns `true` if this action modifies the model or preprocessor state.
61    ///
62    /// `NotifyOnly` and `ReduceConfidence` return `false`; all others return
63    /// `true`.
64    pub const fn is_destructive(self) -> bool {
65        matches!(
66            self,
67            DriftAction::ResetModel
68                | DriftAction::ResetPreprocessor
69                | DriftAction::ReplaceWithBaseline
70                | DriftAction::IncreaseAdaptationRate
71        )
72    }
73}
74
75/// An immutable record of a single drift event.
76///
77/// Produced by [`DriftAwareModel`](crate::drift::aware_model::DriftAwareModel)
78/// whenever the detector reports a change (warning or drift).
79#[derive(Debug, Clone)]
80#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
81pub struct DriftEvent {
82    /// The sample index at which the event was triggered (0-based).
83    pub sample_index: u64,
84    /// The drift level that triggered the event.
85    pub level: DriftLevel,
86    /// The action that was taken in response.
87    pub action: DriftAction,
88    /// The detector-specific value at the time of triggering (e.g. cumulative
89    /// sum for Page-Hinkley, KS statistic for KSWIN). Useful for diagnostics.
90    pub detector_value: f64,
91}
92
93impl DriftEvent {
94    /// Create a new drift event record.
95    pub const fn new(
96        sample_index: u64,
97        level: DriftLevel,
98        action: DriftAction,
99        detector_value: f64,
100    ) -> Self {
101        Self {
102            sample_index,
103            level,
104            action,
105            detector_value,
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn action_as_str() {
116        assert_eq!(DriftAction::NotifyOnly.as_str(), "notify_only");
117        assert_eq!(DriftAction::ReduceConfidence.as_str(), "reduce_confidence");
118        assert_eq!(DriftAction::ResetModel.as_str(), "reset_model");
119        assert_eq!(
120            DriftAction::ResetPreprocessor.as_str(),
121            "reset_preprocessor"
122        );
123        assert_eq!(
124            DriftAction::ReplaceWithBaseline.as_str(),
125            "replace_with_baseline"
126        );
127        assert_eq!(
128            DriftAction::IncreaseAdaptationRate.as_str(),
129            "increase_adaptation_rate"
130        );
131    }
132
133    #[test]
134    fn action_is_destructive() {
135        assert!(!DriftAction::NotifyOnly.is_destructive());
136        assert!(!DriftAction::ReduceConfidence.is_destructive());
137        assert!(DriftAction::ResetModel.is_destructive());
138        assert!(DriftAction::ResetPreprocessor.is_destructive());
139        assert!(DriftAction::ReplaceWithBaseline.is_destructive());
140        assert!(DriftAction::IncreaseAdaptationRate.is_destructive());
141    }
142
143    #[test]
144    fn action_default_is_notify_only() {
145        assert_eq!(DriftAction::default(), DriftAction::NotifyOnly);
146    }
147
148    #[test]
149    fn drift_event_construction() {
150        let event = DriftEvent::new(42, DriftLevel::Drift, DriftAction::ResetModel, 1.23);
151        assert_eq!(event.sample_index, 42);
152        assert_eq!(event.level, DriftLevel::Drift);
153        assert_eq!(event.action, DriftAction::ResetModel);
154        assert!((event.detector_value - 1.23).abs() < 1e-12);
155    }
156
157    #[cfg(feature = "serde")]
158    #[test]
159    fn action_serde_roundtrip() {
160        for action in [
161            DriftAction::NotifyOnly,
162            DriftAction::ReduceConfidence,
163            DriftAction::ResetModel,
164            DriftAction::ResetPreprocessor,
165            DriftAction::ReplaceWithBaseline,
166            DriftAction::IncreaseAdaptationRate,
167        ] {
168            let json = serde_json::to_string(&action).unwrap();
169            let restored: DriftAction = serde_json::from_str(&json).unwrap();
170            assert_eq!(restored, action);
171        }
172    }
173
174    #[cfg(feature = "serde")]
175    #[test]
176    fn drift_event_serde_roundtrip() {
177        let event = DriftEvent::new(10, DriftLevel::Warning, DriftAction::ReduceConfidence, 1.5);
178        let json = serde_json::to_string(&event).unwrap();
179        let restored: DriftEvent = serde_json::from_str(&json).unwrap();
180        assert_eq!(restored.sample_index, 10);
181        assert_eq!(restored.level, DriftLevel::Warning);
182        assert_eq!(restored.action, DriftAction::ReduceConfidence);
183        assert!((restored.detector_value - 1.5).abs() < 1e-12);
184    }
185}