Detect structural change points in the sequence of cohort-level
development trajectories. Each underwriting cohort (indexed by the
cohort of a "Triangle" object) is treated as a feature vector
whose entries are the selected loss metric observed at development
periods 1, ..., window. Cohorts are then ordered by underwriting
period and tested for structural shifts in the multivariate
sequence.
Multi-group Triangle inputs are supported: detection runs
independently per group, and results are combined into a single
Regime object whose $changes, $labels, etc. carry the
group column. Single-group input retains the original scalar /
Date-vector / matrix layout for backward compatibility.
Two detection strategies are supported:
"e_divisive"Multivariate non-parametric divisive change-point detection on the energy statistic (Matteson & James 2014). The number of regimes is determined by the data; only changes significant at
sig_levelare retained. Preferred when the number of regimes is not known in advance."hclust"Ward hierarchical clustering on the scaled cohort feature matrix, cut to
n_regimesclusters. Ignores time ordering – useful as a sanity check since non-adjacent cohorts may cluster together if the trajectory pattern is not strictly chronological.
Usage
detect_regime(
x,
loss = "ratio",
by = NULL,
window = "auto",
method = c("e_divisive", "hclust"),
n_regimes = NULL,
sig_level = 0.05,
min_size = 3L,
treatment = c("segment_bridged", "segment_bridged_borrowed"),
...
)
# S3 method for class 'Regime'
print(x, ...)
# S3 method for class 'Regime'
summary(object, ...)
# S3 method for class 'summary.Regime'
print(x, ...)Arguments
- x
An object of class
"Triangle". May contain one or more groups (per-group detection runs independently). Also used by S3print()method onRegimeobjects.- loss
Trajectory variable. Default is
"ratio"(cumulative loss ratio). Accepts any column on theTriangle(e.g."ratio","loss","premium","incr_loss","incr_premium"), plus three diagnostic derived metrics computed inline per (group, cohort):"loss_ata"Loss age-to-age factor
loss[k+1] / loss[k]– multiplicative loss development speed (CL $f_k$)."premium_ata"Premium age-to-age factor – same form on premium.
"loss_ed"Loss intensity
(loss[k] - loss[k-1]) / premium[k-1]– additive, exposure-anchored (ED model's $g_k$)."premium_ed"Alias of
"premium_ata"– the two differ only by a constant(premium_ata - 1), and the PCA standardization in detection removes that shift, so they yield identical regime changes. Provided for API symmetry with theloss_ata/loss_edpair.
Derived metrics drop the first dev row per cohort (no predecessor), then re-index
devso detection sees a contiguous sequence. See thevignette("diagnostics")"Choice of loss" section for guidance on which loss metric matches which suspected event.- by
Grouping column(s) for per-combination detection.
NULL(default) reuses the Triangle'sattr(x, "groups")when non-empty – sodetect_regime(tri)dispatches per group automatically – and otherwise falls back to pooled detection. Passby = character(0)to force pooled detection on a multi-group Triangle, or a character vector (subset ofnames(x)) to dispatch on an explicit combo, e.g.by = "coverage"orby = c("channel", "coverage").- window
Trajectory window. Integer (e.g.,
12L) for a fixed window, or the string"auto"(default) – resolves to each group's maturity viadetect_maturity(), falling back to6Lwhen maturity is unavailable (NA, pooled mode, orbymismatching the Triangle'sattr("groups")). Cohorts with fewer than the resolvedwindowobserved periods are dropped.- method
One of
"e_divisive","hclust".- n_regimes
Integer. Number of regimes to force.
NULLmeans auto-detect for"e_divisive"; ignored (required to equal the requested value) for"hclust", where the default is2.- sig_level
Significance level for
"e_divisive". Default0.05.- min_size
Minimum segment size for
"e_divisive". Default3.- treatment
How downstream fits should apply this Regime when
$changescontains multiple change points. Both modes mask the triangle to a bridged development band: each segment's natural mini-triangle wall (dev >= max_cal - seg_last + 1) is widened by a calendar-diagonal bridge anchored at the next (newer) segment's first-cohort midpoint dev. The bridge closes the factor gaps that would otherwise open at the segment boundaries, so the band carries a continuous run of age-to-age factorsf_1, ..., f_(K-1)and every cohort can be projected to the full development lengthK. One of:"segment_bridged"(default) Pool the whole bridged band into a single factor estimate – every cohort at dev
kuses the samef_k, estimated from whichever cohorts reach that dev inside the band (the most recent ones, by construction). Not a per-segment fit: the development pattern is treated as shared across regimes, only the band's lower boundary is regime-aware."segment_bridged_borrowed"Estimate factors per segment (early-dev factors stay regime-specific), then borrow the late-dev factors a segment cannot estimate from another segment that can (the bridge guarantees a donor exists). Each cohort projects with its own segment's factors where available and borrowed factors beyond its reach.
- ...
Reserved for future use.
- object
An object of class
"Regime". Used by the S3summary()method.
Value
An object of class "Regime". For single-group input:
callMatched call.
methodDetection method used.
lossTrajectory variable used for detection.
windowTrajectory window per combo. Scalar integer when a single combo was analysed; integer vector (one per surviving combo, in the order of
$labels/$changesgroup rows) otherwise.window_modeEither
"auto"(resolved per group viadetect_maturity()) or"manual"(user-supplied integer).cohortPeriod variable from
x.labelsdata.tableof one row per analysed cohort:[by..., cohort, regime, regime_id]. Group columns are prepended whenbyresolves to a non-empty vector.changesdata.tableof detected regime changes with columns[by..., change, regime_id, pre_value, post_value, magnitude].regime_id= id of the regime that STARTS at this change (the pre-change regime isregime_id - 1); matches$labels$regime_id.pre_value/post_valueare the meanlossover the cohort x dev trajectory windows in the pre- / post-change regimes;magnitude = |post_value - pre_value|. Empty (zero rows) when no change is detected.n_regimesNumber of regimes detected. Scalar integer for single-combo detection; named integer vector (keyed by combo) for multi-combo.
trajectoryCohort x dev feature matrix used for detection. Single matrix when single combo; named list of matrices for multi-combo.
pcaprcompobject (single combo) or named list ofprcompobjects (multi-combo).droppedCohorts excluded due to the
windowwindow constraint. Vector (single) / named list (multi).multi_groupLogical flag;
TRUEwhen detection ran over multiple group combos.treatmentEither
"segment_bridged"or"segment_bridged_borrowed"– the value supplied via thetreatmentargument. Read by downstream fits (fit_ata(),fit_intensity(),fit_cl(),fit_ed()) to decide whether to pool the bridged band into a single factor estimate or estimate per-segment factors and borrow the late-dev factors a segment cannot reach.
Examples
if (FALSE) { # \dontrun{
data(experience)
tri_sur <- as_triangle(
experience[coverage == "surgery"],
groups = "coverage",
cohort = "uy_m",
calendar = "cy_m",
loss = "incr_loss",
premium = "incr_premium"
)
# Hierarchical clustering (no extra package dependency)
r <- detect_regime(tri_sur, method = "hclust",
n_regimes = 2L)
print(r)
summary(r)
plot(r)
# Energy-statistic divisive change-point detection
r_ed <- detect_regime(tri_sur, method = "e_divisive")
# Multi-group: detection per coverage
tri_all <- as_triangle(
experience,
groups = "coverage",
cohort = "uy_m",
calendar = "cy_m",
loss = "incr_loss",
premium = "incr_premium"
)
r_all <- detect_regime(tri_all, by = "coverage", method = "e_divisive")
print(r_all$changes)
} # }
