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.
This vignette works through credit_monitoring, a dataset
shipped with the package: the per-observation error stream of a
credit-approval classifier in production.
head(credit_monitoring)
#> t error drift_true
#> 1 1 0 FALSE
#> 2 2 0 FALSE
#> 3 3 0 FALSE
#> 4 4 0 FALSE
#> 5 5 0 FALSE
#> 6 6 0 FALSEFor its first 500 observations the model runs at its expected 5%
error rate. After observation 500, a shift in the credit market raises
the error rate to 30% — drift_true records this ground
truth, which a real deployment would not have access to
(deriva’s job is to infer it from error
alone).
The one-shot shortcut detect_drift() runs a detector
over an existing column and returns the data annotated with
.warning and .drift flags.
result <- detect_drift(credit_monitoring, .col = error, method = "ddm")
# Where was drift flagged?
subset(result, .drift)
#> # A tibble: 1 × 5
#> t error drift_true .warning .drift
#> <int> <int> <lgl> <lgl> <lgl>
#> 1 542 1 TRUE FALSE TRUEThe detector correctly identifies the distributional change shortly after the known drift point (observation 500), with no false drift detections in the 500 stable observations before it.
deriva follows the same three-verb pattern as
tidymodels: specify → fit → advance.
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
#> keep: 10000Pass method hyperparameters as named arguments. Unknown parameters, and values outside a parameter’s valid range, raise an informative error.
fit() runs the detector over the baseline
period — the stable window against which future observations
are compared. Here, that is the first 500 observations, before the
market shift.
baseline <- credit_monitoring[credit_monitoring$t <= 500, ]
fitted <- fit(spec, baseline, signal = error)
fitted
#> Fitted Drift Detector (ddm)
#> observations: 500 (500 baseline)
#> warnings: 6 | drifts: 0The fitted object is immutable: it stores the internal engine state after processing the baseline, ready to receive new data.
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 <- credit_monitoring[credit_monitoring$t >= 501 & credit_monitoring$t <= 700, ]
batch2 <- credit_monitoring[credit_monitoring$t >= 701, ]
fitted2 <- advance(fitted, batch1)
fitted3 <- advance(fitted2, batch2)
fitted3
#> Fitted Drift Detector (ddm)
#> observations: 1000 (500 baseline)
#> warnings: 24 | drifts: 1Batches can be any size — including a single observation for true
streaming use. The market shift (observation 501) falls inside
batch1; deriva flags it there, and
batch2 confirms the model has settled into its new, worse
error rate with no further alarms.
augment() returns the retained history
as a tibble (the last keep rows of the fitted object — see
?drift_detector).
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 991 0 stream FALSE FALSE
#> 2 992 1 stream FALSE FALSE
#> 3 993 0 stream FALSE FALSE
#> 4 994 0 stream FALSE FALSE
#> 5 995 0 stream FALSE FALSE
#> 6 996 0 stream FALSE FALSE
#> 7 997 0 stream FALSE FALSE
#> 8 998 1 stream FALSE FALSE
#> 9 999 0 stream FALSE FALSE
#> 10 1000 0 stream FALSE FALSEtidy() extracts the detected drift
points.
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 1000 24 1 542autoplot() plots the running mean of
the signal with warning (orange) and drift (red) markers, and a dotted
line separating baseline from stream.
In a real workflow, the signal column comes from model predictions,
not a shipped dataset. 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 0For regression problems, .error is the absolute
prediction error; for classification it is a 0/1 mismatch indicator.
Chaining straight into fit() needs care:
fit()’s first argument is the spec, not the data,
so build the annotated data first and pass the spec and data to
fit() explicitly:
Some detectors monitor the distribution of a numeric stream directly,
without requiring labelled errors — see
vignette("distribution-detectors") for a worked example
with "kswin".
deriva ships with 22 drift detectors across two signal
types.
| Signal type | Methods |
|---|---|
"error" (0/1 errors) |
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.
The core deriva workflow is:
drift_detector("ddm") |> # specify
fit(baseline, signal = error) |> # learn reference level
advance(new_batch) # update state, persist flagsSupplementary 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.