use crate::drift::action::{DriftAction, DriftEvent};
use crate::drift::detector::DriftDetector;
use crate::drift::strategy::DriftStrategy;
use crate::error::RillError;
use crate::traits::OnlineRegressor;
const DEFAULT_MAX_EVENTS: usize = 1000;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DriftAwareModel<M, D, A>
where
D: DriftDetector,
A: DriftStrategy,
{
model: M,
detector: D,
strategy: A,
events: Vec<DriftEvent>,
max_events: usize,
samples_seen: u64,
last_action: Option<DriftAction>,
}
impl<M, D, A> DriftAwareModel<M, D, A>
where
M: OnlineRegressor,
D: DriftDetector,
A: DriftStrategy,
{
pub fn new(model: M, detector: D, strategy: A) -> Self {
Self {
model,
detector,
strategy,
events: Vec::new(),
max_events: DEFAULT_MAX_EVENTS,
samples_seen: 0,
last_action: None,
}
}
pub fn with_max_events(
model: M,
detector: D,
strategy: A,
max_events: usize,
) -> Result<Self, RillError> {
if max_events == 0 {
return Err(RillError::InvalidCapacity(max_events));
}
Ok(Self {
model,
detector,
strategy,
events: Vec::with_capacity(max_events),
max_events,
samples_seen: 0,
last_action: None,
})
}
pub fn predict(&self, features: &[f64]) -> Result<f64, RillError> {
self.model.predict(features)
}
pub fn learn(&mut self, features: &[f64], target: f64) -> Result<(), RillError> {
let prediction = self.model.predict(features)?;
let error = (target - prediction).abs();
let level = self.detector.update(error)?;
let action = self.strategy.decide(level, self.samples_seen);
if level.is_change() {
let event =
DriftEvent::new(self.samples_seen, level, action, self.detector.last_value());
self.events.push(event);
while self.events.len() > self.max_events {
self.events.remove(0);
}
}
if action == DriftAction::ResetModel {
self.model.reset();
}
self.last_action = Some(action);
self.model.learn(features, target)?;
self.samples_seen += 1;
Ok(())
}
pub const fn model(&self) -> &M {
&self.model
}
pub const fn detector(&self) -> &D {
&self.detector
}
pub const fn strategy(&self) -> &A {
&self.strategy
}
pub fn events(&self) -> &[DriftEvent] {
&self.events
}
pub const fn last_action(&self) -> Option<DriftAction> {
self.last_action
}
pub const fn samples_seen(&self) -> u64 {
self.samples_seen
}
pub const fn max_events(&self) -> usize {
self.max_events
}
pub fn reset(&mut self) {
self.model.reset();
self.detector.reset();
self.events.clear();
self.samples_seen = 0;
self.last_action = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::drift::detector::DriftLevel;
use crate::drift::page_hinkley::PageHinkley;
use crate::drift::strategy::StaticStrategy;
use crate::models::{BaselineConfig, MeanRegressor};
fn build(
strategy: StaticStrategy,
) -> DriftAwareModel<MeanRegressor, PageHinkley, StaticStrategy> {
let model = MeanRegressor::new(BaselineConfig::default()).unwrap();
let detector = PageHinkley::default();
DriftAwareModel::new(model, detector, strategy)
}
#[test]
fn predict_delegates_to_model() {
let aware = build(StaticStrategy::default());
let p = aware.predict(&[]).unwrap();
assert!((p - 0.0).abs() < 1e-12);
}
#[test]
fn learn_feeds_error_to_detector() {
let mut aware = build(StaticStrategy::default());
assert_eq!(aware.detector().samples_seen(), 0);
aware.learn(&[], 1.0).unwrap();
assert_eq!(aware.detector().samples_seen(), 1);
aware.learn(&[], 2.0).unwrap();
assert_eq!(aware.detector().samples_seen(), 2);
}
#[test]
fn samples_seen_tracks_learn_calls() {
let mut aware = build(StaticStrategy::default());
assert_eq!(aware.samples_seen(), 0);
for i in 0..10u64 {
aware.learn(&[], i as f64).unwrap();
}
assert_eq!(aware.samples_seen(), 10);
}
#[test]
fn last_action_updated_after_learn() {
let mut aware = build(StaticStrategy::default());
assert_eq!(aware.last_action(), None);
aware.learn(&[], 1.0).unwrap();
assert_eq!(aware.last_action(), Some(DriftAction::NotifyOnly));
}
#[test]
fn default_strategy_does_not_reset_model() {
let mut aware = build(StaticStrategy::default());
for i in 0..20 {
aware.learn(&[], i as f64).unwrap();
}
assert!(aware.model().samples_seen() > 0);
let before = aware.model().samples_seen();
aware.learn(&[], 100.0).unwrap();
assert_eq!(aware.model().samples_seen(), before + 1);
}
#[test]
fn reset_model_action_calls_model_reset() {
let strategy = StaticStrategy::new(DriftAction::NotifyOnly, DriftAction::ResetModel);
let mut aware = build(strategy);
for _ in 0..30 {
aware.learn(&[], 1.0).unwrap();
}
assert!(aware.model().samples_seen() > 0);
let mut reset_happened = false;
for _ in 0..100 {
aware.learn(&[], 100.0).unwrap();
if aware.model().samples_seen() < aware.samples_seen() {
reset_happened = true;
break;
}
}
assert!(
reset_happened,
"ResetModel action should have reset the model"
);
assert!(!aware.events().is_empty());
}
#[test]
fn detects_drift_and_records_event() {
let strategy = StaticStrategy::new(DriftAction::NotifyOnly, DriftAction::NotifyOnly);
let mut aware = build(strategy);
for _ in 0..50 {
aware.learn(&[], 1.0).unwrap();
}
assert!(aware.events().is_empty());
let mut drift_recorded = false;
for _ in 0..100 {
aware.learn(&[], 50.0).unwrap();
if let Some(last) = aware.events().last()
&& last.level == DriftLevel::Drift
{
drift_recorded = true;
break;
}
}
assert!(drift_recorded, "a drift event should have been recorded");
let last_event = aware.events().last().unwrap();
assert_eq!(last_event.level, DriftLevel::Drift);
}
#[test]
fn replace_with_baseline_action_recorded() {
let strategy =
StaticStrategy::new(DriftAction::NotifyOnly, DriftAction::ReplaceWithBaseline);
let mut aware = build(strategy);
for _ in 0..50 {
aware.learn(&[], 1.0).unwrap();
}
let mut seen_action = false;
for _ in 0..200 {
aware.learn(&[], 100.0).unwrap();
if let Some(action) = aware.last_action()
&& action == DriftAction::ReplaceWithBaseline
{
seen_action = true;
break;
}
}
assert!(
seen_action,
"ReplaceWithBaseline action should have been recorded"
);
}
#[test]
fn increase_adaptation_rate_action_recorded() {
let strategy =
StaticStrategy::new(DriftAction::NotifyOnly, DriftAction::IncreaseAdaptationRate);
let mut aware = build(strategy);
for _ in 0..50 {
aware.learn(&[], 1.0).unwrap();
}
let mut seen_action = false;
for _ in 0..200 {
aware.learn(&[], 100.0).unwrap();
if let Some(action) = aware.last_action()
&& action == DriftAction::IncreaseAdaptationRate
{
seen_action = true;
break;
}
}
assert!(
seen_action,
"IncreaseAdaptationRate action should have been recorded"
);
}
#[test]
fn events_bounded_by_max_events() {
let strategy = StaticStrategy::new(DriftAction::NotifyOnly, DriftAction::NotifyOnly);
let model = MeanRegressor::new(BaselineConfig::default()).unwrap();
let detector = PageHinkley::default();
let mut aware = DriftAwareModel::with_max_events(model, detector, strategy, 3).unwrap();
assert_eq!(aware.max_events(), 3);
for i in 0..500 {
let target = if i % 50 < 25 { 0.0 } else { 100.0 };
aware.learn(&[], target).unwrap();
}
assert!(aware.events().len() <= 3);
}
#[test]
fn reset_clears_all_state() {
let mut aware = build(StaticStrategy::default());
for i in 0..20 {
aware.learn(&[], i as f64).unwrap();
}
assert_eq!(aware.samples_seen(), 20);
assert!(aware.last_action().is_some());
aware.reset();
assert_eq!(aware.samples_seen(), 0);
assert_eq!(aware.last_action(), None);
assert!(aware.events().is_empty());
assert_eq!(aware.model().samples_seen(), 0);
assert_eq!(aware.detector().samples_seen(), 0);
}
#[test]
fn rejects_zero_max_events() {
let model = MeanRegressor::new(BaselineConfig::default()).unwrap();
let detector = PageHinkley::default();
let strategy = StaticStrategy::default();
let result = DriftAwareModel::with_max_events(model, detector, strategy, 0);
assert!(result.is_err());
}
#[test]
fn model_detector_strategy_accessors() {
let strategy = StaticStrategy::new(DriftAction::ReduceConfidence, DriftAction::ResetModel);
let aware = build(strategy);
assert_eq!(
aware.strategy().decide(DriftLevel::Warning, 0),
DriftAction::ReduceConfidence
);
assert_eq!(
aware.strategy().decide(DriftLevel::Drift, 0),
DriftAction::ResetModel
);
assert_eq!(aware.detector().samples_seen(), 0);
assert_eq!(aware.model().samples_seen(), 0);
}
#[cfg(feature = "serde")]
#[test]
fn serde_roundtrip() {
let strategy = StaticStrategy::new(DriftAction::NotifyOnly, DriftAction::ResetModel);
let model = MeanRegressor::new(BaselineConfig::default()).unwrap();
let detector = PageHinkley::default();
let mut aware = DriftAwareModel::new(model, detector, strategy);
for i in 0..10 {
aware.learn(&[], i as f64).unwrap();
}
let json = serde_json::to_string(&aware).unwrap();
let restored: DriftAwareModel<MeanRegressor, PageHinkley, StaticStrategy> =
serde_json::from_str(&json).unwrap();
assert_eq!(restored.samples_seen(), aware.samples_seen());
assert_eq!(restored.last_action(), aware.last_action());
assert_eq!(restored.events().len(), aware.events().len());
}
}