Getting Started with deriva

library(deriva)

What is concept drift?

A machine learning model trained on historical data operates under the implicit assumption that the data-generating process remains stable over time. When this assumption breaks down — because user behaviour shifts, sensor calibration drifts, or the world simply changes — the model’s predictions degrade without any obvious error being raised. This phenomenon is called concept drift.

Monitoring for drift requires a drift detector: an algorithm that reads a stream of per-observation signals (typically prediction errors) and raises a flag when the signal’s distribution has changed significantly. The deriva package provides a tidy interface to a catalogue of 22 such detectors, designed to compose naturally with the tidymodels ecosystem.

Quick start

The one-shot shortcut detect_drift() runs a detector over an existing column and returns the data annotated with .warning and .drift flags.

# Simulate a stream: 500 stable observations, then 500 with higher error rate
stream <- sim_drift_stream(n_pre = 500, n_post = 500,
                           p_pre = 0.05, p_post = 0.30,
                           seed = 42)

result <- detect_drift(stream, .col = error, method = "ddm")

# Where was drift flagged?
subset(result, .drift)
#> # A tibble: 3 × 5
#>       t error drift_true .warning .drift
#>   <int> <int> <lgl>      <lgl>    <lgl> 
#> 1    49     1 FALSE      FALSE    TRUE  
#> 2   388     1 FALSE      FALSE    TRUE  
#> 3   512     1 TRUE       FALSE    TRUE

The detector correctly identifies the distributional change after the known drift point (observation 500).

The deriva interface

deriva follows the same three-verb pattern as tidymodels: specify → fit → advance.

1. Specify a detector

drift_detector() creates an inert specification — no computation happens here.

spec <- drift_detector("ddm", min_instances = 30)
spec
#> Drift Detector Specification (ddm)
#>   min_instances: 30
#>   warning_level: 2
#>   out_control_level: 3

Pass method hyperparameters as named arguments. Unknown parameters raise an informative error.

2. Fit on a baseline

fit() runs the detector over the baseline period — the stable window against which future observations are compared.

baseline <- sim_drift_stream(n_pre = 300, n_post = 0, seed = 1)

fitted <- fit(spec, baseline, signal = error)
fitted
#> Fitted Drift Detector (ddm)
#>   observations: 300 (300 baseline)
#>   warnings: 18 | drifts: 3

The fitted object is immutable: it stores the internal engine state after processing the baseline, ready to receive new data.

3. Advance over new batches

advance() feeds a new batch to the detector and returns a new fitted object with the state updated and the annotated batch appended to the history. The original object is not modified.

batch1 <- sim_drift_stream(n_pre = 200, n_post = 0,   seed = 2)
batch2 <- sim_drift_stream(n_pre = 0,   n_post = 300, p_post = 0.35, seed = 3)

fitted2 <- advance(fitted,  batch1)
fitted3 <- advance(fitted2, batch2)
fitted3
#> Fitted Drift Detector (ddm)
#>   observations: 800 (300 baseline)
#>   warnings: 274 | drifts: 4

Batches can be any size — including a single observation for true streaming use.

4. Inspect results

augment() returns the full annotated history as a tibble.

history <- augment(fitted3)
tail(history[, c("t", "error", ".phase", ".warning", ".drift")], 10)
#> # A tibble: 10 × 5
#>        t error .phase .warning .drift
#>    <int> <int> <chr>  <lgl>    <lgl> 
#>  1   291     1 stream FALSE    FALSE 
#>  2   292     0 stream FALSE    FALSE 
#>  3   293     0 stream FALSE    FALSE 
#>  4   294     0 stream FALSE    FALSE 
#>  5   295     0 stream FALSE    FALSE 
#>  6   296     0 stream FALSE    FALSE 
#>  7   297     1 stream FALSE    FALSE 
#>  8   298     1 stream FALSE    FALSE 
#>  9   299     1 stream FALSE    FALSE 
#> 10   300     0 stream FALSE    FALSE

tidy() extracts the detected drift points.

tidy(fitted3)
#> # A tibble: 4 × 2
#>   index phase   
#>   <int> <chr>   
#> 1   111 baseline
#> 2   219 baseline
#> 3   293 baseline
#> 4   530 stream

glance() gives a one-row summary.

glance(fitted3)
#> # A tibble: 1 × 5
#>   method n_obs n_warning n_drift first_drift
#>   <chr>  <int>     <int>   <int>       <int>
#> 1 ddm      800       274       4         111

autoplot() plots the running mean of the signal with warning (orange) and drift (red) markers, and a dotted line separating baseline from stream.

library(ggplot2)
autoplot(fitted3)

Bridging from tidymodels

In a real workflow, the signal column comes from model predictions, not a simulation. add_prediction_error() converts the output of tidymodels’ augment() (which contains truth and estimate columns) into a .error column that drift detectors can consume.

# Simulate tidymodels augment() output for a classifier
predictions <- data.frame(
  time     = 1:8,
  truth    = factor(c("yes","no","yes","yes","no","yes","no","yes")),
  .pred_class = factor(c("yes","no","yes","no" ,"no","no" ,"no","yes"))
)

add_prediction_error(predictions, truth = truth)
#> # A tibble: 8 × 4
#>    time truth .pred_class .error
#>   <int> <fct> <fct>        <int>
#> 1     1 yes   yes              0
#> 2     2 no    no               0
#> 3     3 yes   yes              0
#> 4     4 yes   no               1
#> 5     5 no    no               0
#> 6     6 yes   no               1
#> 7     7 no    no               0
#> 8     8 yes   yes              0

For regression problems, .error is the absolute prediction error; for classification it is a 0/1 mismatch indicator.

Distribution-based detectors

Some detectors monitor the distribution of a numeric stream directly, without requiring labelled errors. These signal_type = "distribution" methods (such as "kswin" and "adwin") expect a continuous input column.

cont_stream <- sim_dist_stream(
  n_pre = 500, n_post = 500,
  mean_pre = 0, mean_post = 2,
  seed = 99
)

detect_drift(cont_stream, .col = value, method = "kswin") |>
  subset(.drift) |>
  head()
#> # A tibble: 6 × 5
#>       t   value drift_true .warning .drift
#>   <int>   <dbl> <lgl>      <lgl>    <lgl> 
#> 1   222  2.03   FALSE      NA       TRUE  
#> 2   374 -1.54   FALSE      NA       TRUE  
#> 3   491  0.0701 FALSE      NA       TRUE  
#> 4   561  2.40   TRUE       NA       TRUE  
#> 5   796  2.31   TRUE       NA       TRUE  
#> 6   980  3.20   TRUE       NA       TRUE

Available methods

deriva ships with 22 drift detectors across two signal types.

Signal type Methods
"error" (0/1 or continuous error) ddm, eddm, hddm_a, hddm_w, ewma, rddm, stepd, fhddm, fhddms, mddm_a, mddm_e, mddm_g, wstd, ftdd, fpdd, fsdd
"distribution" (numeric stream) kswin, adwin, page_hinkley, cusum, seed, seqdrift2

Use drift_detector("<method>") to inspect default hyperparameters for any method.

Summary

The core deriva workflow is:

drift_detector("ddm") |>          # specify
  fit(baseline, signal = error) |> # learn reference level
  advance(new_batch)               # update state, persist flags

Supplementary verbs — augment(), tidy(), glance(), autoplot() — follow the tidymodels convention and make it straightforward to inspect, summarise, and plot detection results at any point in the stream.