Skip to contents

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_level are 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_regimes clusters. 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 S3 print() method on Regime objects.

loss

Trajectory variable. Default is "ratio" (cumulative loss ratio). Accepts any column on the Triangle (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 the loss_ata / loss_ed pair.

Derived metrics drop the first dev row per cohort (no predecessor), then re-index dev so detection sees a contiguous sequence. See the vignette("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's attr(x, "groups") when non-empty – so detect_regime(tri) dispatches per group automatically – and otherwise falls back to pooled detection. Pass by = character(0) to force pooled detection on a multi-group Triangle, or a character vector (subset of names(x)) to dispatch on an explicit combo, e.g. by = "coverage" or by = 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 via detect_maturity(), falling back to 6L when maturity is unavailable (NA, pooled mode, or by mismatching the Triangle's attr("groups")). Cohorts with fewer than the resolved window observed periods are dropped.

method

One of "e_divisive", "hclust".

n_regimes

Integer. Number of regimes to force. NULL means auto-detect for "e_divisive"; ignored (required to equal the requested value) for "hclust", where the default is 2.

sig_level

Significance level for "e_divisive". Default 0.05.

min_size

Minimum segment size for "e_divisive". Default 3.

treatment

How downstream fits should apply this Regime when $changes contains 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 factors f_1, ..., f_(K-1) and every cohort can be projected to the full development length K. One of:

"segment_bridged"

(default) Pool the whole bridged band into a single factor estimate – every cohort at dev k uses the same f_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 S3 summary() method.

Value

An object of class "Regime". For single-group input:

call

Matched call.

method

Detection method used.

loss

Trajectory variable used for detection.

window

Trajectory window per combo. Scalar integer when a single combo was analysed; integer vector (one per surviving combo, in the order of $labels / $changes group rows) otherwise.

window_mode

Either "auto" (resolved per group via detect_maturity()) or "manual" (user-supplied integer).

cohort

Period variable from x.

labels

data.table of one row per analysed cohort: [by..., cohort, regime, regime_id]. Group columns are prepended when by resolves to a non-empty vector.

changes

data.table of 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 is regime_id - 1); matches $labels$regime_id. pre_value / post_value are the mean loss over 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_regimes

Number of regimes detected. Scalar integer for single-combo detection; named integer vector (keyed by combo) for multi-combo.

trajectory

Cohort x dev feature matrix used for detection. Single matrix when single combo; named list of matrices for multi-combo.

pca

prcomp object (single combo) or named list of prcomp objects (multi-combo).

dropped

Cohorts excluded due to the window window constraint. Vector (single) / named list (multi).

multi_group

Logical flag; TRUE when detection ran over multiple group combos.

treatment

Either "segment_bridged" or "segment_bridged_borrowed" – the value supplied via the treatment argument. 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)
} # }