The stabilisation release. It settles the contract before 1.0 freezes it
(classed conditions, versioned results, a JSON schema, real ggproto
layers, a changepoint convention verified across every engine), connects
what already existed (families the engines could fit but nobody could ask
for, a formula interface the regression engines had and cpt_detect()
could not reach), and replaces the package's hand-written claims about its
engines with measurements that ship as data.
Read these first: each changes what an existing call returns.
changepoint engines (pelt, binseg, amoc, np) and
binsegrcpp now use a minimum segment of two observations for a change
in mean. The engines' own default of one let a single observation be a
segment, so an outlier became two changepoints and a monotone series was
split after every point. segneigh, which refuses minseglen > 1, and
series shorter than four observations keep the old behaviour; pass
minseglen = 1 (or min_segment = 1) to restore it. On wide noise the
documented shattering of a raw-scale penalty drops from 39 and 141
changepoints to 27 and 76 (?cpt_detect).cpm's arl0 now defaults to the smallest average run length it ships
that is at least five times the series length, instead of 500 whatever
the length. On a series of 1,000 the old default expected two false
alarms; the expected count is now recorded on the result
($diagnostics$expected_false_positives).cpt_consensus()'s min_votes defaults to "majority" (more than half
the methods that ran). For the default three methods that is the old
default of 2; for larger panels it is stricter. "all" asks for every
method.envcpt: the autoregressive models reported rows of a lagged design, one
or two positions early, and the design's last row as a changepoint. Both
fixed; found by the new convention test.ocd: cp was the declaration time, one after the last observation
consistent with the change; it is now the declaration time minus one,
with the declaration itself in declared_at. Found by the same test.binsegrcpp with a non-normal distribution used a BIC on the wrong
scale and chose no change; it now uses the distribution's own likelihood.beast detects on the centred series (its answer depended on the
series' level) and returns the fitted trend and seasonal signal.cpt_monitor("cpm") given a baseline containing a change no longer
alarms on the first monitored observation: the model restarts after
each change inside the baseline, and a warning says the baseline was not
in control.cpt_detect() gains family: "gaussian", "poisson", "binomial",
"exponential", "gamma", "laplace" or "l1". Counts, 0/1 outcomes
and waiting times get a cost written for them, through the engines that
already had one: changepoint's test statistics, binsegRcpp's
distributions, fastcpd's families, stepR::smuceR(), cpm's
parametric statistics, ocp and segmented's GLMs.cpt_families() is the translation table: every legal combination
of method, change type and family, and the engine call it becomes. A
combination not in it is refused by name, with the methods that can fit
the family.cpt_simulate() and cpt_power() gain family, so a power curve can be
drawn for the data a study will actually collect.smuce_wrapper() accepts family = "poisson" and "binomial", and
fastcpd_wrapper() accepts thirteen of fastcpd's families (among them
lm, binomial, poisson, exponential, lasso, arima and var);
0.5.0 exposed six. glm, mvtnorm and a custom cost are not wired.cpt_detect(y ~ x1 + x2, data = d, method = ) reaches strucchange,
segmented and fastcpd with covariates, and y ~ 1 any univariate
method. The result carries per-segment coefficients with standard errors
(tidy(fit, what = "coefficients")), fitted values, and the design.cpt_segment_models() fits your own model to every segment and
returns them as a table with tidy(), glance() and print(); new
predict() for a result forecasts from the last segment (or any other).segmented_wrapper() takes a formula with data, the covariate that
breaks as seg_z (a name or ~ t), and family.tsibbles and long data frames with
group = are detected one series per group and returned as a
ggcpt_batch with the grouping columns.na_action: "error" (the default) refuses missing values and says
how to proceed; "omit" detects on what was observed and reports every
location in the original positions, gaps kept in $data; "engine"
passes them to the engines measured to handle them.
cpt_methods()$na_handling records what each engine does with a missing
value, measured ("native", "compacts", "silent_loss", "reject").fixed: changepoints known in advance, kept, with the rest estimated
around them. within: windows the changepoints must fall in.
min_segment: a minimum segment length translated into each engine's own
argument (cpt_methods()$min_segment names it). min_effect: drop
changes smaller than a given number of noise standard deviations.
Locations are positions or values of the index; on a numeric index such
as a ts's years, a number outside 1..n is read as a year, as
cpt_annotate_events() already did.keep_fit = FALSE drops the engine's own object, which for
strucchange is most of a result's size; a fit over 10 MB warns once per
method per session.cpt_effect(): the size of each change in the data's units and in
noise standard deviations, with an interval, the percentage change, the
change in spread, and the rate, odds or hazard ratio for the families.
method = "split" locates on half the observations and measures on the
other half, removing the winner's curse.cpt_test_at(): a test for a change at a date fixed in advance, the
statistically easy case and the commonest applied question, with an
exact test per family, a permutation test over a window, and a Chow test
for a formula.cpt_attribute_event(): is a known event inside a detected change's
interval?cpt_test_null(): a test for a change anywhere (CUSUM, Pettitt,
sup-F), which locates nothing and so selects nothing.cpt_null_power(), and an empty result now says which kind of empty
it is: a constant series, or a series where the smallest detectable shift
is reported.cpt_test() gains relevance, a test for a change larger than a given
number of noise standard deviations.cpt_assumptions(): residual dependence, scale sensitivity, the
expected number of false positives, count plausibility and data type,
each flagged with the measured alternative. cpt_report() prints it.cpt_gof() and autoplot(fit, type = "diagnostics"): residuals,
their autocorrelation, and per-segment checks.cpt_robustness(): re-runs a detector under each setting of its
noise-model argument and reports which changepoints survive, the check
that touches the commonest cause of a false positive.cpt_stability() gains a block bootstrap and a reversal check, takes a
fit, and counts failed replicates instead of treating them as "found
nothing".$diagnostics$residual_dependence).data-raw/:
cpt_runtimes (every engine up to a million observations),
cpt_invariances (scale, shift and reversal), cpt_noise_benchmark
(false positives and power under four noise regimes),
cpt_data_types (Gaussian, Bernoulli, Poisson and proportion data),
cpt_null_sizes (false alarms on pure noise at scale) and
cpt_calibration (the package's own intervals, tests and monitors
against their promises).cpt_methods() gains the vocabulary columns (families, choices,
formula, min_segment, na_handling, cp_convention_upstream) and
the measured ones (scale_invariant, sequential, max_cp, tier,
noise_model_arg, rate_arg, cost, max_n).cpt_recommend() is rebuilt on the measurements: it scores every method
on its measured errors in the stated noise regime and data type, returns
the call to run (with the noise-model or family argument the score
depends on), the measured hits and false positives, ties, reasons and
caveats; it takes n_expected, data_type, family, jump (simulated
power) and a fit to read the rest from; and it has an autoplot().?ggchangepoint-conditions): refusals carry their data (method,
requested, supported, ...) and no call; an engine's own failure is
re-signalled as ggchangepoint_engine_error keeping the engine's call.
Fan-outs (cpt_consensus(), cpt_benchmark(), ...) record a method that
fails and raise anything else.... name that is a near miss of a real argument is refused instead of
being swallowed by an engine.seed means the same for every method: taken by the wrappers that have
one, scoped around the fit for the rest.fit$versions, glance()'s engine_version); print() notes when the
installed engine has moved on, and new cpt_verify() re-runs a result
and reports what changed.as_json() writes a result under a documented, versioned schema;
new cpt_export() and cpt_import() write and read JSON or CSV;
cpt_report(format = "json").ggplot2 extensions (GeomChangepoint,
GeomCptSegment, GeomCptCi, GeomCptRegion, GeomCptLabel,
GeomCptEvent, StatChangepoint, StatCptRegion), each with its own
legend glyph. New stat_cpt_region() draws significance regions or
location intervals computed in the layer.cpt_metrics(), cpt_metrics_annotated() and ggcpt_eval() take a fit
(and cpt_metrics() a monitor); cpt_metrics() adds covering_floor
(what the trivial answer scores) and covering_scaled.cpt_crops() and cpt_stability() take a fit.cpt_select() warns when AIC sits at the top of the ladder and when a
criterion picks the smallest candidate.cpt_consensus() reports its panel's disagreement and each method's
count.augment() gives per-coordinate fitted values and residuals for a
multivariate fit; glance() gains engine_version and family.bocpd and envcpt return a fitted signal,
npmojo a detector statistic and strucchange a solution path.?ggcpt_compare and the comparison vignette say plainly what choosing a
method by its answer does to a p-value.cpt_confint(method = "bootstrap"),
cpt_stability(), cpt_select(), cpt_influence(),
cpt_sensitivity(), cpt_robustness(), cpt_effect(method = "split")
and cpt_verify()) replay its missing-value handling and constraints
(na_action, fixed, within, min_segment, min_effect). On a fit
made with na_action = "omit" they failed replicate by replicate, and a
fixed changepoint was re-estimated like any other; its bootstrap interval
is now the point itself. The bootstraps keep the gaps where they are, and
cpt_test_at() and cpt_test_null() take such a fit.na_action = "omit", $constraints and a regression fit's
$coefficients are in original positions, like everything else.within window may reach either end of the series; a location that
cannot be placed says whether it was missing, not in the index, or
before the start. print() names the within, min_segment and
min_effect of a fit, with how many changepoints each dropped, as it
already named fixed.cpt_segment_models() and predict() work for formulas with
transformed or factor terms, and predict() by horizon on a model with
covariates says it needs newdata.cpt_import() reads back a result with gaps, a timestamp index's time
zone and every coordinate of a multivariate result (the JSON gains
index$tz and data$coordinates, the CSV one value_<name> column per
coordinate).cpt_test_null() gives p = 1 on a constant series (CUSUM returned no row
and sup-F p = 1e-05), and sup-F refuses fewer than 14 observations.cpt_test_at(window = ) finds the best split by its likelihood ratio.
Its statistic was infinite for any p below 1e-16, so every strong
candidate tied and the window's first one won.cpt_null_power() simulates a count, binary or waiting-time fit in its
own family, and cpt_min_detectable() with such a family adapts its
search range and reports the change in the family's parameter.family = with the default change_in goes to the method's native
change type (segmented fits Poisson slopes), as it does without one.cpm's FET statistic takes cpm's documented default lambda = 0.1 when
none is given, which cpm itself fails to apply, so cpt_detect(method = "cpm", family = "binomial"), the call cpt_recommend() suggests for
binary data, runs.fcov and fmean rescale the curves before fChange sees them:
measured on pure noise, fcov found 109 changepoints in 120 curves at a
thousandth of the units and none at unit scale.binsegrcpp takes a min_segment longer than its default search had
room for; a min_segment that leaves no room for a change at all is
refused by name, and a fixed stretch shorter than two of them is left
unsearched instead of failing the fit.cpt_detect(na_action = "omit").cpt_recommend(change_in = "regression") (or a formula fit) recommends
the formula methods instead of none.stat_cpt_region() hands the detector the arguments it takes, so NSP's
alpha sets its level rather than the band's transparency.change_in = "meanvar" only
where the method offers it, and ?cpt_detect lists envcpt and var
among the scale-sensitive engines, as cpt_invariances measured.as_cpt_series() has.cpt_consensus(), ggcpt_compare(), cpt_stability() and
cpt_sensitivity() accept a series with gaps when na_action is passed
through ...; cpt_influence() and cpt_sensitivity() refuse a result
they cannot re-run before trying.segmented keeps its fitted signal under na_action = "engine".cpt_crops() and cpt_scale_space() take a fit with gaps. The scale
space handed them to mosum, which is measured to lose changepoints on a
missing value without saying so.cpt_penalty("Manual", value = ), NA for a choice argument
(method, family, ...), method = "", and a JSON report's bad file.cpt_simulate(family = ) records signal and true_segments as the
Gaussian path does, and warns when params has unused values.tidy() methods, as every other result
class does: cpt_effect(), cpt_test_at(), cpt_assumptions(),
cpt_gof(), cpt_robustness(), cpt_verify() and cpt_null_power().Every analysis tool run against fits of every shape: no changepoints, gaps, dates, a slope, a formula, fixed locations, small units.
cpt_stability(), cpt_select(criterion = "stability") and cpt_influence(type = "outlier") resample a slope fit
around its fitted line (the engine's signal, or a least-squares line per
segment for not and nsp) and a seasonal fit around its cycle.
Resampled around the segment means, every replicate of a clean slope
change came back flat: the interval collapsed onto the point and the
stability was 0. The residual checks of cpt_gof(),
cpt_assumptions() and the diagnostics a fit records use the same line.cpt_influence(), cpt_leverage(), cpt_sensitivity(), cpt_select()
and cpt_null_power() refuse a formula fit, as the other tools that
re-run a fit already did: its covariates are not stored on it, and they
re-ran an intercept-only search in its place.cpt_select() warns when its criterion scores a ladder of another change
type as a change in the mean (measured, it chose K = 0 on a variance
change and K = 19 on a slope), and no longer repeats the
implausible-count warning of every over-segmenting rung.cpt_test() reports an estimate for a fit with gaps (it was NA
beside a valid statistic), and its Welch fallback warns that it tests a
shift in the mean when the fit found a change in variance, slope or
distribution.cpt_detect(fixed = ) is
selection_adjusted = TRUE in cpt_test() and cpt_effect(): the data
did not locate it.cpt_effect() warns that segment means do not measure a change in
slope, and names cpt_segment_models(), which fits the lines.cpt_null_power() says what its answer measures for a change in
variance, in mean and variance, or in slope, where it called every one a
shift in the mean; tidy() gains change_in.cpt_confint(),
cpt_stability(), cpt_influence(), cpt_sensitivity(),
cpt_select(), cpt_power(), cpt_min_detectable(),
cpt_null_power()) raise each kind of warning once per call, not once
per replicate: on a series in small units cpt_confint(B = 100) warned
100 times.cpt_assumptions(), cpt_gof(), cpt_effect(),
cpt_segment_models() or cpt_power() result drops its class, so it
prints like any tibble; print() failed or warned about the columns
that were gone.tidy() works on cpt_stability(), cpt_min_detectable(),
cpt_label_error_curve() and cpt_learn_penalty() results.call no longer holds the data or a function's source.
do.call(cpt_detect, list(x, ...)) stored the whole series and the
source of cpt_detect(), so as_json(fit, data = FALSE) still wrote
every value (31 KB for 1,000 points) and cpt_report() printed them
under "Call".cpt_import() restores the call a JSON result was written with. Reports
showed the importer's own code, and cpt_verify() re-ran without the
engine arguments the call held (a Q = 1 binseg fit read back as
"CHANGED"). A CSV import records no call.cpt_verify() refuses a result that no cpt_detect() call made
(as_ggcpt(), the fit cpt_select() returns), which it re-ran at the
defaults and reported as changed.group = or grouped data, and so does
cpt_test_at(), where every group was fitted as one stacked regression
with a spurious break at the join.print() formats both ends of a timestamp index alike, and a fit passed
where a series belongs (cpt_consensus(fit)) says how to pass the
series.seed that is not a
single number (every seeded tool, "supplied seed is not a valid
integer"), an infinite fixed position or when, cpt_labels() with a
missing bound, cpt_attribute_event() with no events, an
cpt_annotate_events() location or label that is not one column
name, a non-finite influence subset, an empty or non-numeric
cpt_power() grid, and predict(level = ) outside (0, 1).
cpt_assumptions() reports a Ljung-Box lag too large for the series
instead of failing on it.cpt_simulate() refuses params that are not finite numbers (a string
failed in the arithmetic; NA or Inf returned a series of NA or
Inf) and a missing changepoint, which it dropped without a word.cpt_attribute_event() reports an event position before the first or
after the last observation as outside the series, as it already did for
an index value there.taylor no longer passes on its engine's "no non-missing arguments to
max" warning from a stretch with no candidate change.cpt_statistic(), cpt_solution_path(), ggcpt_posterior() and
ggcpt_runlength() on a result without its engine object
(keep_fit = FALSE, or read back with cpt_import()) say so, where they
said the engine "does not expose" the thing and then listed that engine
among the ones that do.cpt_export() refuses file = NULL by name, cpt_scale_space() refuses
bandwidths that are not numbers (they became "No usable bandwidth"), and
cpt_metrics() warns when it drops a missing location (truth = NA was
scored as no true changepoints).ocp was archived from CRAN on 2026-10-12, over two documentation notes
in its own checks, and has no other lasting home. It stays an optional
engine: the package website now serves the last CRAN release of every
dependency CRAN has archived as a CRAN-like repository,
https://pursuitofdatascience.github.io/ggchangepoint/repo/, declared in
Additional_repositories, and cpt_install_engines("bayesian") and the
"Package 'ocp' is required" error install from there. fpop still comes
from R-Forge; the website serves it too, so CRAN's check of the declared
repositories no longer depends on R-Forge answering.crops 1.0.5
stopped exporting two methods that cpop 1.0.9 re-exports, and
cpt_detect(x, method = "cpop") told users to install cpop, which they
had.changepoint,
strucchange, segmented, bcp and ecp), Calibration and
Stability, deprecation and the contract.data-raw/use_cpt_wrapper.R) and a weekly workflow that installs every
engine, runs the whole suite and checks every dependency against CRAN.The release that fills in what 0.4.0's engine wave left open: inference,
selection, diagnostics, supervised detection, time indices, streaming,
benchmarking, and an extension mechanism that makes the CRAN-availability
question stop being a blocker. cpt_detect() goes from 31 to 50 wired
methods, and the surface around the detectors roughly doubles.
The highest-leverage addition, and the one everything else leans on.
cpt_register_method() / cpt_unregister_method() /
cpt_registered_methods() teach cpt_detect() about a detector this
package does not (and often cannot) depend on: an engine that is not on
CRAN, a Python detector reached through reticulate, a neural detector, a
proprietary in-house method. The registered method then works with
autoplot(), the geoms, tidy()/glance()/augment(), cpt_metrics(),
cpt_consensus(), cpt_benchmark(), cpt_stability() and
cpt_report().as_ggcpt() turns any set of changepoints (a published paper's
reported breaks, an analyst's annotations, another package's output) into
a validated ggcpt, running the same contract checks as every built-in
wrapper.cpt_methods() gives
them status = "registered", print() marks their results, and
cpt_cite() returns the citation the registration supplied or states
plainly that none was given.cpt_methods() gains capability columns: multivariate, univariate,
online, ci, fitted, posterior, statistic, path,
scale_space. subset(cpt_methods(), ci)$method answers "which methods
give me a confidence interval?" directly.cpt_install_engines() installs a whole family of engines at once
("core", "bayesian", "nonparametric", "highdim", "functional",
"regression", "inference", "applied", "time", "reporting", or
"all"), with a dry_run.fpop was archived from CRAN on 2026-09-14 at its maintainer's request.
It is still built on R-Forge, where it is developed, so it stays an
optional engine: DESCRIPTION declares the repository in
Additional_repositories, cpt_install_engines("core") fetches it from
there, and the "Package 'fpop' is required" error now gives the
install.packages() call that works, because a bare
install.packages("fpop") no longer finds it. fpopw, which exports an
Fpop() with the same signature, was measured as a replacement and
rejected: on tied data (counts, rounded values, a noiseless step) it
returned a segmentation with a higher penalised cost than the optimum in
200 of 1,224 cases, where fpop was exact in all of them.cpt_detect() gains index: detection still runs on positions (every
wrapped engine assumes an equally spaced sequence) but the index is
stored on the result and threaded through tidy() (as cp_index),
augment(), autoplot() (axis and labels), cpt_confint(),
cpt_annotate_events() and cpt_report(). An index that is not equally
spaced warns rather than silently mislabelling the axis.ts, xts, zoo and (unkeyed) tsibble objects are accepted directly
and their own index is carried through. New as_cpt_series() is the one
place that separates the values from the clock.cpt_detect(df, y = value, index = date), where
y and index accept a bare column name, a string or a position. A data
frame passed without y keeps its 0.4.0 meaning.nsp_wrapper() / cpt_detect(method = "nsp") wraps Narrowest
Significance Pursuit (Fryzlewicz 2024): intervals each guaranteed to
contain at least one changepoint at a prescribed global level, with
self-normalised and autoregressive variants for heavy tails,
heteroscedasticity and serial dependence.regions slot on ggcpt, read with cpt_regions(), drawn
by the new geom_cpt_region() layer and by autoplot(show_regions =),
which is on by default for a result that has regions. NSP's cp column is
the interval midpoint and says so, in the cp_source column, in
print(), and in the documentation: the region is the inferential object,
the midpoint is not an estimate.cpt_confint() answers "where could this changepoint be?" for any
result, behind one contract with four provenances: "native" (the
engine's own interval), "posterior", "bootstrap" (within-segment
resampling, available for every engine) and "nsp", and reports which
one it used in a source column.cpt_test() attaches a test to each changepoint or segment, using the
engine's own test where it has one (strucchange's Chow F, segmented's
Davies test) and an explicitly unadjusted Welch two-sample test where it
does not. A selection_adjusted column and a warning make the difference
impossible to miss, because a p-value computed at a location chosen from
the same data is anti-conservative.cpt_select() builds one candidate ladder and scores it by any of six
criteria: "bic", "mbic" (the real Zhang-Siegmund segment-length mBIC,
which cpt_penalty() cannot express), "aic", "crops_elbow" (the knee
rule made explicit and citable rather than eyeballed), "cv"
(order-preserved cross-validation via crossvalidationCP: the criterion
with a consistency proof) and "stability".autoplot() on the result draws the criterion curve, the chosen
segmentation, or (the new display) a ladder of small multiples
showing how the segmentation coarsens as K falls.cpt_influence() implements the Wilms-Killick-Matteson influence
family: delete and outlier perturbation, re-rendered in ggplot2 with
plot_type = "overview" | "location" | "parameter" | "map". It uses
changepoint.influence where that applies and a generic recomputation
everywhere else, so it works for every wired and registered method.cpt_leverage() ranks observations by a composite influence score.cpt_sensitivity() sweeps tuning parameters and shows the detected
locations across the grid: the direct answer to "is this robust to the
penalty?".cpt_statistic() / ggcpt_statistic() return and draw the detector's
criterion as a function of location; new cpt_solution_path() /
ggcpt_solution_path() return and draw the order in which candidates
entered the model; new cpt_scale_space() / ggcpt_scale_space() sweep a
multiscale detector's bandwidth and draw the location-by-bandwidth
heatmap. autoplot(fit, type = "statistic" | "path" | "scale_space")
reaches all three. An engine that exposes nothing errors with the list of
engines that do.cpt_labels() and as_cpt_labels() build labelled regions: the
ground-truth representation shared with cpt_metrics_annotated(), so the
package has one notion of an annotation rather than two.cpt_label_error() scores a segmentation in label errors;
cpt_label_error_curve() traces them across a penalty grid and reports
the target interval.cpt_learn_penalty() fits the max-margin interval regression of
Hocking et al. (2013), delegating to penaltyLearning when it is
installed and falling back to a built-in squared-hinge fit. The result has
predict(), and cpt_detect(x, penalty = model) and
cpt_penalty(model, series = x) accept it directly, as do the wrappers
that take a numeric penalty.geom_cpt_label() draws the labels, and scale_fill_cpt_label()
colours them by assertion or by correct / false-positive /
false-negative status.cpt_consensus() runs several detectors and reports the locations
they agree on, with a vote count and the methods behind each. Matching
reuses cpt_metrics()'s tolerance rule, so the package has one notion of
"the same changepoint". The documentation and the print method both state
that agreement is a robustness display and not a significance test.cpt_recommend() turns the capability matrix into advice: given the
dimension, the change type, the noise structure, the series length and
whether uncertainty or an online alarm is needed, it returns a ranked
shortlist with a reason and a caveat for each.cpt_annotate_events() matches detected changepoints to a table of
known events and reports all three outcomes: matched, unexplained
changepoints, and undetected events. Events may be given on the position
scale or on the result's own index. New geom_cpt_event() draws them.cpt_report() assembles a reproducible artifact (method, citation,
penalty, locations with intervals, regions, segments, optional stability
and events, the call, and sessionInfo()) as markdown or plain text.cpt_gt() renders a publication-ready changepoint table through
gt, degrading to a tibble with a note when gt is absent.cpt_benchmark() runs a method-by-dataset grid, scores every cell
with cpt_metrics() (or cpt_metrics_annotated() when a dataset has
several annotators), and records an engine failure as a message instead of
losing the run. autoplot() gives a heatmap, a rank plot, or the
Demšar critical-difference diagram.cpt_datasets() builds an offline, deterministic collection from the
package's own canonical signals, so the benchmark runs inside
R CMD check.cpt_load_tcpd() downloads and caches the Turing Change Point Dataset
under tools::R_user_dir(), with its multi-annotator ground truth intact;
new cpt_annotations() returns the per-annotator sets one row at a time,
so the disagreement between annotators stays visible.cpt_monitor() creates a stateful sequential detector, fed by
cpt_update() and read with alarms(). Three methods: cpm, ocd, and
edetector.ggcpt_benchmark,
ggcpt_batch, ggcpt_recommendation, ggcpt_label_curve,
cpt_labels, cpt_label_error) now drops the class rather than keeping
a fragment that its own print() method cannot read. dplyr::select()
on one of these behaves the same way; filter() and row indexing keep
the class, as they should.edetector is a native implementation of the mixture
Shiryaev-Roberts e-detector of Shin, Ramdas and Rinaldo (2023): a
deliberate, separately scoped exception to this package's
wrap-don't-implement rule, taken because no R package implements
e-detectors and the construction is short enough to audit. Under the null
the mixed statistic M_t satisfies E[M_t] = t, so optional stopping at
the alarm time gives a finite-sample lower bound of 1 / alpha on the
in-control average run length, with no calibration run. The shifts are
combined by averaging, not by taking a maximum: a convex combination
of e-detectors is an e-detector and a maximum is not, and the test suite
measures the in-control alarm rate against the bound rather than taking
the derivation on trust. It is labelled as native wherever it appears.relearn), so a
persistent change is reported once rather than on every subsequent
observation. It is also dimensioned at construction: feeding
cpt_update() a different number of coordinates is an error rather than
a silent coercion.cpt_replay() runs a whole series through a monitor; new
cpt_delay() scores it the way the sequential literature does (detection
delay per change, false alarms, and the average run length) instead of
asking whether a location was recovered, which a sequential procedure
never claims.cpt_power() reports detection probability, location error and false
positives across a scenario grid, with the Monte Carlo standard error
attached and drawn as a band.cpt_min_detectable() inverts it: the smallest change reaching a
target power, for pre-registration and study design.cpt_scenarios() builds a reproducible grid of simulation settings as
data, ready for cpt_benchmark().cpt_simulate() gains seasonality (sine or sawtooth) and sd_trend
(smoothly varying noise scale, distinct from the piecewise-constant
change_in = "var"), so the conditions the dependence-aware and seasonal
engines exist for can actually be simulated.cpt_detect() reaches 50 wired methods. New change_in levels
"covariance", "network", "regression" and "seasonality" come with
them, and the capability matrix was extended in lockstep.
nsp (nsp).mcp (mcp): formula-based multiple-changepoint
regression with full posteriors. Needs JAGS, a system dependency, and
says so plainly when it is missing.esac and pilliat (HDCD) for sparsity-adaptive
mean changes; hdcov, network, var and hdreg (changepoints) for
changes in covariance, dynamic-network structure, VAR(1) dynamics and the
coefficients of a sparse high-dimensional regression: changes no
mean-change engine can see.fmean and fcov (fChange); kwc
(KWCChangepoint), robust depth-rank segmentation; fabisearch
(fabisearch), network structure via non-negative matrix factorisation.pettitt, buishand and snht (trend):
the hydrology and climatology standards, each with a valid p-value
because the location was not chosen from a model search; taylor
(ChangePointTaylor): the quality-control default, with bootstrap
confidence per changepoint; bfast (bfast): season-and-trend breaks
for remote sensing.wbsts (wbsts) for second-order changes;
binsegrcpp (binsegRcpp) as a fast binary-segmentation path across
several loss functions.ggcpt_interactive() gains engine = "ggiraph" alongside the existing
\pkg{plotly} path. ggiraph renders the ggplot itself to interactive SVG,
so facets and every layer survive, which \pkg{plotly}'s own model does
not always manage for a faceted multivariate result.
autoplot() gains labels =: pass a cpt_labels() set and the labelled
regions are shaded behind the series and coloured by outcome (correct,
false positive, false negative), so scoring against expert labels is a
picture rather than a table.
New scale_colour_cpt() / scale_fill_cpt() / scale_linetype_cpt()
provide an Okabe-Ito palette that stays distinguishable under the three
common forms of colour-vision deficiency. ggcpt_compare(layout = "overlay") now maps linetype as well as colour, so the panel reads in
greyscale.
Every autoplot() on a ggcpt carries generated alt text, which knitr
and Quarto pass through to the rendered image.
cpt_register_method() is now visible inside
future workers. The registry lives in the package namespace and a worker
loads the package fresh, so cpt_batch(), cpt_benchmark(),
cpt_consensus(), ggcpt_compare(), cpt_influence(),
cpt_sensitivity() and cpt_power() used to fail on a registered method
under plan(multisession) with a misleading "'arg' should be one of"
error. Each now carries a snapshot of the registry to the worker.cpt_benchmark() accepts changepoints as ground truth alongside truth
and annotations, treats a list-valued truth as several annotators
rather than flattening it, and warns when a list dataset carries none
of the three: previously it returned a full benchmark table in which
every metric was silently NA.mcp_wrapper() works, and says so honestly when it cannot. Two faults,
both of which had gone unnoticed because the engine needs JAGS and its only
test was the negative one that skips when \pkg{mcp} is installed. First,
the default segment model is plateau-only (list(y ~ 1, ~ 1)), so
\pkg{mcp} could not derive its x-axis variable from the formulas and
stopped with "This is a plateau-only model"; the wrapper now names the data
frame's t column via par_x unless the caller supplies their own.
Second, having the package is not the same as being able to run it:
\pkg{rjags} installs on some platforms and only fails when it looks for
the JAGS library at run time, in which case mcp::mcp() returns a fit with
no posterior samples and a warning, and summary() on that died with
"subscript out of bounds". The wrapper checks for the samples and reports
the real cause. The documented claim that \pkg{mcp} "will not install at
all" without JAGS was wrong, and is corrected; the example is \dontrun{}
because no test of installed R packages predicts whether a system library
can be reached.cpt_methods() no longer loads every engine to find out which ones are
installed. It asked requireNamespace(), which loads the package, so
building the table pulled in all thirty-five namespaces, including
\pkg{rgl}, by way of \pkg{fabisearch}, which fails outright on a machine
with no OpenGL. find.package() answers the question without touching
anything: the call drops from seconds to hundredths of a second and loads
nothing. Wrappers still load their engine when they actually need it.cpt_scale_space() validates the shape of its input before requiring the
engine, so asking for method = "mosum" with a matrix says that mosum is
univariate rather than telling you to install a package that could not
have accepted the input anyway.pilliat_wrapper() refuses a dimension that is an exact power of two.
HDCD 1.1's Pilliat() builds one fewer partial-sum threshold than it
uses at those dimensions, so it reported a changepoint at every
observation (on pure noise as readily as on a real change) for
p = 2, 4, 8, 16, 32, 64 and 128. The wrapper now says so and points at
esac, which is unaffected; the refusal lifts automatically once a fixed
HDCD is installed.fabisearch_wrapper() rejects an all-zero time point with a message that
names the offending rows, instead of letting NMF's own error surface
several layers down.cpt_confint() reads NSP's own intervals. NSP reports an interval that
provably contains a change, under region_start/region_end; cpt_confint()
looked only for ci_lower/ci_upper and so bootstrapped 200 re-runs of the
detector to produce a weaker statement than the one already on the object.
The source column now distinguishes "nsp_region" from "native", and
reports NSP's global level.bfast_wrapper(change_in = "seasonality") works. \pkg{bfast} reports "no
breakpoints in this component" as a bare NA rather than an empty
breakpoints object, so the declared capability errored with $ operator is invalid for atomic vectors on any series whose seasonal amplitude is
stable. Asking for seasonal breaks with season = "none" is now an error
rather than a puzzle.binsegrcpp no longer claims a variance-only change. \pkg{binsegRcpp} has
no variance-only cost, so change_in = "var" was mapped to a distribution
the engine does not have; "mean" and "meanvar" are what it offers.taylor_wrapper() validates n_bootstraps against the engine's real range
(100 to 1,000,000) instead of letting a smaller value fail inside
ChangePointTaylor with a message about its own misspelled argument.cpt_batch(keep_fit = FALSE) drops each engine's raw fit. A few engines
return fits far larger than the data: measured on a 2000-point series,
strucchange costs about 135 MB (a triangular O(n^2) RSS matrix),
bfast 53 MB and bocpd 31 MB, while every other engine stays under 4 MB,
and a panel multiplies that by the number of series. cpt_recommend()
now carries both this and pilliat's dimension restriction as caveats.cpt_select() gains an index argument and inherits one from an indexed
ggcpt. It previously read only the values off its input, so a selection
made from a dated fit came back reporting positions.tidy() now works on every result class the package returns.
ggcpt_influence, ggcpt_power, ggcpt_monitor, ggcpt_delay,
ggcpt_recommendation, cpt_labels and cpt_label_error had no method,
so tidy() failed on half the surface; glance() also reports the
one-row summary a ggcpt_delay already carries.cpt_monitor() names a missing value in the baseline instead of reporting
it as zero variability, and cpt_delay() refuses a truth that falls past
the end of the stream, is empty, or is non-positive, each of which used to
be scored as a clean miss.alpha parameter's documentation still credited Ville's inequality, which
is a different statement.plot() works on every result class. Thirteen plot() methods were
missing, so plot() on a selection, monitor, batch, benchmark, influence,
sensitivity, stability, path, power, delay, events or label-curve result
fell through to plot.default() and failed with base R's 'x' is a list, but does not have components 'x' and 'y': a message that names neither
this package nor autoplot(), arriving at the moment a new user is most
likely to type plot(result).plot() on a subclass now draws the subclass's figure. plot.ggcpt()
called autoplot.ggcpt() by name rather than dispatching, so
plot() on a cpt_consensus() result silently produced the plain
changepoint plot instead of the consensus one.plot() method now draws as a side effect and returns the ggplot
invisibly, so plot() works inside a loop or a function while
p <- plot(result) still gives you the object to add layers to.cpt_detect() coerces the series before validating it, so
cpt_detect(factor(...)) ran to completion and reported changepoints in
an alphabetical ordering of the labels with nothing said about it.
Character input is refused by name too, instead of warning "NAs
introduced by coercion" from base R and then blaming non-finite data.cpt_detect() coerced one
before validating and cpt_select() validated before coercing, so a 0/1
series worked in one and was refused by the other.cpt_batch(NULL) gave 'data' must be of a vector type, was 'NULL',
cpt_batch(list()) returned a batch of nothing at all, and a
zero-column data frame reached X[, 1] and gave subscript out of bounds.mosum reaches
tcltk through plot3D and misc3d, so the first cpt_scale_space()
or cpt_statistic() call on any headless box (a server, a container, a
CI runner, a cluster node) warned no DISPLAY variable so Tk is not available. need_pkg() muffles a load-time warning while still
reporting a load that fails.cpt_power() with a single change size plots something. One scenario is
one point per curve, so geom_line() drew nothing and advised adjusting
the group aesthetic (about a plot that was already right) and the
ribbon carrying the Monte Carlo interval was invisible while the
subtitle still announced it. A single change size now gets a vertical
range and a subtitle that says so; two or more are unchanged.cpt_metrics(), cpt_metrics_annotated() and ggcpt_eval() say what is
wrong when handed a ggcpt. They are the only tools in the package that
take bare changepoint indices rather than the fit, so passing the fit is
the obvious mistake, and as.integer() answered it with 'list' object cannot be coerced to type 'integer'. A ggcpt is also a list, so
cpt_metrics_annotated() read one as a set of annotators and scored its
own fields. Each now names the argument and the fix
(fit$changepoints$cp), including for a tidy() table.cpt_monitor() warns when a tuning argument the chosen detector ignores
is explicitly supplied. The three detectors are calibrated in different
currencies (edetector by alpha, cpm by arl0, ocd by
patience) and each ignores the others', so cpt_monitor("edetector", arl0 = 5000) changed nothing at all. ?cpt_monitor and the monitoring
vignette both said so; now the call does too. The knobs that apply, and
the defaults, stay silent.?cpt_monitor marks method = "ocd" multivariate only, so the
requirement is visible where the method is chosen rather than only in the
error a univariate baseline eventually raises.cpt_scenarios() warns when a requested location is clamped into
2..(n - 2). The scenario table records the requested fraction, so a
clamped row and the data generated from it disagreed silently about where
the change is.ggcpt_eval() validates margin the way cpt_metrics() does. A negative
margin was accepted and drew its tolerance rectangles inside out
(xmin > xmax) while the metrics function refused the same value.ggcpt, cpt_detect() took it
entirely on trust, so a function that built its result from some other
series handed back a result whose $data, row count and n described
that series instead of x: the wrong series to plot, the wrong number of
rows from augment(), the wrong n for every metric. The comment and
?cpt_register_method both claimed the returned object went through "the
same contract checks as every built-in wrapper"; only the bare-index
branch did.as_ggcpt() refuses changepoint locations it cannot read instead of
reporting none. cp was coerced under suppressWarnings(), so
as_ggcpt(c("a", "b"), x) returned a clean-looking result with zero
changepoints, and as_ggcpt(factor(c("60", "90")), x) returned
changepoints at 1 and 2: the factor's level codes. A logical vector
is refused too, pointing at which(cp). The documented drops
(out-of-range, duplicated, missing) and the acceptance of a character
vector that converts cleanly are unchanged.as_ggcpt() reports a wrong-length fitted signal. It was dropped
silently, after which autoplot(show_fit = TRUE) said the result "carries
no fitted signal", about a signal the caller had supplied. Every sibling
slot (index, ci, regions, extra) already reported its length
mismatch.fmean_wrapper() and fcov_wrapper() say what shape they got. Two or
three columns satisfy the "needs at least two" guard and are still too
coarse a grid for \pkg{fChange}'s basis expansion, which stopped with base
R's subscript out of bounds, naming neither the argument nor the shape.
The error now reports the time-points-by-grid-points shape and passes the
upstream message through verbatim, since a coarse grid is the usual cause
and not the only one.as.integer() on a factor
returns level positions (alphabetical unless the caller set levels)
so cpt_metrics(factor(c("100", "150")), c(100, 150), n = 200) scored the
predictions as 1 and 2 and reported a recall of 0 for predictions that
were exactly right. Ten entry points read locations through a bare
as.integer(): cpt_metrics() (pred and truth),
cpt_metrics_annotated(), ggcpt_eval(), cpt_delay(),
as_cpt_labels(), cpt_labels(), cpt_label_error(), cpt_simulate(),
cpt_benchmark()'s dataset annotations and as_ggcpt(). All ten now
refuse, each naming the argument the caller passed; a character vector
that converts cleanly is still accepted everywhere it was before.cpt_detect(x, index = month.abb) was accepted while
cpt_detect(x, index = factor(month.abb)) was refused with "index must
be non-decreasing", because the codes of an alphabetically levelled
factor are 5, 4, 8, 1, 9, .... An ordered factor does carry its order
in its codes and keeps the ordering and spacing checks.cpt_report() validates file before building the report. A path in a
directory that does not exist, a directory, NA or a two-element vector
each produced a base-R connection error naming neither the argument nor
the package, and file = "" printed the report to the console and wrote
no file at all, leaving the caller with a report they believed they had
saved. file remains ignored for format = "gt", as documented.@return: cpt_regions() carries through whatever extra columns the
engine supplied (nsp_wrapper() adds value, the region's statistic),
cpt_scale_space() returns detected alongside significant (a
location can clear the threshold without surviving the engine's own
pruning); cpt_label_error() returns series, and cpt_benchmark()
returns n_annotators.cpt_annotate_events() adds cp_index only when the result carries a
time index. It was created unconditionally and filled with a bare NA,
so the same column was a Date on an indexed fit and a logical on an
unindexed one, and every unindexed result carried a mystery all-NA
column. attach_index() and cpt_confint() both key on the column's
presence, so this now does too. Its @return also documents cp_index
and event_value, which it had never named.autoplot() on a cpt_stability() result honours a time index. It was
the one plot in the package drawn against series position that read the
positions directly instead of going through the shared index helpers, so
a dated series came back in positions there while autoplot() on the
fit, ggcpt_statistic(), ggcpt_scale_space(), ggcpt_solution_path()
and the influence and events plots all showed dates. An unindexed result
is unchanged.validate_data() accepts three observations, and at that length seven
engines (pelt, fpop, wbs2, tguh, smuce, decafs, nsp) return
a changepoint after every one: k = n - 1, every segment one point
long, which is a failure to segment rather than a segmentation; at
n = 5, three of them still do. The threshold is engine-specific, so the
check is on the result rather than a blanket minimum that would refuse
calls which work. The message distinguishes the two causes: with
penalty = 0 (which is what penalty = "None" resolves to for the
numeric-penalty engines) one segment per observation is the correct
unpenalised optimum at any series length, and only a positive penalty
reaching the same place means the series is too short.dpi = 72 rather than rmarkdown's default 96.
The source tarball goes from 5.2 MB to 4.2 MB (CRAN's limit is 5 MB) and
the installed doc directory from 4.9 MB to 3.5 MB, with no visible
change to the figures: html_vignette displays them at their natural
size, so fewer pixels means a smaller file, not a smaller picture.Further passes, each sweeping a surface rather than re-reading code: every
wrapper against every argument its engine accepts, every method against
every change_in value it advertises, every documented claim against the
installed package, and (the ones that found the most) invariances, where
the answer is compared against another answer rather than against a
recorded value. 84 further items in all, itemised below.
Five are wrong answers. Four are below; the fifth has its own section
because it is the one that could have reached a publication: a seed
argument that reset the caller's random stream, so a simulation loop
analysed the same dataset six times over.
wbsts could not report more than one changepoint.
wbsts::wbs.lsw() ends in
suppressWarnings(if (is.na(OUT)) OUT = NULL), which was a warning
before R 4.2 and is an error after it, and OUT has length > 1
exactly when post-processing kept two or more changepoints. So the call
died precisely when the method would have reported the multiple changes
it exists to find: 2 of 20 runs failed on a one-changepoint series and
19 of 20 on three- and five-changepoint ones, where the successes never
reported more than one. On that one error the wrapper restores
.Random.seed and replays the engine's own body with all(is.na(OUT))
in place of is.na(OUT) (the reading its suppressWarnings() shows
was intended) so the result is upstream's answer retrieved, not a
different one.hdcov and network failed on 44-96% of runs, at random.
changepoints::thresholdBS() prunes with for (i in 2:level_length),
so a binary-segmentation tree with one level runs the body at i = 2,
table(...)[2] is NA, and 1:NA stops with base R's NA/NaN argument. Binary segmentation stops at one level whenever the series is
short relative to the dimension, and the threshold comes from a
permutation draw, so whether a given call landed there was random:
hdcov failed on 23 of 25 runs at n = 120, p = 8 and 24 of 25 at
n = 400, p = 20; network on 10 of 12 for a 20-point sequence of
4-node graphs. One level means one candidate split with no ancestors to
prune against, so it is a changepoint exactly when its own statistic
clears the threshold: a rule that reproduces thresholdBS()'s output
on a multi-level tree, which is what makes it safe to apply.cpt_batch() detected on a panel it had rewritten. It reached the
engine through a bare as.numeric(), which cpt_detect() has refused
since 0.4.0: a factor member became its level codes (an
alphabetical ordering of the labels) and a character member became
NAs. A matrix member was worse, because as.numeric() unrolls it
column after column: an 80×2 member became a 160-point series and
reported a changepoint at index 80, the seam where the second column was
appended. The message names the offending series, because in a panel
"which one?" is the question.ecp_wrapper() absorbed non-finite values rather than refusing
them, and what came back was wrong rather than merely missing. On a
180-point series with one change at 90, twenty NAs lost the
changepoint entirely, and an all-NA second half reported two
changepoints at 12 and 14 that the data does not contain.
cpt_wrapper() and cpt_detect() had always refused this; only the
ecp route was open.cpt_wrapper() and ggcptplot() refuse multi-column input instead of
concatenating it. The same unrolling as above: cpt_wrapper() on a
120×2 matrix reported 58, 120 and 180, where only the 58 is real and the
120 is the seam. ggcptplot() drew 240 points for 120 observations;
it now plots the first column and says so, the convention
ggecpplot() already had.cpt_replay(), cpt_monitor() and cpt_update() refuse a factor
series. Each branch coerced its own way, so a factor reached two of the
three detectors as level codes: for labels like "10", "2", "30"
that is the order 3, 1, 2 rather than the numbers. The series is now
normalised once, before the branch. cpt_penalty(model, series =) is
guarded for the same reason: a learned penalty predicted from a factor's
level codes is silently just a number.cpt_power() no longer reports a power figure for a changepoint nobody
asked about. location went through no validation at all, and the
scenario loop clamps with max(2, min(cp, n - 2)), so location = 1e6
at n = 200 answered "power 0.75" for a change at 198. Out-of-range
positions are now refused; an extreme fraction still clamps, which
is correct.cpt_power() warns instead of returning power = NaN. One unlucky
replicate may legitimately fail, which is why the loop tolerates
errors, but when every replicate fails (most often because an
argument forwarded through ... is not one cpt_detect() accepts) the
rate is mean(all-NA), and a NaN is a number the caller could plot.
The first engine error is now reported with it, and
cpt_min_detectable() stops on it rather than reaching
if (power < ...) and answering missing value where TRUE/FALSE needed.cpt_replay(baseline = 500) on a 180-point series is refused rather
than reinterpreted. A lone number is documented as the count of leading
observations; when it was fractional or out of range it fell through to
the explicit-series branch, which turned it into a one-point
baseline and then failed with "needs at least 5 pre-change
observations", never mentioning the 500.cpt_methods() no longer advertises two capabilities it could not
deliver: wbsts and binsegrcpp were marked path = TRUE but expose
no solution path to cpt_solution_path().A sweep of all 64 wrapper argument slots and of every wrapper against every argument its engine accepts.
jumpint, bocpd's getR, wbs2/tguh's
solution.path and model.selection, sn's plot_SN, envcpt's and
beast's narration switches, decafs's warningMessage, and R
answered formal argument "verbose" matched by multiple actual arguments, naming neither the wrapper, nor the engine, nor what to do
instead. Each is now refused with the reason the wrapper sets it.... is documented as reaching the engine, so the engine's own name is
the natural thing to pass: mindist for min_dist, M for
n_intervals, cpmType for cpm_type, ARL0 for arl0, lambda
for penalty, and so on. Each now redirects to the argument that does
the job, or says the value comes from x and is not the caller's to
set.cpt_crops() pins the two
arguments that make its call CROPS at all (method, penalty) and the
interval it sweeps (pen.value); cpt_wrapper() and ggcptplot()
rename the \pkg{changepoint} package's method to cp_method, so
method (the most natural name to reach for) was the one that broke;
and cpt_monitor() renames or pins five of cpm's and ocd's
(cpmType, ARL0, MC_reps, dim, beta), which cpt_replay()
inherits by forwarding ... to it. Each now redirects by name, and
ggcptplot() names itself rather than the function it shares the rename
with.missing value where TRUE/FALSE needed, negative length vectors are not allowed or
NAs in foreign function call. burnin, min_size, cstar,
lambda, alpha, patience, mc_reps, wsize, kmax, lag,
npsi, minseglen, confidence, frequency, ord, N, the two
threshold_* constants, n_perm, sigma, df, seed, startup
and arl0 are all validated by name now.not reports max.length must satisfy 3 < max.lenght <= n, envcpt Minimum segment legnth is too large,
wbsts base R's subscript out of bounds, which does not even say the
series is the problem. None named the method the caller asked for or the
length they gave it. The engine's diagnosis is kept (it is the
informative half) and the method, the observation count and, where the
threshold moves with an argument, the arithmetic are added:
bfast needs 2 * frequency, strucchange needs h * n, envcpt
more than 2 * minseglen.hdcov non-conformable arrays,
network 'x' must be an array of at least two dimensions, var
incorrect number of dimensions, kwc dim(X) must have a positive length, and geomcp a clearer line that still named neither method nor
argument. All five now match the four engines that always named the
requirement.NA in 10,000 points and a half-missing series are different problems
with different fixes), from one definition rather than seven copies.
network was the one high-dimensional route with no finiteness check at
all, and a single NA surfaced as replacement has length zero from
inside its random edge-splitting.as.matrix() returns an all-character matrix, so x must be numeric left the reader to find which column. The note also says what
coercing a factor would have given.as_mv_matrix() said x where the argument was baseline, new_obs,
series or response; cpt_scale_space() reported
Method `scale_space` is univariate, naming a method the caller had
never heard of; cpt_replay() reported a non-finite value against
baseline or new_obs depending on which slice happened to contain it,
and counted it against the slice rather than the series.cpt_benchmark(methods = character(0)) is refused instead of building a
zero-row grid and stopping at attempt to set an attribute on NULL.ggcpt_interactive() validates width_svg/height_svg on the
ggiraph path, where girafe() answered `width` must be a scalar positive number about its own internal argument.cpt_monitor() refuses a multi-column baseline for a univariate
detector by shape. length() on a data frame counts its columns, so a
60-row, 2-column baseline was refused for having fewer than 5
pre-change observations, naming a count of 2 for 60 observations.ggcpt_posterior() on a bocpd or mcp result names the accessor that
does work (ggcpt_runlength(), and ci_lower/ci_upper respectively)
instead of being a dead end reached from cpt_methods()' own
posterior column.cpm's two thresholdsarl0 grid was half the real one. cpm ships
thresholds for 24 average run lengths, not the 12 the help page and the
error message listed: 300, 800, 900, 3000, 4000, 6000, 7000, 8000, 9000,
30000, 40000 and 50000 were all refused by this package and accepted by
the engine. The grid is the same for every cpm_type, and 50000 is the
ceiling.cpm_type = "FET" needs a lambda and had no default. Without one
processStream() dies inside cpm with only 0's may be mixed with negative subscripts. It is now refused by name, with the two values
cpm ships FET thresholds for (0.1 and 0.3).arl0 and lambda, and blaming
arl0 for a lambda failure sent the reader after an argument that was
already correct."ExponentialAdjusted" is offered again. It was withheld alongside
"GLRAdjusted" as one of two types cpm's own dispatch rejects;
re-measured against cpm 2.3, it runs and returns changepoints. Only
"GLRAdjusted" is genuinely rejected upstream.fabisearch needs NMF attached rather than loaded, and attaching it
brings its Depends (Biobase, BiocGenerics) and the
foreach/doParallel/doRNG stack the engine registers: eight packages
measured, where the wrapper detached only NMF; and the baseline was
taken after need_pkg(), which is itself what attaches two of them.
bcp::bcp() calls require(bcp) in its own body, so every call
attached package:bcp and package:grid. Both now restore exactly what
the call added, leaving a package the user had already attached
untouched.require() speaks through packageStartupMessage(), so restoring the
search path silently was not enough: bcp_wrapper() still printed
Loading required package: bcp and Loading required package: grid on
stderr. Only package startup messages are suppressed, so an engine's own
message() and warning() still reach the caller.inspect_wrapper() no longer prints an upstream loading
diagnostic. InspectChangepoint::inspect() and sparse.svd() both
call requireNamespace("RSpectra") without quietly = TRUE, and
RSpectra is only suggested there, so on a machine holding the
engine and not RSpectra every call wrote Loading required namespace
and Failed with error: there is no package called 'RSpectra'. The
engine handles the absence itself by falling back to base::svd, so it
is a diagnostic rather than a problem, and thirteen repetitions of it
are what truncated a CI test log down to nothing else.
suppressMessages() is not enough (requireNamespace() writes the
second line straight to stderr) so the message stream is captured;
warning conditions and errors still propagate.seed no longer resets the caller's random streamA seed argument silently collapsed simulation studies to a sample
size of one. Every one of the thirty-six sites that honoured a seed
did it with if (!is.null(seed)) set.seed(seed) in the function's own
frame, which does not merely consume the caller's random stream: it
resets it. So the argument whose entire purpose is trustworthiness
pinned the stream of whatever loop the call sat inside:
set.seed(2026)
for (i in 1:6) {
d <- c(rnorm(100), rnorm(100, 3))
f <- cpt_detect(d, method = "wbs", seed = 1)
}
Iteration 1's set.seed(1) pins the stream, so every later rnorm()
starts from the same place and regenerates the same series: measured
with the data built outside the call, 6 distinct datasets of 6 without
the seed and 2 of 6 with it. Nothing warned and no test failed. The
same collapse was measured through cpt_stability(),
cpt_select(criterion = "cv"), cpt_simulate(), cpt_power() and the
stochastic engines, which is to say exactly the functions a user calls
inside a simulation loop.
The seed is now scoped to the call. A new internal helper saves
.Random.seed, sets it, and restores it when the calling function
exits, at all thirty-six sites. A fresh session that had no
.Random.seed is left without one, rather than acquiring one. Nested
calls stack correctly: an inner scope restores what the outer one set.
Nothing documented changed. A seeded call is still byte-reproducible, and still reproducible across a thousand intervening draws or after an unseeded call has moved the stream on: all three are now tested, as metamorphic assertions that compare calls to each other rather than to a recorded value, which is the only kind that could have caught this.
Every @param seed says so: the seed is scoped to the call and does not
pin the loop's own stream.
cpt_metrics_annotated() returns six fewer columns than
cpt_metrics(), and @return said only "a tibble with averaged
metrics". A call moved from one to the other silently loses
n_truth, hausdorff, rand_index, annotation_error, mae_matched
and rmse_matched. The page now names the four it does average
(precision, recall, f1, covering), says they are plain
unweighted means over n_annotators, and lists what is gone.NA whenever an annotator shares no matched pair with the
prediction, so averaging them would quietly divide by fewer annotators
than n_annotators reports. Measured on three annotators against one
prediction: mae_matched was available for one of the three and
hausdorff for two, while f1 and covering were finite for all
three. The page says to score those per annotator with cpt_metrics()
and combine them yourself, so the divisor is the caller's choice, and
notes that covering and f1 are the pair the Turing Change Point
Dataset benchmark reports, which is why they are the ones averaged.ggcpt, a data frame) are refused by name.cpt_leverage() ranked the most influential observation last.
max_shift (how far each original changepoint had to move to find a
match) and param_shift (the largest change in a segment parameter) are
both undefined for a perturbation that left the engine with no
changepoints (nothing to match against, no parameters to compare) so
the composite score came out NA and order(-leverage) sent that row to
the bottom of a table whose entire purpose is which observations matter
most. An observation whose deletion destroys the whole segmentation is
the most influential one there is; measured, it ranked 3 of 3.
Collapsed-fit rows now come first.NA itself is kept, because those two components genuinely are
undefined and filling them in with a fabricated number would be worse.
Such a row still says what happened through its delta_n_cp, and
?cpt_leverage now explains both the ordering and how to read it. It
also records that an NA here always means this perturbation collapsed
the fit: when the original fit found no changepoints, max_shift is
missing for every observation, the standardisation's zero-variance guard
returns zeros, and every leverage is finite.coef() and predict() on a learned penalty are on different scales,
and the help page said only that both methods exist. coef() gives an
intercept plus one weight per feature on the log-penalty scale, where
the interval regression is fitted; predict() exponentiates and returns
a penalty on the natural scale, which is what cpt_penalty() and
cpt_detect() consume. So a coefficient of −0.04 on log_n is a
multiplicative effect, not an additive one. Both scales are now stated,
and a test pins the relation by reconstructing a prediction from the
coefficients by hand.penalties until the largest one over-segments if
the coefficients need to mean something.tidy() on an events result has a status column whose three values
appeared nowhere but the source, and misreading it overstates your
results. The table is one row per changepoint plus one row per event,
with status in "matched", "unexplained_changepoint" and
"undetected_event", and an "unexplained_changepoint" row carries a
non-missing cp. So subset(tidy(x), !is.na(cp)) returns the matched
pairs and the changepoints no event explains, which silently
overstates how much the events account for. @return documented the
object's three slots thoroughly and never said what tidy() does with
them; it now gives the vocabulary, which column is NA in each row
shape, and says to filter on status rather than on is.na(cp).autoplot(plot_type = "critical_difference") is described as "the Demšar
diagram ... the standard way this literature says method A beats method
B", which is a specific methodological claim, and Demšar (2006) appeared
in neither inst/REFERENCES.bib nor any \insertRef: on a page that
already cites van den Burg and Williams for the metrics. Added, and the
page now carries a "Reading the critical-difference diagram" section.NA takes the
worst rank rather than being dropped, so a method that failed on a
dataset is penalised instead of quietly scoring on a smaller sample; and
the formula for the bar, CD = q_α √(k(k+1)/6N) with q_α the
Studentised range over √2.autoplot() draws the diagram it is asked for and does not run that
omnibus test. With the handful of datasets cpt_datasets() supplies N
is small and CD correspondingly wide, so the page now says to read such
a diagram descriptively.NA takes the
worst rank in both directions, ties share the average, mean_rank
averages over datasets, and an all-NA metric returns NULL rather than
a table of ties. cpt_stability()'s frequencies are exact multiples of
1/B, so hits really is a count of replicates, which is what the
clipping bug fixed earlier in this cycle had hidden.cpt_consensus() accepted a min_votes no location could reach, and
returned an empty consensus without comment. min_votes = 3 against
two methods resolves to a threshold of 3, which nothing can clear, and
an empty consensus is indistinguishable from the methods agreed on
nothing, which is a finding rather than an arithmetic mistake. It now
warns, naming the threshold and the number of methods that actually ran
(not the number requested, since a method that errors is excluded from
the vote).?cpt_consensus now says so. A value strictly between
0 and 1 is a proportion; anything else is a count. So with three methods
min_votes = 0.99 requires all three while min_votes = 1 (and 1.0,
the same number) is a count of one, the least strict setting there is.
The two neighbouring values mean opposite things, silently. The parameter
now spells that out and says to pass the method count, or a fraction just
below 1, for unanimity; the new warning repeats it, because a count above
the method total is the likeliest way to arrive there.?cpt_select gave the formula for one of its three closed-form
criteria and not the other two. "mbic" was written out in full
(\eqn{3K\log n + \sum_i \log(l_i/n)}); "bic" was "Gaussian BIC over
the ladder" and "aic" "Gaussian AIC over the ladder", which leaves the
value column a reader cannot reproduce: both the cost convention and
the changepoint parameter count vary between authors. Both are now
stated: n log(RSS/n) + (2K + 1) log n and n log(RSS/n) + 2(2K + 1),
with the parameter count spelled out as K locations plus K + 1
segment means, and the note that the first term is the cost column.cost column is the Gaussian profile cost at that rung's own
locations, exactly one row is marked chosen, and it is the argmin of
value. A test pins the formulas rather than recorded numbers, so a
change to what a criterion means fails instead of passing."aic" is now a tested behaviour rather
than prose: on a 300-point series with changes at 100 and 200, "bic"
and "mbic" both choose K = 2 and "aic" takes the whole ladder.fcov costs minutes on a hundred-point series, and nothing said so.
It is by a wide margin the most expensive engine in the package. Timed
against fmean_wrapper() on identical input (same package, same
data) fcov took 316 s at n = 60, p = 5 and 598 s at
n = 120 against fmean's 4.5 s and 2.9 s: a factor of seventy to two
hundred. ?fcov_wrapper now carries a "How long this takes" section with
those numbers, notes that the cost is roughly linear in the number of
time points and lives in the engine's covariance-operator estimation
rather than in the wrapper, and says plainly not to put the method in a
loop: a twelve-replicate study at n = 120 is two hours. taylor and
ocd already carried cost sections for the same reason; this is the
third and the most extreme.cpt_simulate() dropped a surplus params entry silently. k
changepoints make k + 1 segments, which is the arithmetic easiest to
get wrong, and the two directions were treated differently: too few
entries already warned (the last one is recycled, so the trailing
changepoints would be recorded as ground truth with no change behind
them), while too many used the first k + 1 and discarded the rest
without a word. So cpt_simulate(400, changepoints = 200, params = c(0, 3, 9)) returned an ordinary two-segment series and the
9 vanished, from a caller who had plainly meant two changepoints, and
whose cpt_power() or cpt_benchmark() numbers are then scored against
a truth they did not intend. It now warns in both directions, naming the
changepoint count, the segment count and how many entries went unused,
for every change_in rather than only "mean".cpt_simulate() is scored against them: the realised mean
jump matches params to within a standard error at three sizes, the
realised sd ratio matches at two, seg_id increments at exactly the
requested changepoints for three configurations, noise = "ar1" recovers
rho, and noise = "t" is materially heavier-tailed than Gaussian.as_ggcpt() dropped a changepoint it could not use without saying
so. ggcpt_build() discards an index that is NA or outside
1..(n - 1), and for a wrapper that is right: the indices come from an
engine, some of which legitimately emit a boundary value, and normalising
a machine's output is the wrapper's job. But as_ggcpt() is handed the
caller's values, and its documented use cases are a published paper's
reported breaks, an analyst's annotations, another package's output. On a
200-point series:
as_ggcpt(c(50, 500), x) -> 1 changepoint at 50
as_ggcpt(c(0, 50), x) -> 1 changepoint at 50
as_ggcpt(c(50, 200), x) -> 1 changepoint at 50
as_ggcpt(50.5, x) -> 1 changepoint at 50 (truncated, not rounded)
as_ggcpt(c(50, 50), x) -> 1 changepoint at 50 (deduplicated)
One mistyped index left a result that looked complete and was short a
changepoint. The dropping is unchanged (it is the documented
contract and refusing would break working code) but each of the five now
warns, naming the values, the range they had to fall in, and why the
convention makes n invalid under "left" and 1 invalid under
"right". Sorting still happens silently, because reordering loses
nothing.
The report is a classed condition (ggchangepoint_cp_dropped),
because one caller was right to be silent: cpt_detect() routes a
registered method's bare-vector return through as_ggcpt(), and there
the indices came from the detector rather than from a person transcribing
them: the wrapper case, where normalising an engine's output is the
point. That one call site muffles this condition and nothing else, so a
registered detector emitting a boundary index does not warn on every
call while a user's own transcription still does.
cpt_power()'s seeded answer depends on the future::plan(), and
?cpt_power now says so. Seven exported functions dispatch on the
plan, and this is the one whose farmed-out tasks consume random numbers:
under a parallel plan the replicates draw from \pkg{future.apply}'s
L'Ecuyer streams (derived from seed), and sequentially from the calling
stream seed set. Both are deterministic and they are not the same
numbers: measured, one and the same seed = 11 gave power = 0, 1
sequentially and 0.125, 0.875 on two workers. The guarantee is same
seed and same plan, same answer, and the new section says how to pin a
figure that has to be reproducible by someone else. It also notes that
the gap is Monte Carlo error rather than disagreement, which mc_se
quantifies.cpt_benchmark(),
cpt_batch(), cpt_consensus(), cpt_influence(), cpt_sensitivity()
and ggcpt_compare()) were measured to return identical results
under a sequential and a two-worker plan, stochastic engines included,
because their parallel tasks are deterministic given their input. Tests
now pin both facts: every one of the seven reproduces within a plan, and
the six are plan-independent.cpt_confint(method = "auto") could answer at a different level than
the one asked for, silently. An nsp result carries significance
regions, so "auto" resolves to "native" and reports them at the level
the engine already used: nsp_wrapper()'s alpha = 0.1, i.e. 0.9, so
cpt_confint(res, level = 0.95) returned 90% regions. The level column
said 0.9 throughout, which is honest but only if you inspect it, and
?cpt_confint documented level as ignored by "native" without noting
that "auto" lands there whenever the engine supplied an interval.
Supplying a level the answer does not carry now warns and names the
route it took, the way cpt_monitor() already warns for a tuning
argument that does not affect the chosen method. The default never warns,
and "bootstrap", "posterior" and "nsp" honour the level as before.ci_lower <= cp <= ci_upper, both bounds inside 1..(n - 1), one row per changepoint, and
a non-empty source. Fifty-nine method/route pairs, no violations.cpt_metrics() says what its twelve numbers mean?cpt_metrics now describes each, with the direction that is
better (the same directions cpt_benchmark() ranks by) because a
benchmark table of twelve unlabelled columns is not readable otherwise.annotation_error is a count difference and nothing else, and that
is now said where it can be seen. It is abs(n_pred - n_truth), so a
segmentation with the right number of changepoints in entirely the
wrong places scores a perfect 0: on a 100-point series, predicting 5
against a truth of 90 gives annotation_error = 0 alongside
hausdorff = 85 and f1 = 0. The page now says to read it beside a
location metric, never alone.precision, recall, f1
and rand_index are 0 rather than NA; but hausdorff,
mae_matched and rmse_matched are NA, because they are distances
with no pair to measure. So an all-wrong answer returns f1 = 0 and
mae_matched = NA in the same row, and cpt_benchmark() ranks the NA
last rather than dropping it. Only the both-empty case was documented
before.covering,
hausdorff, rand_index and the matched precision/recall/F1 agree
exactly with brute-force reference implementations (explicit set
intersection over every segment pair, a contingency-table ARI, a double
minimax) on nine hand-built cases and twenty random 400-point ones. The
findInterval() fast path in calc_covering() is a performance change
to a formula, so a test now pins it to the definition.skip_on_cran(), and so does every test that runs stepR: its Monte
Carlo critical values are recomputed in every check, because R.cache
keeps out of the user's cache under R CMD check, and once tcltk is
loaded a fresh simulation costs 32s instead of 5s. The test step of
R CMD check went from a median of 440s to 29s here, over six paired,
interleaved runs (15.1 times faster, 95% CI 14.0 to 15.5, slowest pair
13.4). The skipped tests still run wherever NOT_CRAN is "true": on
every CI runner, on every push, and under devtools::test().seed argument (above) changed them
again. Each of the eleven now seeds itself, in
vignettes/ggchangepoint.Rmd, vignettes/monitoring.Rmd and
vignettes/supervised.Rmd, so a rebuilt vignette is a function of the
vignette. No prose claim depended on the old values.rnorm(),
so those snapshots were a function of test execution order, stable
only because every block above them consumed randomness deterministically.
Scoping the seed argument (above) took that away and four snapshots
changed; the figures were the same layers of a different series, not a
broken plot. Each of those blocks now seeds itself, and the snapshots
reproduce from three deliberately different starting RNG states, which
they did not before.test_that() block, and it used to end one at the line
before the next test_that(, which swept up the section comment
introducing the following test. So a test that touches no optional
engine was reported as calling one, because the next test's header
mentioned it in prose, and the reader was sent to edit a block that was
never at fault. Block extents now come from R's parser (srcrefs cover
an expression and nothing between expressions), which is also exact where
brace-counting would not be: several blocks contain a brace inside a
string, skip("{mcp} is not installed") among them. A test builds a
synthetic file with all five shapes (unguarded-and-innocent, guarded,
genuinely unguarded, brace-in-string, expect_error) and asserts the new
spans flag exactly the one guilty block while the old rule flags two.print() headers padded their labels by hand, and all four had
drifted. print.ggcpt() (the package's most-seen output) put its
values in three different columns, because Changepoints found: is
longer than the pad the other five lines use; print.ggcpt_delay() had
five of six lines right and Average run length: one column out; and
print.ggcpt_path() left Distinct segmentations: unpadded entirely.
print.ggcpt() and print.summary.ggcpt() also indented their values
differently, though they are two views of one object. One internal
helper now emits every field line, so they cannot drift apart again.cat(" Label: ", value, "\n")
puts the separator between the value and the newline, which left a
trailing space on 5 of 13 lines of a ggcpt and 7 of a summary.
(The tibble printed below the header pads its own columns; that is
tibble's output and is left alone.)autoplot(show_ci =), autoplot(show_fit =),
augment.ggcpt() and cpt_confint(method = "native") were each short
by three to four engines. They now name exactly the methods
cpt_methods() marks in its ci and fitted columns, and say why
nsp is marked for uncertainty while being drawn by show_regions
rather than show_ci.?cpt_detect documents the six method/change_in pairs that are routed
to the method's own native change type because the engine has no
separate estimator: not's "var", cpm's "mean" and "var",
kcp's two, and wbsts's "mean". The routing was never silent (the
result's change_in records what was detected), but it was never
written down either. Measured across every method and every value its
registry entry lists; every other combination returns what was asked
for.?cpt_detect names two more engines whose own signature ends in ...,
so a misspelt argument is discarded upstream rather than reported:
fChange and bfast. Every other wired method rejects an unknown
argument by name: checked, rather than asserted.scale_space is not the capability column the docs said it was, on
three help pages. ?cpt_methods, autoplot(type =) and
?cpt_scale_space all grouped it with statistic and path as an
engine internal whose accessor errors without it, so a reader with a
pelt fit was told the scale-space view was closed to them. It is not:
nothing stores a scale space on a result at all,
cpt_scale_space() computes one by sweeping a multiscale detector over
the series, and it returns a full sweep for a pelt result as readily as
for a mosum one. What the column marks is the two engines the sweep can
be run with: the domain of that function's own method argument.
statistic and path do gate their accessors, and still do, with the
list of supporting engines in the message.cpt_methods() documents that online describes the algorithm and
not what cpt_monitor() accepts. The two sets overlap without
coinciding: bocpd is an online algorithm the monitor does not offer,
and edetector is native to this package and has no row in the table.
Also documented: what univariate = FALSE means (a high-dimensional
method, which is what cpt_recommend() filters on: nine of the
fourteen do error on one column, five run and would still be poor
advice), what ci = TRUE covers for nsp, and that posterior = TRUE
does not imply ggcpt_posterior() can draw it.fpop, wbs, wbs2, not,
mosum, idetect, tguh) and fcov_wrapper() credited the wrong
paper: fChange's package citation rather than the covariance-change
method it wraps.**: the
block is not @md, so they are \strong{} now.?ocd_wrapper's timing note understated Monte Carlo construction by
2-3× and has been re-timed, with the machine dependence stated and the
linearity in mc_reps (the part worth planning around) separated
from the absolute numbers.?taylor_wrapper gains a section on series length, because the engine
runs where R cannot look: a setTimeLimit() of 45 s was not honoured
after 170, and Ctrl-C will not stop it either (R checks both at the
same points). n_bootstraps is the knob, and the cost is roughly linear
in it.?wbs2_wrapper gains a reproducibility section. The engine is not
reproducible call to call within a session and no argument can make it
so (repeated identical calls with a byte-identical .Random.seed on
entry returned a last changepoint of either 183 or 188) because the
state that varies is not R's random stream. It reproduces upstream, a
fresh session is deterministic, and it needs a series whose model
selection sits near a tie: across the methods swept, wbs2 on one
configuration was the only case, and tguh (same package) was stable
throughout.?network_wrapper documents that the series the result carries (and so
the one autoplot() draws) is the mean edge weight per time point,
not a coordinate. It is the one multivariate method with no data_wide
slot, because a p×p network has p² entries per time point.fChange wrappers' note on a too-coarse grid said "two or three
columns"; measured on 60 time points, two fail and three, four and six
all return a fit, so the guard above it cannot be raised without
refusing grids the engine handles.cpt_load_tcpd()'s example says why it is \dontrun{} (every call
downloads from the Turing Change Point Dataset's repository) rather
than leaving a reader to guess.@seealso links now connect them. print.ggcpt(), is_ggcpt(),
new_ggcpt() and alarms() gained a description distinct from their
title (roxygen had been copying the title into \description) and
print.ggcpt() documents its return value, which no \value section
had stated.plotly::ggplotly() now
survives a facet column named variable (the name stays coordinate
because that is what the column holds); EnvCpt's arima() stderr leak
would not reproduce on any of eight series chosen to provoke it (the
diversion is kept as a cheap net, and a genuine convergence warning
still reaches the caller); breakfast's "lp" selector does not misfire
on constant data, so pinning "ic" is for reproducibility against an
upstream default of NULL; and ocd's single-column failure depends on
thresh = "MC", which is why the shape is checked in the wrapper rather
than left to the engine.A code review conducted from the sources alone (no R session) raised 102 findings. Each was checked here by measurement rather than by reading, which refuted two of them and turned up one the review had only half-named. 17 are fixed below; the rest are still being worked through in the review's own priority order.
Four are wrong answers.
change_in was inherited from a result object by nobody. Every entry
point that takes raw data forwards change_in to cpt_detect(); every one
that takes a finished ggcpt read object$method and left change_in at
its own default of "mean". So a var or meanvar fit was silently
re-detected as a change in the mean by confint_bootstrap(),
influence_recompute(), cpt_select() and cpt_sensitivity(). On a pure
variance change the mean detector finds nothing, so every bootstrap
replicate was discarded and the caller got a zero-width 95% interval
plus a warning blaming the detector: a symptom pointing away from its
cause. Measured after the fix on a 300-point variance-only series: interval
width 5, no warning, and cpt_sensitivity()'s answer now moves with the
penalty. A value passed through ... still wins over the inherited one.cpt_load_tcpd() substituted NA after unlist(), by which point no
NULLs remained (unlist(list(1, 2, NULL, 4)) has length 3) so a JSON
null did not become NA, it vanished, shifting every later observation
down one and invalidating the human annotations cpt_benchmark() scores
against. The Turing Change Point Dataset ships series with missing values.
One statement out of order.augment() subtracted two different series. .resid was computed
against X[, 1] for the multivariate engines whose param_estimate is a
row mean, so .resid did not equal value - .fitted in the frame it was
returned in. It is now computed against the series param_estimate came
from in every case, and @details no longer claims the coordinate-one
convention universally.ts's seasonal frequency never reached bfast. as_cpt_series()
reduces every input to a bare numeric vector, which threw away the one
thing BFAST cannot guess, and which ?bfast_wrapper tells the user to
supply by passing a ts for exactly that reason. So
cpt_detect(quarterly_ts, method = "bfast") refitted at the wrapper's
default frequency of 12, monthly seasonality on quarterly data, silently.
as_cpt_series() now reports the frequency and the dispatcher hands it to
the engines that take one; an explicit frequency still wins.The rest.
cpt_monitor(method = "cpm") bypassed every guard cpm_wrapper() has.
It built its model straight from cpm::makeChangePointModel(), so a
withheld cpm_type, a missing FET lambda and an off-grid arl0 each
reached the user as an error from inside cpm or base R that named no
argument at all: no applicable method for '@' applied to an object of class "NULL" for two of them, only 0's may be mixed with negative subscripts for the third. The three checks now live in one place and both
doors use them.R <- (1 + R) * inc grows multiplicatively, so under reset = FALSE with relearn = 0
(a configuration the arguments explicitly offer) it passes
.Machine$double.xmax a few hundred observations after a real change and
the next product is Inf. The alarm rule reads a non-finite statistic as
"no alarm": measured 82 alarms and then 1418 observations of total
silence on a stream that had shifted by five baseline SDs. The statistic
now saturates instead, which keeps it ordered against the threshold and
(unlike Inf, which is absorbing) still decays when the stream returns to
its baseline.future.apply documents
that for every future.seed value except FALSE/NULL the caller's RNG
state is forwarded one step, and it is: measured, future_lapply() leaves
a different .Random.seed behind for both future.seed = 7 and
future.seed = TRUE. cpt_batch() and ggcpt_compare() registered the
restore handler only inside their sequential branch, so under
plan(multisession) they broke the promise @param seed makes verbatim.
Both now register it above the branch, as the other three call sites
already did.ggcpt_build() dedups and range-filters cp, normalise_regions() does
neither. print() then reported one fewer changepoint than the regions
table below it and cpt_confint() one fewer interval than there were
regions. The two are now filtered in lockstep, with a warning naming what
collapsed.cpt_annotate_events() mislabelled and then lost events sharing a
position. Two annotated events at one changepoint had one row silently
dropped; matching is now one-to-one, so every event comes back either
matched or undetected_event.cpt_simulate() dropped an out-of-range changepoint and recorded the
filtered set as ground truth, silently, in the function every accuracy
number in the package is scored against. It now warns and names the
dropped values.cpt_metrics_annotated(annotations = list()) returned a malformed
tibble rather than erroring: do.call(rbind, list()) is NULL,
NULL$n_pred[1] is NULL, and tibble() drops a NULL argument, so the
caller got a one-row tibble with the n_pred column missing, four NA
metrics and four base-R warnings about a non-numeric argument. Refused now.cpt_select(criterion = "stability") could die with base R's "argument
is of length zero". The stability curve is NA at every rung with no
changepoints, so an all-NA curve makes which.max() return integer(0).
The criterion now says it could not score the ladder, and names the
criterion, the method and the rungs.cpt_install_engines() offered two packages nothing could use.
tsbox (in "time") and patchwork (in "reporting") appear nowhere
else in the package (not in Suggests, not in R/, not in the tests or
vignettes) and "reporting" installed seven packages where
@param bundle documented six. Both dropped; a test now asserts every
bundled extra is declared in Suggests.import() directives were dead weight and are gone
(@importFrom was already in place for everything actually called). The
seventh, import(changepoint), is load-bearing and stays: glance()'s
cost column calls bare logLik(fit), and the method for class cpt is an
S4 method owned by changepoint, so qualifying the call is what breaks it.
The audit note now sits in the source so the next sweep does not delete it.ggplot2 floor moves from 3.4.0 to 3.5.0:
discrete_scale() is called without scale_name at three sites, and that
argument only became optional in 3.5.0, so ggcpt_compare(layout = "overlay") would have failed on the declared minimum.Depends: R (>= 4.0.0) added: fourteen S3method(base::plot, ...) entries
cannot resolve before R 4.0.0, where plot moved to base.?cpt_delay promised that cpt_metrics() "warns if you point it at" an
online detector. It does not, and it cannot: cpt_metrics() takes bare
integer vectors and never learns which detector produced them. The clause
is replaced with why it cannot.change_in in
cpt_select() exposed that its three closed-form criteria score with a
Gaussian-mean deviance whatever was detected, so a variance ladder is
scored by a cost that barely moves and the criterion tends to choose
K = 0 on a real variance change. @param change_in now says so and points
at "cv" and "stability", which score by re-detection and carry no such
assumption.The remaining 79 findings, worked through in the review's own order. Four turned out not to be defects and are recorded as such below; the rest are fixed, with a regression test each.
Wrong answers, or an answer the object misdescribed.
cpt_monitor(method = "cpm") bypassed every guard cpm_wrapper() has
(above), and the e-detector went silent on overflow (above). Two more
of the same kind:fastcpd's penalty was discarded and then misreported.
cpt_detect(x, method = "fastcpd", penalty = 5) resolved the 5 and threw
it away, and the result reported Penalty: MBIC whatever beta the call
actually used. A numeric penalty is now forwarded as beta, the three
names the two packages share ("MBIC", "BIC"/"SIC", "MDL") are
translated, anything else is left to the engine's own default rather than
silently approximated, and whatever was used is what print() and
glance() report. Measured on a four-segment series: 6 changepoints at
penalty = 1, 3 at 5, none at 50.esac_wrapper() drew two columns from the wrong rows. The ordering
was computed from the NA-filtered changepoints and applied to the
unfiltered CUSUMval and depth, so one NA from the engine
misaligned both: silently, because the lengths still matched.selection_adjusted = TRUE. Its reference distribution assumes the date was fixed in advance,
so quoting it at a date the Bai-Perron program chose is exactly the
circularity the column exists to flag. Now FALSE, and the method string
says "unadjusted". segmented's Davies test stays TRUE (it is the
right object) but its method string now says it is one global test, so
the repeated p-value across rows no longer reads as a test per
changepoint.CD shrinks with N. So a benchmark
where 3 of 8 datasets failed drew a narrower critical distance than the 5
informative ones support, reporting more methods as distinguishable than
they are. n_datasets now counts the datasets that carry a score;
n_datasets_total records the rest.cpts.full() are not nested and can differ by more than one
changepoint; keeping only the first dropped the rest and mis-numbered
step.ts's frequency, and three labels the registry refused.
not(contrast = "pcwsConstMeanVar"), a strucchange formula fit and
nsp(variant = "tvreg") each produced a change_in that
validate_method_change_in() would reject, so a result existed that
cpt_detect() could never be asked for. The registry now lists them, and
the tvreg variant reports "regression" rather than labelling a
regression-coefficient change as a change in the mean.cpt_confint(method = "bootstrap") on a formula fit re-ran a different
model. A strucchange formula fit keeps neither the formula nor data,
so the bootstrap resampled the response and re-ran an intercept-only
breakpoint search, reporting the spread of the wrong search as the
interval of the right one. Refused now, pointing at the engine's own
intervals.Failures with a message that named the wrong thing, or nothing.
cpt_simulate(change_in = "slope", params = c(0, 1)) gave $ operator is invalid for atomic vectors; it now names the shape it wants.
cpt_select(criterion = "stability") could give argument is of length zero; it now says it could not score the ladder. cpt_label_error() with
an unrecognised change value gave replacement has length zero; the
vocabulary is checked at both doors now. cpt_unregister_method(42) gave
base R's invalid first argument. print(rec, top = -1) reached
seq_len(-1). A registered solution_path without a cp column died
inside the plot at idx_vals[path$cp]; it now goes through the same
filtering, step-numbering and selected computation as a built-in path.cpt_metrics_annotated() accepted an empty list (returning a malformed
one-row tibble with the n_pred column missing) and a data frame, and a
data frame is a list, so cpt_metrics_annotated(pred, tidy(fit), n)
read each column as an annotator, scoring raw data values as
changepoint locations and producing plausible numbers.cpt_annotate_events() emptied its own table on a character index: a
character events column matched the index on class, but the lookup was
arithmetic, as.numeric("Q1 2020") is NA, and every event was then
filtered out, so it reported zero matched, zero undetected, and every
changepoint unexplained. A label scale is now matched exactly.nemenyi_cd() validated neither k nor alpha: qtukey() is undefined
below nmeans = 2 and answers with NaN, which drew a diagram with a
NaN rectangle and an all-NA "within CD" column instead of saying
anything was wrong.autoplot.ggcpt_benchmark() reported "no dataset carries ground truth"
when the truth was that every method had errored: sending the user to
fix annotations when the diagnosis was in the error column.cpt_label_error() scored every series' labels against one fit when
given the multi-series label set cpt_learn_penalty() takes; it warns
now. cpt_learn_penalty() passed series with an unbounded target interval
into the fit, where the penaltyLearning path rejects them and the whole
fit silently downgraded to the fallback with a warning naming the wrong
cause; they are dropped and counted now. And a constant training series
puts every scale feature at its floor, which one flat series is enough to
bias the fit toward, that warns too.ggcpt_build() dropped a wrong-length fitted signal without a word,
after which autoplot(show_fit = TRUE) told the user the result "carries
no fitted signal" about a signal the engine had computed. attach_index()
did the same for a wrong-length index.cpt_load_tcpd() treated any download.file() warning as a failure,
deleted the file and reported the specific, wrong diagnosis "not in the
repository (its source does not permit redistribution)".Things that were right and could not be relied on staying right.
sample(resid[idx], length(idx), replace = TRUE) at three bootstrap
sites: R's classic pitfall is that sample(x, n) means
sample.int(x, n) when x is a single number ≥ 1, so a one-observation
segment would resample 1:round(resid). It was safe only because a
length-1 segment's residual against its own mean is exactly 0. All three
now index with sample.int().cpt_influence()/cpt_sensitivity() hard-coded future.seed = TRUE
where the other three parallel call sites pass seed %||% TRUE;
reproducible today only because a local_seed() call happens to precede
them.ifelse(cp >= i, cp + 1L, cp) in the influence loop returns
logical(0) on an empty fit, mixing types in the list of segmentations.cpt_methods()'s ifelse() evaluated both arms, so find.package() ran
for every planned and registered row (including a registration made with
engine = NULL) and the answers were then overwritten with NA.cpt_register_method() validated capability flag names and not their
values, so capabilities = list(ci = 1) registered a method reporting
ci = FALSE and cpt_confint() then told the user their own engine
supplies no intervals. A non-string citation reached cpt_cite()'s
cat(). glance()'s duck-typed cost lookup could read a whole data
frame column.cpt_regions()'s empty return was always three columns while its
@return promises the index columns too, so
rbind(cpt_regions(a), cpt_regions(b)) failed when one was empty and the
other indexed.cpt_power(n = c(50, 500), location = 400) validated against the
largest n and then silently clamped to 48 in the n = 50 scenario.cpt_monitor() validated 3 of its 10 arguments. deltas = 0 makes every
likelihood ratio exactly 1, so the statistic grows on nothing and the
monitor alarms at t = 1/alpha on pure noise; and ... reached the
engine in two branches and vanished in the third.cpm_wrapper()'s sibling pilliat_wrapper() left one of its four
threshold arguments unvalidated, under a comment asserting the set was
complete.npmojo's threshold.val was read with a bare $, where the same file
argues twice for exact [[ because threshold is the rule and
threshold.val the number.cpt_simulate(n = 2) returned a tibble every consumer in the package then
refuses.cpt_update() grew $data one element at a time inside its loop, so
cpt_replay() on 10,000 observations did tens of millions of element
copies.Names, labels and claims that did not match the code.
autoplot(amoc_fit, type = "statistic") labelled its panel "AMOC
log-likelihood-ratio profile". It is a standardised CUSUM divided by the
whole-series standard deviation: the argmax is unaffected, so the
peak was always honest, but the values are compressed by the change
itself. Renamed to what it draws.cpt_power()'s false_positive_rate is a count of extra
changepoints, not a rate: on a scale-mismatched run it reads 137.
Renamed false_positives.contrast column carries a penalty value for
binseg/segneigh, |CUSUM| for wbs, |max.contrast| for not and
\pkg{breakfast}'s own criterion for wbs2/tguh, all under one legend
reading "Contrast". The legend now names the quantity, and @return
says the values are not comparable across engines.?cpt_select and the log-likelihood scale in ?cpt_penalty, with
neither page naming its scale: a reader comparing them would conclude
one was a factor-of-two bug. Both say so now.param_estimate is the segment mean for every method, including the
variance and distribution detectors, and nothing said so, nor that
augment()'s .fitted/.resid, cpt_gt()'s level columns and the
bootstrap's residuals all inherit that convention.autoplot() draws. For
fcov this means changepoint rules can legitimately sit on a visibly
flat line, because a covariance change need not move the mean: now
documented, because it reads as a misfire.decafs and cpop reported their own internal default of 2 log n as a
user-supplied "Manual" penalty, so the object gave no way to tell it
from the dispatcher's stronger MBIC default.ocd_wrapper()'s declared_at column is an exact copy of cp, and
@return presented it as additive: \pkg{ocd} declares a change without
estimating where it began, so there is nothing else for it to hold.
Compare cpm_wrapper(), whose engine supplies both.ggcpt_compare(layout = "overlay") dodges its changepoint rules to
keep two methods' agreeing rules visible, which moves each by up to half
an observation; and neither compare function takes an index, so a ts
is plotted in positions. Both now stated.cpt_simulate(change_in = "slope") restarts the time origin in every
segment, so list(list(intercept = 0, slope = 1), list(intercept = 100, slope = -1)) produces an unrequested level jump. Documented with the
worked example.cpt_test(type = "segment") is always the unadjusted Welch test (the
native routes apply only to type = "jump") and the selection-bias
section described the split by engine alone.print.ggcpt_recommendation() printed [not installed] for a detector
the user had registered that session, because installed is NA by
design for registered methods and isTRUE(NA) is FALSE.cpt_recommend()'s slow-method list contained "changepoints", which is
an engine, so the four high-dimensional dynamic-programming methods
never received the "slow at n" caveat.?geom_cpt_ci documented geom_errorbarh, ?stat_changepoint offered
"rug" (which consumes x/y, not xintercept), ?is_ggcpt said a
ggcpt subclass returns FALSE when the one real subclass returns
TRUE, ?cpt_install_engines listed six reporting packages against a
code list of seven, and a comment counted four planned engines eleven
lines above a table of five.cpt_cite() could never cite a registered method whose name contains an
uppercase letter: the registry is an environment, so its lookup is
case-sensitive, and the name was lowercased first, leaving the user told
to supply a citation they had already supplied.cpt_cite()'s table while
spelled correctly in the roxygen prose; they now use \uxxxx escapes.geom_cpt_event() extracted colour and linetype from the caller's
mapping and then set both as fixed parameters, which beat the mapping
silently, so the two aesthetics a caller is most likely to map were the
two that could not be mapped, while alpha and linewidth worked.autoplot.ggcpt_label_curve()'s band guard was all(is.finite(tg)),
which is vacuously TRUE on a missing attribute and hid the band exactly
when an endpoint is infinite, which is the deliberate signal that the
penalty grid was too narrow, i.e. the diagnosis the reader needs.ggcpt_interactive() called autoplot.ggcpt() directly, bypassing the
one class that both inherits ggcpt and has its own method: the hazard
plot_via_autoplot() was written to avoid, with a comment naming it,
fixed in fourteen places and left standing in the fifteenth.?cpt_register_method along with the
solution_path contract.cpt_install_engines() offered tsbox and patchwork, which appear
nowhere else in the package.autoplot() draws one stacked panel per coordinate
with no cap; at 30 coordinates that is 30 unreadable slivers. It says so
first now.Every other documented measurement, re-run.
The monitoring vignette's false-alarm figures were quoted to a
precision one run cannot support. It read "cpm raises about 3.7
false alarms against the 4 that arl0 = 500 implies, and edetector
about 13": inviting the reader to conclude cpm is calibrated to
within 0.3 alarms. Measured over 20 in-control streams of 2000
observations: cpm averages 3.0 with a standard deviation of 2.0 and a
range of 0-7, and the e-detector 11.5 with a standard deviation of 5.2
and a range of 1-19. Neither original number is wrong (both sit inside
sampling error of the 20-stream estimate) but the spread is larger than
the discrepancy they were being compared against. The section now gives
the mean, the spread and the replicate count, and says that agreement to
within one alarm is not something one run can establish.
Four were accurate and are recorded as such so they are not
re-swept: ?cpt_metrics_annotated's annotator-availability counts (1, 2,
3, 3: exact), ?new_ggcpt's engine object sizes on a 2000-point series
(135.4 / 53.3 / 30.9 MB against a documented 135 / 53 / 31, with the
largest of the others at 0.1 MB against a documented "under 4"),
?strucchange_wrapper's 1.7 / 5.9 / 22.6 MB size scaling (measured 1.6 /
5.8 / 22.1, and the "four times larger each doubling" claim holds), and
?ocd_wrapper's and ?cpt_penalty's figures.
One was stale: ?cpt_power's reproducibility section quoted
power = 0, 1 sequentially against 0.125, 0.875 on two workers for
"two scenarios at n_sim = 8", without saying which two, so it could
not be reproduced. The measurement on a named scenario is 0.25, 0.125
against 0, 0.375. The claim's point survives; the section now names the
scenario and says the numbers depend on it and on the worker count while
the disagreement does not.
And a fourth copy of the scale-sensitivity count turned up in
?cpt_wrapper, phrased as "29 changepoints instead of 1" so that a grep
for the previous fix's wording missed it.
A documented measurement that had gone stale, and the CI step that failed for a reason unrelated to the package.
?cpt_detect, the README and the
introduction vignette were wrong and unreproducible. All three quoted
"pelt returns 1 changepoint at σ = 1, 29 at σ = 3 and 138 at σ = 10"
without saying how long the series was, so the claim could not be
checked. Re-measured at n = 200: 1 / 39 / 141 as means over 20 draws, and
1 / 37 / 142 on a single draw, so the 29 was an unrepresentative draw
rather than a typical value. The pages now give n, say the numbers are
means, and note that the effect grows with the series as well as with the
noise (21/75 at n = 100, 57/266 at n = 400). A test re-runs them with
tolerance, so the property is checked without freezing an upstream
engine's exact behaviour into the suite.?ocd_wrapper's "about 10 s at p = 3, 22 s at p = 10" (measured 9.9 and
21.6) and ?cpt_penalty's "19.9 against 11.8 at n = 360" (exact).Install JAGS (Linux) CI step now cannot be taken down by an
unrelated repository. apt-get update exits non-zero if any
configured repository fails, and the runner image ships third-party lists
this package has nothing to do with: Google's Chrome index returned "Hash
Sum mismatch" and failed all three Linux jobs plus the pkgdown workflow,
on a commit whose previous run had passed on all five runners. The step
drops those lists first, retries, and lets only the jags install decide
its exit status, and since JAGS is optional here, even a genuine failure
to fetch it now leaves the check running.Three engines answered an ordinary degenerate series with a base-R error.
NA, one Inf, all NA, huge and
tiny scale, monotone, and a single spike) turned up three unguarded
paths out of 300 cells, each reachable with an input a user could
plausibly have:
sn fails on any series with a long enough flat stretch, because a
self-normalisation window ends up with zero variance:
missing value where TRUE/FALSE needed. The guard that existed caught
a series that never moves; this is a series that stops moving for a
while. Measured, the breaking run length tracks the window size (6 at
n = 60, 10 at n = 100, 20 at n = 200) so the wrapper now diagnoses the
engine's failure and reports the longest run rather than trying to
predict grid_size.buishand and snht both standardise by the series' own standard
deviation, so a constant series divides by zero and the statistic
reaches \pkg{trend}'s Fortran routine as NaN:
NA/NaN/Inf in foreign function call (arg 1). Both now say what is
undefined and point at test = "pettitt", which is rank-based and
(measured) runs on a constant series and reports no changepoint.bfast decomposes a series into trend and season and a constant series
has neither, so its iteration answered with missing value where TRUE/FALSE needed from inside the optimiser. It now reports no
breakpoints, the way sn already did for a column that never moves, and
the empty result is a usable one, with augment(), glance() and
autoplot() all working on it.wbsts decomposes by wavelet scale, and a
constant series has no spectrum. It now reports no changepoints for a
flat series, like bfast and sn, and diagnoses the flat-stretch case
by naming the longest run of identical values rather than letting the
engine's missing value where TRUE/FALSE needed through.fcov's help page said "there is no argument here that reduces it"
about a method it also describes as costing minutes. There is one, and it
is the biggest lever in the wrapper. Measured at n = 60, p = 6, M = 50:
target = "covariance" (the default) 477 s, "eigenjoint" and
"eigensingle" 21.7 s, and "trace" 2.1 s: the default is some two
hundred times the cheapest, and the wrapper's own example uses "trace"
for exactly that reason while the help page denied the option existed.
The timing section now carries the table, and says plainly that a cheaper
target is a different test rather than a free speedup: the trace is a
scalar summary of the covariance operator, so it is a weaker instrument
that happens to be cheap.fmean/fcov helper attributed one
engine's behaviour to both: "two columns fail and three, four and six all
return a fit". Re-measured per engine at 60 time points (fcov fails at
two columns, fmean returns a fit at two as well) which is why the
shared guard cannot be raised without refusing grids fmean handles.print, 14 tidy, 14 plot, 14 autoplot, 6 [, 2
glance, and one each of the rest) and nothing checked any of four
properties that hold for all of them: print() must return its argument
invisibly (a method that forgets invisible(x) double-prints at the top
level), glance() must be exactly one row and tidy() a tibble, [ must
keep the subclass when every required column survives and drop it when
one does not, and autoplot() must return a ggplot that builds.
Measured across 17 result classes: all clean, in both directions. Now a
test, because these are precisely the conventions a refactor breaks in
silence, and keeping a class whose required column is gone is what makes
a later print() fail.bcp because require() inside the engine puts
it and \pkg{grid} on the search path, and fabisearch because
\pkg{NMF} has to be attached for the engine to dispatch (detaching NMF
alone left eight packages behind, which is why the helper gives back
everything the call attached). Both restore through on.exit, and the
error path was never asserted; a leak would leave eight packages on a
user's search path silently. Measured: the helper restores when its
expression throws, bcp restores after both a successful and a failing
call, and fabisearch restores after a failure that happens after the
attach. Another negative result, now a test.seed contract now covers the failure path, and three functions it
never covered. Every existing seed test measured a successful call,
and an error is exactly when a hand-rolled save/restore leaks:
local_seed() registers its restore through on.exit() in the caller's
frame, so the claim is that an error unwinds through it, and nothing
asserted that. Measured across four error paths and three functions that
were in neither existing test's list (cpt_consensus, cpt_influence,
cpt_sensitivity): the seed is preserved in every case, and a session
that started without a .Random.seed is still left without one after an
error. A negative result, now a test.download.file() opens its destination for writing before it knows
whether the transfer will work, so tcpd_download() writing straight to
the cache path truncated the cached file, and its own unlink() then
removed the remains. Measured: a 72-byte cached nile.json plus one
unreachable URL left no file at all, so a single
cpt_load_tcpd(refresh = TRUE) on a flaky network lost the dataset and
reported only "could not download". One user and one network hiccup; no
concurrency needed. The download now goes to a process-unique file beside
the target and is renamed into place only on success, which leaves a
failed refresh with the cache intact and also makes two processes
downloading the same dataset safe.R CMD build twice in parallel killed one
of them: kcpRS::kcpRS() opens a PSOCK cluster unconditionally
(kcpRS.default() calls makeCluster(ncpu) whenever
ncpu <= detectCores(), so no value of ncpu, not even 1, avoids it),
and base::serverSocket() fails outright when the port it picked is
taken. The two builds collided on port 11246 and one died mid-vignette
with "creation of server socket failed", which is exactly the shape of
failure CRAN's parallel package checks produce. kcp_wrapper() now
retries up to three times on that error and only that error, since
makeCluster() picks a fresh port each time; a genuine engine or
argument failure is still raised on the first attempt. Both parallel
builds now complete.capture.output(), and one of the
review's findings was a printed error tryCatch() never saw. Swept
every engine and verb on clean input for stray stdout: all silent. What
the sweep did turn up is \pkg{Rbeast}'s all-NaN fit, and it is far worse
than documented. The note here claimed "roughly 0.7% of calls, and more
often when other compiled engines are loaded"; re-measured across fresh
processes on one identical series the rate was 0 of 8, 1 of 8, 1 of 10
and 30 of 30, so a session either mostly works or mostly does not, no
rate is quotable, and the old figure was one session reported as a rate.
Three explanations were measured and refuted: other engines loaded (a
66-namespace session ran 5 of 5 seeds clean), do.call() inlining the
series into the call object (inline and quoted-symbol forms were both 10
of 10 clean while a plain direct call produced the all-NaN in the same
process), and the chain configuration (mcmc.seed, mcmc.chains,
mcmc.samples and mcmc.burnin all gave a finite fit on seeds that had
just failed).beast example, its
test and one vignette chunk all called the wrapper unguarded, so
a check landing in a bad session would have failed on an upstream defect
the wrapper already reports honestly: a red build that looks like a bug
in this package. The test now skips with the engine's own message, the
example tolerates it, and the vignette's has_rbeast gate is no longer
"is it installed" but "does it fit here", tried once.message(), which is the easiest of the three to miss.
Swept every engine and verb on a clean series: exactly one is not silent.
bfast pulls in \pkg{strucchangeRcpp}, which overwrites
\pkg{strucchange}'s S3 methods, so R printed a table of method names on
stderr on every cpt_detect(x, method = "bfast") call, about two
packages the caller did not ask for and cannot stop shadowing each other.
need_pkg() now suppresses messages while loading an engine, which is
where it already suppresses the headless-machine Tk warning and for the
same reason: that function's job is to make the engine available, and the
fit runs afterwards, so nothing an engine says about the data can be
hidden by it. Measured for the thing that would matter if it were true:
running bfast first does not change what strucchange answers: the
same changepoint and the same interval before and after, because
\pkg{strucchangeRcpp} is loaded but never attached.NULL call) found
eleven leaks across roughly a thousand cells, and triage kept ten of
them: \pkg{changepoint}'s "increase Q" (the answer is censored, and Q
is an argument here), its SegNeigh cost advice, \pkg{DeCAFS} reporting
that it adapted its lag, and \pkg{EnvCpt}'s perfect-fit note. The one
that had to go is \pkg{binsegRcpp}'s "some consecutive data values are
identical in set=subtrain, so you could get speedups by converting your
data to use a run-length encoding": it fires whenever the series has
ties, it advises an input format binsegrcpp_wrapper() does not accept
(x is a numeric vector), and set=subtrain names an internal split the
caller never sees. Muffled by message, so everything else the engine says
still gets through.call. = FALSE, so conditionCall() is
NULL for a deliberate refusal and non-NULL for one that leaked out of
base R or an engine. The old version carried a list of base-R phrasings:
a heuristic assembled from the failures already seen, which is exactly
why it classified cpt_learn_penalty()'s
missing values and NaN's not allowed if 'na.rm' is FALSE as deliberate
and would have passed over it. A companion test asserts the
call. = FALSE convention that makes the discriminator valid, since one
bare stop() would make a real leak indistinguishable from a refusal.
Re-swept all three faces with it (232 cells across engines, verbs and
accessors) and nothing leaks.cpt_detect() being well-guarded says nothing about them, and cpt_batch,
ggcpt_compare, cpt_simulate and cpt_label_error are where four of the
review's findings lived. A 14-verb by 4-shape sweep turned up one more
unguarded path: cpt_learn_penalty() coerced its series with
as.numeric() and nothing else, so an NA reached cpt_features() and
failed inside stats::mad() with base R's missing values and NaN's not allowed if 'na.rm' is FALSE, naming neither the argument, the series,
nor which of several was bad. It now names the member the way
cpt_batch() does, and predict()'s bare-vector branch, which bypassed
the shared coercion entirely, is guarded at the same standard.1..n exactly, .resid is a residual,
param_estimate is each segment's own mean even for a one-observation
segment, and the three summaries agree with the object about how many
changepoints there are) now run as a test rather than sitting in a
scratch script.Interval coverage, measured for the first time.
cpt_confint() had never been checked against its own nominal level.
Over 120 replicates on a 200-point series with one changepoint and a
three-SD jump, at a nominal 0.95: "bootstrap" on pelt covered 0.992
at a mean width of 2.2, strucchange's native intervals 1.000 at 4.4,
smuce's 0.992 at 4.6, and "posterior" on bcp 1.000 at width 157.
Every route is conservative; none under-covers. ?cpt_confint now carries
the table, because "conservative" is the useful thing to know about an
interval and nothing said it.level behaves less like a confidence level than
like a switch: width 0 at 0.5, 72-91 at 0.8, and 166-187 at 0.95 on a
200-point series. This is not an arithmetic error (the requested level is
delivered in every case, which is now asserted) so the fix is that
cpt_confint() warns when an interval covers more than half its window
and names the mass at the estimate, and ?cpt_confint explains that a
wide interval means the posterior did not localise the change rather than
that the location is uncertain by that much.Example timings, measured for the first time.
--as-cran
runs \donttest{} blocks, so being wrapped in one exempted none of them.
ocd_wrapper 10.4s → 4.2s (its Monte Carlo threshold calibration is
nearly all of the cost and is linear in mc_reps, so the example uses 2),
fmean_wrapper 6.7s → 2.9s and fcov_wrapper 6.1s → 2.8s (10 curves and
M = 50 rather than 20 and 200), cpt_min_detectable 5.3s → 1.5s.
fabisearch_wrapper went from 27s to 5-6s, which is its floor:
n_reps = 1 fails inside fabisearch (its permutation test needs two) and
a smaller matrix is not reliably cheaper, because the search then
evaluates more splits relative to min_dist.?ggcpt_plot_methods (1.3s) and the shared cpt_influence/cpt_leverage
page for running "~160 detector fits". Neither is in the top five; the
four functional and high-dimensional engines are.And the README, which turned out to be the stalest thing in the repository.
README-unnamed-chunk-N-1.png, from
knitr's counter over unlabelled chunks, so inserting one chunk near the
top renumbers every figure below it, orphaning 20 files and breaking 20
image links in a single commit. All 50 chunks now carry a label, so a
figure's filename is a property of the chunk that draws it. The three
figure-producing chunks that had no fig.alt have one, which makes it 23
of 23.seed argument was scoped, and scoping it changed how much of
the random stream each seeded call consumes, so every number below the
first seeded call in the README was the output of a package that no
longer exists, including a print() layout that had been realigned since.
Nothing detects this: the doc-coverage suite checks that every shipped
figure is referenced and that every claim in the prose is true, not that
the recorded output is what the current code produces.cpt_crops() example had degenerated to a single segmentation for
exactly that reason, under a paragraph describing it as computing "every
optimal segmentation over a penalty range"; it shows four again.Four findings the measurement refuted, recorded so they are not re-swept.
import() directives were dead weight; import(changepoint)
is load-bearing for glance()'s logLik() call on an S4 method.changepoints::CV.search.DP.VAR1()'s cpt_hat is a matrix-list with
the same column-major linearisation as test_error, so
cpt_hat[[which.min(unlist(test_error))]] indexes the intended cell. The
reported risk of a nested sublist does not exist.ggcpt_benchmark or cpt_label_error does carry
its attributes through, so neither print() degrades; the %||% guards
added are belt-and-braces and there are now tests that would notice.network_wrapper() deliberately
carries no data_wide and says so, and hdcov takes an \eqn{n \times p}
matrix. Thirty panels build in half a second, so the cost is readability
rather than time.cpt_detect() on a registered method now reports what the contract
had to change about the detector's output. ?cpt_register_method promises
a registration gets "the same contract checks as every built-in wrapper",
and it did -- but silently: a registration returning c(30, NA, 60) gave
two changepoints and no warning, where as_ggcpt() on the same vector
names the value it dropped. Missing, out-of-range, zero, duplicated
and fractional locations are all reported now, with the valid range.
Truncation is the case a count-based check misses: c(30.7, 60.2)
supplies two values, keeps two values, and moves both.sqrt("not a number") inside someone's detector
no longer arrives as a bare non-numeric argument to mathematical function. An error the registration raises deliberately
(stop(..., call. = FALSE), this package's own convention) is the
author's message and still passes through untouched.augment.ggcpt()'s @return named none of the six columns it returns.
It now lists the four added ones -- seg_id, .fitted, .resid,
is_changepoint -- and the three shapes the data half takes: index and
value for a univariate result, index plus one column per coordinate
for a multivariate one, and an extra fitted column for the engines that
supply their own fitted signal.cpt_monitor()'s whole @return was "A ggcpt_monitor object." It now
describes alarms, t/offset, data and the settings it fixes, says
that state is engine internals rather than interface, and points at
alarms() for the alarm columns. cpt_update()'s adds the thing a
reader needs most: the monitor is a value, so an unassigned
cpt_update(mon, y) is discarded.cpt_detect()'s @return was "A ggcpt object." The structure was
documented -- on new_ggcpt()'s page -- but nothing pointed there from
the page a user reads. It now summarises the components and links to the
full list; as_ggcpt() and new_ggcpt() cross-reference rather than
repeat.tidy.cpt_labels() was registered, exported and aliased on no help page:
its roxygen block carried @noRd and @rdname cpt_labels, and the
@noRd won. ?cpt_labels now documents it, like the other 13 tidy
methods. cpt_batch()'s @return gained the columns its tidy() gives.cpt_test() now reports cp_index when the result carries a time index,
as cpt_confint() already did. A user with daily dates got 2020-03-30
from one and 90 from the other for the same changepoint. The column
appears whether or not anything was found (zero-length, of the index's own
type), so results from several fits still stack.cpt_scale_space() no longer blames the bandwidths for an engine's
refusal. ... goes straight to mosum::mosum() or
CptNonPar::np.mojo(), so a single unsupported argument makes every
bandwidth fail at once -- and the message was "No bandwidth produced a
usable fit", which reads as a statement about the sweep. Measured:
cpt_scale_space(x, bandwidths = c(20, 40)) works and adding
index = dates did not, because mosum has no index argument. The
engine's own reason is now kept and reported, with a pointer to the right
help page (and truncated, because R's "unused argument" text deparses the
whole rejected value).cpt_learn_penalty()'s fallback said IntervalRegressionCV() "failed on
this training set". Measured on four series whose every label is a
"change", the engine's actual complaint is target.mat has no lower limits, but should have at least one -- a fact about the label structure,
not about the series. The warning now carries the engine's reason.cpt_metrics() agrees with independent
implementations of every metric it reports, over 800 random instances
including the clustered ones where a greedy matching is supposed to
break. The documented claim that the greedy match "yields a maximum
matching for interval-structured problems" holds against Kuhn's
algorithm; covering, adjusted Rand and Hausdorff match definitions built
from segment index sets; and the seven properties that hold whatever the
implementation (F1 as the harmonic mean of its own parts, swap symmetry,
monotonicity in margin, the [0, 1] ranges) hold everywhere.
cpt_metrics_annotated() is the plain unweighted mean it claims to be.print() and summary() methods of 15
result classes report counts, method names and locations that agree with
the objects they print, and the documented list of engines whose own
signature ends in ... -- the ones that swallow a misspelt argument --
is exactly right, including the three methods (fcov, hdreg,
fabisearch) the existing sweep has to skip for want of a generic input
shape.cpt_confint(method = "bootstrap"), cpt_influence(), cpt_leverage(),
cpt_sensitivity() and cpt_select() rebuilt the request from $method
and $change_in alone. The fit's penalty was dropped, so a
penalty = 40 fit was bootstrapped under MBIC. A change_in the result
records but cpt_detect() cannot be asked for (kcp's
"running mean", envcpt's "trend", wbsts's "var") made every
re-run fail, and the failures were swallowed: a zero-width interval blamed
on "the detector found no changepoints", and an influence table in which
every observation destroyed the segmentation. The penalty now travels,
those labels map back to the request that produced them, a label that
cannot be mapped is refused with the way out, and a re-run that errors is
reported as an error rather than as a finding.$data$value, the first coordinate: with the
change in column 3 of an ecp fit, the bootstrap interval had zero width
and every observation looked maximally influential. cpt_test() tested
coordinate one the same way. Each now says so, and cpt_stability() no
longer calls a multivariate method "univariate".cpt_test() on a regression-mode strucchange fit ran its Chow test on
.y ~ 1, so a break in a slope from 2 to -1 (F = 772 on the fitted model)
was reported as F = 0.26, p = 0.61. It now tests the fitted regression.cpt_influence() and cpt_leverage() errored on every pelt, binseg,
segneigh or amoc fit with change_in = "var" or "meanvar":
engine = "auto" sent them to changepoint.influence, which supports
cpt.mean() models only. Under "auto" an outlier diagnostic also
ignored outlier_sd whenever that package was installed, so one call
meant different perturbations on different machines; "auto" now uses it
for deletions only.cpt_select() refuses a method or change type that cpt_detect() would
refuse, instead of returning a K = 0 "selection" labelled with a typo, and
method = "fpop", change_in = "var" no longer builds a variance ladder
under the fpop label. Its "stability" criterion re-detected with
change_in = "mean" whatever was asked, though ?cpt_select recommended
it for re-detecting with the same change type; the page no longer makes
that claim for "cv", whose estimator is a least-squares change in mean.cpt_label_error_curve() refuses an unknown method up front: a typo came
back as an all-NA curve, after which cpt_learn_penalty() reported that
the labels were "satisfied at every penalty in the grid". A factor member
of cpt_learn_penalty()'s series is refused rather than learned from
as its level codes, and an empty cpt_label_error() keeps its series
column.cpt_benchmark() scored a run that errored as if it had found no
changepoints (covering 0.5 per cell), so a failing engine could outrank
one that ran; failed cells are now NA and rank last, as documented. A
matrix series (TCPD ships several) was flattened column after column
into a series twice as long, with a changepoint at the seam; it stays a
matrix. cpt_load_tcpd()'s catalogue no longer documents two columns it
never had, and cpt_annotations() no longer mistakes a dataset named
series_1 for a single dataset.cpt_confint() warns when an explicit level meets an engine interval
whose level the result does not record (smuce, strucchange, segmented,
bfast, taylor) instead of passing it over in silence.cpt_delay()'s average run length counted a replay's training baseline,
where no alarm can fire: 300 monitored observations with 8 false alarms
read 50 rather than 37.5, in the direction that flatters the calibration
it exists to check. ?cpt_monitor described t and data as including
the baseline and offset as a field of every monitor; it now describes
the object the code builds. The "statistic" plot no longer recommends
cpt_replay() for a "full statistic trace" it does not keep.as_ggcpt() accepts a one-column data frame (it failed with "'list'
object cannot be coerced"), converts ci along with cp under
cp_convention = "right" (the interval sat one position off its
changepoint), and keeps a usable region when another has a missing bound
instead of failing.geom_cpt_region(aes(fill = )), geom_cpt_label(aes(colour = )) and the
labels of geom_cpt_event(aes(colour = )) were silently drawn in the
defaults.cpt_annotate_events() warns about an event it cannot place (a missing
location, or date strings against a Date index) instead of dropping it
from all three outcomes.cpt_batch() refuses an index list that skips a series or mixes index
types (either made tidy() and autoplot() fail with a base-R rbind
error), and reads a POSIXlt index as one index rather than as a list of
its components.cpt_consensus() rounds a fractional count up: min_votes = 2.5 resolved
to a threshold of 2.cpt_power(change_in = "meanvar") simulated a pure mean change; it now
moves the standard deviation too, the design cpt_scenarios() uses, and
?cpt_power states the design for every change type. cpt_scenarios()
crosses rho with the other arguments instead of recycling it down the
rows.?cpt_simulate had its slope recipe backwards: equal intercepts are
continuous only after a flat segment, and its "unrequested level jump"
example was in fact continuous. It now gives the continuity rule and an
example that shows the jump.mcp_wrapper() converted mcp's continuous change location with
round(), but mcp starts a segment at x >= cp, so the last
observation before the change is ceiling(cp) - 1: the old reading was
one position late whenever the posterior mean's fraction exceeded one
half, which for a sharp change is about half the time. The interval
bounds convert the same way.envcpt on a constant series returns no changepoints without fitting
twelve exact models, which leaked summary.lm()'s "essentially perfect
fit" warnings: the only two warnings the test suite raised.autoplot() warns about labels and unknown arguments
it cannot use, as the univariate path does. ggcpt_posterior(),
ggcpt_runlength() and the cpt_select() ladder draw on the result's
time index, and cpt_influence()'s overview subtitle no longer carries
the line breaks and indentation of its source.R CMD check is clean of package-side findings again: ?cpt_labels
documents the arguments of the tidy() method it now lists, two "no
visible binding" NOTEs are gone (one of them was the parallel seed
above), and the "Imports field not imported from: 'Rdpack'" NOTE is
resolved with Rdpack's own importFrom(Rdpack, reprompt).autoplot(object, type = "series") for the summary series, which is the
default call that had just drawn the panels; it now names a route that
works.ggcpt_runlength() drew its heatmap one observation late: column 1 of
ocp's run-length matrix is the prior, before any data, and was plotted as
observation 1. The run length now restarts at the first observation of
the new segment.var_wrapper() reported every change one observation early for an
even-length series and two for an odd one. changepoints' search fits
on every other observation and reports 2c for a change after its c-th
transition, whose last observation is 2c + 1, and for an odd length it
drops observation 1 before pairing the rest and never adds it back. The
location is now converted to the package's left convention; its
regression search, hdreg, already reported that observation.esac and pilliat dropped a constant input column from the result
as well as from the engine's input: it vanished from $data_wide (so
from augment() and autoplot()), and $data$value moved onto the next
column. They now describe the input they were given, as inspect, kcp
and npmojo do.hdcov's minimum spacing compared each candidate with its predecessor
whether or not that one was kept, so 10, 14, 18 at delta = 5 kept only
10 although 18 is 8 from it. It is now measured from the last changepoint
kept.fabisearch warned, on its own documented example, that no split could
be significant whatever the data, and then returned one. The warning
took the engine's p-value for a permutation p-value with a 1 / n_reps
floor; it is a t-test of the refitted losses against the permuted ones,
BH-adjusted across splits, and has no such floor. The warning now fires
only for the rank tests that do have one (testtype = "wilcox" or
"ks"), before the search rather than after it, and ?fabisearch_wrapper
describes the test. n_reps = 1 is refused up front instead of failing
inside the test after the whole search; the engine's "Loading required
package" messages no longer reach the console; and the page no longer
promises a progress note the wrapper never printed.cpt_detect() refuses a malformed penalty by name. NA or a misspelt
name ("mbic") silently became the fpop, cpop, decafs or fastcpd
default, a vector was cut to its first element or printed as two
penalties, and a negative number put a changepoint at every observation.
fpop_wrapper() also reported its own 2 * log(n) default as a
"Manual" penalty, as cpop and decafs had until 0.5.0.wbsts_wrapper(cstar = 1e6) crashed the R session: cstar is a
proportion inside the engine's unchecked C++ search, and above 1 it
failed with "subscript out of bounds" (2) or "only 0's may be mixed with
negative subscripts" (5) before it segfaulted. It is now held to the
method's own range, 0.5 to 1, and ?wbsts_wrapper describes cstar and
lambda as what they are (the unbalancedness parameter and the number
of scales) rather than as "post-processing constants". A lambda that
asks for more wavelet scales than the series has is refused by name.taylor's confidences are checked against the engine's own ranges
(conf_level 0.9 to 0.999, min_conf 0.5 to 1, min_candidate_conf 0.3
to 1), which it then refused under its own names (CI, min_tbl_conf),
and segmented_wrapper() refuses more breakpoints than a series can hold
instead of running without returning at npsi = 1e6.kcp call broke parallel code for the rest of the session.
kcpRS::kcpRS() registers a doParallel backend on a cluster it then
stops, so every later %dopar% failed with "invalid connection" or
waited on the dead socket; fabisearch did so from inside the test
suite. kcp_wrapper() now gives the caller's foreach registration
back, and so does fabisearch_wrapper(), whose engine registers one of
its own when n_core > 1.inspect on a coordinate whose successive differences are
mostly equal (a 0/1 alternation, a noiseless step), which the engine
divides by, failed with "missing value where TRUE/FALSE needed";
hdcov on a constant matrix and network on an all-zero sequence
reached thresholdBS() with a zero threshold and now report no
changepoints; var on 5 rows failed with "Not a matrix."; hdreg on
any series shorter than 4 * delta + 4 (24 at the default) failed with
"replacement has length zero"; and wbsts below 8 observations ended in
upstream's ".........Choose at least two scales.........".?cpt_detect, the introduction vignette
and the README said that every engine outside the penalised
change-in-mean four returns the same segmentation whatever the units.
Measured at a thousandth, one and a thousand times the units, three do
not: geomcp runs PELT on its mapped series, decafs floors its noise
estimate at about 0.03, and bocpd's default prior is on the data's
scale. The notes now name them. ?bocpd_wrapper also described
hazard as the hazard rate 1/lambda; it is lambda, the expected run
length.cpt_penalty() checks k for every penalty type; only MBIC did, so a
negative k returned a negative BIC, AIC, Hannan-Quinn or sSIC
"penalty" that rewards changepoints. cpt_delay() checks max_delay,
which was compared straight against alarm times, and cpt_scenarios()
refuses a missing or non-numeric location or rho instead of
generating a dataset with no change in it, labelled with a jump.fcov's alpha (a bad
value failed inside the engine and was blamed on the grid resolution, or
ran and found nothing), bcp's mcmc, ocd's beta and train,
strucchange's h and breaks (breaks = 0 fitted one break),
mcp's iter, adapt and chains, inspect's lambda and
threshold, mosum's and npmojo's G, sn's grid_size, wbs's
threshold (a string ran and found nothing), wbsts's scales, the
gamma_set and lambda_set grids of var and hdreg (an NA in
either ran and found nothing), fastcpd's order, and fabisearch's
n_runs, n_reps, n_core, alpha and rank (NMF refused a bad
rank itself, but only after the call had attached NMF).hdbinseg is archived on CRAN again, contrary to the 0.5.0 roadmap's
note that it was back at 1.0.3. sbs therefore stays in the planned
table, alongside gfpop, robseg, FOCuS and changeforest, and all
five now read "when on CRAN". cpt_register_method() is the supported
route to any of them today.inst/CITATION.Config/testthat/edition: 3).
The whole suite passes unchanged under it, and it is what makes
announce_snapshot_file() available, without which a plain test_dir()
deletes every visual snapshot as unused and the next run silently
regenerates them.vdiffr visual-regression snapshots for every autoplot() type and
every new layer: the package had no visual net at all, so a dropped
layer or an inverted axis could pass every existing test. They are a
local net: an SVG snapshot records the font stack of the machine that
made it, so they are skipped on CRAN and on CI rather than reporting a
failure on every platform but one.test-050-registry.R, -index, -inference, -diagnostics,
-supervised, -engines, -tools) plus 25 visual snapshots in
-visual, all engine-dependent tests guarded with
skip_if_not_installed(). The suite runs about 2350 assertions with the
engines installed and about 1470 without them.stats, tools and utils are declared in Imports; the new engines
and extras are in Suggests behind requireNamespace() guards, as
before: 35 engines inside a 56-package Suggests list, and the package
still checks clean with none of them installed. withr joins Suggests,
which the tests already used, and rjags, which mcp_wrapper() tests for
because having \pkg{mcp} does not imply JAGS can be reached.future::plan(multisession), and the wrappers that the suite only ever
reached through cpt_detect() (esac_wrapper(), pilliat_wrapper(),
kwc_wrapper(), not_wrapper(), wbs2_wrapper(), trend_wrapper(),
taylor_wrapper(), wbsts_wrapper()) are now called directly, so their
own argument handling is covered.Imports and none of the Suggests. The second is
the only thing that exercises the no-Suggests path the DESCRIPTION
promises, and it caught a test that asserted geom_cpt_event(repel = TRUE)
builds: true only where \pkg{ggrepel} is installed. That assertion now
covers both worlds instead of one.tests/testthat/setup.R sets rgl.useNULL. fabisearch imports rgl,
which warns twice about the X11 display the moment its namespace loads on
any headless machine; the option is rgl's own way to say no window is
needed, and it keeps the suite's output about the package.Everything from 0.4.0 keeps working. Almost all the additions are new
functions, new optional arguments with their previous defaults, or new
optional slots on ggcpt that are absent unless something supplies them:
is.null(fit$regions) remains the test for "this engine does not do
regions", exactly as data_wide has always worked. Three changes are worth
naming rather than leaving to be discovered:
autoplot() now prefers a time index carried on the result over the
observation position. This affects only results built with the new
index argument, which did not exist before.cpt_methods() returns nine capability columns by default. Code that
reads it by name is unaffected; code that reads it by position, or checks
ncol(), is not. cpt_methods(capabilities = FALSE) returns the 0.4.0
shape.cpt_detect() gained index and y as its fifth and sixth formal
arguments, ahead of .... Named calls are unaffected. A call that passed
a wrapper's own argument positionally past penalty (which no example
or vignette ever did, because ... arguments have always had to be named
to reach the right engine) would now bind it to index.cpt_detect() grows from 13 to 31 wired methods. Eighteen new wrappers, all
of whose engines live on CRAN and enter Suggests behind
requireNamespace() guards:
smuce_wrapper(): SMUCE/HSMUCE multiscale inference (stepR), the first
engines to populate ci_lower/ci_upper confidence-interval columns.cpop_wrapper(): exact change-in-slope detection (cpop);
cpt_detect(change_in = "slope") now routes here or to NOT's linear
contrast instead of erroring.bcp_wrapper(), bocpd_wrapper(), beast_wrapper(): the Bayesian
pillar (bcp, ocp, Rbeast), with posterior_prob columns and the
posterior mean carried as a fitted signal.cpm_wrapper(): sequential distribution-free detection (cpm), with a
detection_time column.kcp_wrapper(): kernel change point analysis on running statistics
(kcpRS; mean, variance, autocorrelation, correlation).npmojo_wrapper(): nonparametric MOSUM under serial dependence
(CptNonPar).decafs_wrapper(): abrupt changes amid drift and AR(1) noise
(DeCAFS).sn_wrapper(): self-normalised segmentation (SNSeg; mean, variance,
acf, bivariate correlation).inspect_wrapper(), ocd_wrapper(), geomcp_wrapper():
high-dimensional and multivariate detection (InspectChangepoint,
ocd, changepoint.geo).strucchange_wrapper(): Bai-Perron structural breaks with break-date
confidence intervals (strucchange); accepts a bare series or a
regression formula.segmented_wrapper(): broken-line regression with kink confidence
intervals (segmented).envcpt_wrapper(): changepoints vs. trends vs. autocorrelation model
selection (EnvCpt).fastcpd_wrapper(): the modern fastcpd engine (fastcpd), covering
mean/variance/meanvariance plus AR/ARMA/GARCH model changepoints.cpt_crops() computes the full CROPS penalty path and returns a
ggcpt_path object with print(), tidy(), and
autoplot(type = c("elbow", "path", "segmentations")).cpt_batch() runs one detector over many series (matrix, data frame,
or list) with optional future parallelism; returns a ggcpt_batch
tibble with tidy() and a faceted autoplot().cpt_stability() bootstrap stability diagnostic: segment-preserving
resampling with a detection-frequency profile and autoplot().ggcpt_posterior() (posterior mean + per-location
changepoint probability) and ggcpt_runlength() (the BOCPD run-length
posterior heatmap).ggcpt_interactive() renders any result as a plotly widget.cpt_cite() returns the verified methodological reference(s) behind
a result or method name.autoplot.ggcpt() gains show_ci (draws changepoint-location confidence
intervals from ci_lower/ci_upper) and show_fit (overlays the
engine's fitted signal), and renders multivariate results as faceted
small-multiples.geom_cpt_ci() migrated off the deprecated ggplot2::geom_errorbarh()
to geom_errorbar(orientation = "y").autoplot()/ggcptplot() now
warn instead of being silently discarded, and plotting an empty ggcpt
errors cleanly instead of producing infinite axis limits.cpt_detect(penalty = <number>) works for the changepoint-package methods
(pelt, binseg, segneigh, amoc): a numeric penalty is now translated to
the engine's penalty = "Manual", pen.value = <number> instead of erroring
with "Unknown Penalty" (#2).binseg / segneigh no longer crash on short series that pass validation;
the maximum number of segments Q is clamped to a length-safe value (#3).augment() uses the engine's fitted signal when the result carries one,
instead of always the per-segment mean (#4).augment() keeps all coordinates for a multivariate result instead of
dropping everything but the first (#5).segments table's start / n columns are integer, matching the
documented schema (#6).signal_mix() gains a minimum-n guard and filters its changepoint indices,
so true_changepoints no longer contains 0, n, or duplicates for small n
(#7).autoplot() honours the index argument for multivariate results (#8).cpt_batch() / ggcpt_compare() no longer crash with "factor level
duplicated" when two series share a name, or the methods vector repeats (#9).wbs returns an empty result instead of erroring when a manual threshold
admits no changepoints (#10).idetect returns an empty result on short series instead of erroring with
"wrong sign in 'by' argument" (#11).ecp_wrapper() no longer fabricates changepoints on no-change data (the
positional boundary strip reversed c(1, n+1)), and no longer drops
genuine changepoints in e.agglo's wrap-around case (C1).wbs_wrapper() now returns the sSIC model selection it documents; a
manual threshold is recorded as the penalty actually used (C2).cpt_detect() now error on multi-column input
instead of silently flattening it column-major (C3).idetect_wrapper() returns an empty result on no-change data instead of
erroring (C4).tguh_wrapper() pins breakfast's model selection to "ic": no more
spurious changepoint on constant data, no crash on short series, and the
scalar-0 "no changepoints" sentinel is handled (C5).glance() is always one row: fpop's per-position cost vector no longer
explodes the tibble, and $ partial matching no longer grabs unrelated
fit elements (C6).mosum_wrapper() records the numeric threshold as penalty$value (was
the string "critical.value") and implements its documented multiscale
argument via mosum::multiscale.localPrune() (C7, C8).cpt_detect() forwards change_in to NOT via contrast mapping and the
result reports what actually ran (C9); penalty = "None" resolves to 0
for numeric-penalty engines (C10).cpt_penalty("sSIC") implements the strengthened SIC k * log(n)^alpha
(was 0.5 * k * log(n), weaker than BIC) (C11).ggcpt_eval() uses the same one-to-one matching as
cpt_metrics() and its "Miss" legend entry renders (C15).ggcpt_compare() keeps a facet panel for every method, including those
that found nothing, and no longer errors when no method finds anything
(C16).stat_changepoint() sorts by the x aesthetic before detecting (results
were previously row-order dependent) and declares dropped_aes so
building the plot is warning-free (C17).signal_blocks() generates the true Donoho-Johnstone blocks signal
(cumulative jumps, not absolute levels) (C18); simulated t-noise is
rescaled so its standard deviation matches sd (C19); all signal
generators validate their minimum lengths (C20).cpt_wrapper(cp_method = "SegNeigh") falls back to the SIC penalty the
engine supports instead of always erroring under the default; np
results report change_in = "distribution"; meanvar results stay
"meanvar" in the user's vocabulary; ggecpplot() handles multivariate
input without crashing.smuce_wrapper(), cpop_wrapper(),
bcp_wrapper(), bocpd_wrapper(), beast_wrapper(), cpm_wrapper(),
decafs_wrapper(), strucchange_wrapper(), segmented_wrapper() and
envcpt_wrapper() turned a 120x2 matrix into a 240-point series. The new
cpt_crops() and cpt_stability() entry points guard the same way (R16).segneigh no longer errors with "subscript out of bounds" on short
series. The Q clamp added for #3 missed the engine's real constraint:
Segment Neighbourhood requires Q >= 3 regardless of length, so the
clamped Q of 1 or 2 failed for every n < 8. Q is now clamped into
the engine's valid window (3 <= Q <= n - 2 for a mean change,
floor(n / 2) + 1 when a variance is estimated per segment), and a series
too short to admit any valid Q gets an actionable message naming the
constraint instead of the engine's internal error (R17).index no longer crashes
mv_data_wide() with "Column name index must not be duplicated"; it is
made unique against the position column, so ecp, inspect, geomcp,
ocd, npmojo, kcp and fastcpd all accept such data (R18).NA changepoint indices from an engine are dropped rather than
propagating into build_segments() as an "NA/NaN argument" error, and any
engine-supplied extra columns (ci_lower, posterior_prob, ...) stay
row-aligned through the drop (R19).cpt_penalty()'s "MBIC" no longer misattributes its formula to Zhang and
Siegmund (2007), whose modified BIC penalty depends on the segment lengths
and cannot be written as a function of n and k alone. The computed
value is unchanged; the documentation now states what it is (BIC plus a
combinatorial placement term) and how it differs.cpt_detect() no longer errors
with "formal argument ... matched by multiple actual arguments". The
dispatcher derives some arguments from change_in and was passing them
alongside the caller's, so the documented ... passthrough was broken for
not's contrast, cpm's cpm_type, kcp's running_stat, sn's
parameter, fastcpd's family and hsmuce's family. A value supplied
by the caller now wins over the derived one (R20).smuce_wrapper(family = "poisson") (current stepR accepts no
such family, so it always errored) and
cpm_wrapper(cpm_type = "GLRAdjusted"), which cpm::processStream()
rejects by printing an error and returning no changepoints, making it
silently report "no changes" for any input. cpm_type = "FET" is retained
and documented as needing 0/1 data plus a lambda value.ocd_wrapper() no longer advertises univariate input: ocd's detector
cannot be constructed for a single coordinate (it fails with "subscript out
of bounds"), so a bare vector now gets a message naming the requirement
instead of the engine's internal error (R22).sn, kcp, npmojo, inspect) or, for segmented, a
spurious kink recovered from a singular fit (R23).inspect,
npmojo and kcp standardise each coordinate, so one flat column (a dead
sensor channel, say) made their statistics undefined and the whole call
failed with "missing value where TRUE/FALSE needed" even when the other
coordinates carried an obvious change. Flat coordinates are now dropped
with a warning naming them, detection proceeds on the rest, reported
locations stay in the original row space, and the dropped coordinates are
still kept for plotting (R24).kcp and sn explain themselves on series too short for their windows,
instead of surfacing "wrong sign in 'by' argument" and "only 0's may be
mixed with negative subscripts" (R25).print() and summary() no longer render penalties at full double
precision or with a placeholder value: Penalty: Manual = 17.8459510605346 is now Manual = 17.846, and a penalty that carries no
numeric value prints as MBIC rather than MBIC = NA.The whole exported surface was exercised with degenerate, contract-violating and self-generated input. Items are listed with the ones that change an answer or end a session first.
hsmuce no longer aborts the R session. When a series carries essentially
no noise at the per-segment scale, stepR's heterogeneous variance
estimator does not raise an R error but terminates the session, so
nothing downstream can catch it and the user loses their work. It is
reachable straight from cpt_detect(x, method = "hsmuce"). Two regimes
were measured as fatal: a globally flat series such as
rep(4, 300) + rnorm(300, 0, 2e-7), and (more dangerous, because it looks
entirely ordinary) a clean step whose segments are numerically constant,
c(rep(0, 150), rep(5, 150)) + rnorm(300, 0, 1e-9), which is what
cpt_simulate(sd = 0) produces once any rounding is added. Both are
refused when the point-to-point variation lies more than about seven orders
of magnitude below the data's own scale, with a message naming
family = "gauss", which handles the whole range. An exactly noiseless
series is safe upstream and still works (R53).idetect no longer invents changepoints on a constant series.
IDetect::ID() is erratic on flat input: its statistics go to 0/0, and
what it returns depends on the value and the length: rep(3, 200) came
back with 126 changepoints at 1, 3, 4, 6, 7, ..., while rep(0, 100)
errors and rep(-2.5, 60) returns a sentinel 0. Every other search
wrapper reports none, and the 0.4.0 audit fixed exactly this class of bug
for segmented, sn, kcp, npmojo and inspect: idetect was
missed. It now short-circuits to the empty result, decided by exact
equality so a series with tiny but genuine variation still reaches the
engine (R50).cpt_stability() reports the quantity it documents. freq is described as
"the proportion of replicates detecting a changepoint within margin of
that index", but the loop incremented once per changepoint, so a
replicate whose detections had overlapping ±margin windows was counted
twice at the shared indices; pmin(hits / B, 1) then hid the overflow by
clipping it. The effect was to inflate exactly the number the function
exists to report: in a measured example an index that only half the
replicates covered was shown as 1.00, "re-detected every time". Each
replicate now contributes at most one to any index, so freq is a genuine
proportion and needs no clipping (R38).glance() always returns the single row it documents. new_ggcpt()
defaulted method and change_in to character(0), so tibble()
recycled every other column down to zero rows: an empty summary for any
hand-built result, including the one the README demonstrates. Those
defaults are now NA_character_, and glance() coerces the metadata
fields to length one whatever the object carries (R37).changepoint engines (pelt, binseg, segneigh, amoc, np) keep
their upstream cpt object in $fit. It was NULL, although $fit is
documented as "the raw upstream object" and every other engine stored one,
which also left the inherits(fit, "cpt") branch of glance() unreachable,
and with it a sign error and a wrong element index that had never run.
glance()$total_cost now reports the unpenalised −2 log L for those
engines where changepoint exposes it on that scale, and stays NA where
it does not, rather than mixing two scales in one column; ?glance.ggcpt
spells out which cases are which (R35).ggcpt_interactive() works on multivariate results. The faceted
small-multiple that autoplot() builds for them used a facet column named
variable, which is also the name plotly::ggplotly() gives a column of
its own when it melts the built plot, so every multivariate result failed
with "Names must be unique". The column is now coordinate; the facet
strips are unchanged (R36).add_column() reject the
wide frame with "must have unique names as of tibble 3.0.0"; the R18 fix
had only deduplicated a coordinate named index against the position
column, not the coordinates against each other. All coordinate names are
now made unique in one pass (R34).envcpt no longer prints its engine's internal failures as though the
call had failed. EnvCpt fits up to twelve models with try(), and a
non-silent try() writes its error straight to stderr, so on a degenerate
series envcpt_wrapper() printed six lines beginning "Error in arima(...):
non-stationary AR part from CSS" and then returned a perfectly good result.
Those failures are expected (the criterion ignores the models that did not
fit) so the message stream is diverted for the duration of the call.
Genuine warnings are deferred past the diversion and still reach the user,
and a call that really does fail still errors (R52).cpt_simulate(change_in = "meanvar") works without params. It was the
one change type with no parameter default, so the call died with
"replacement has length zero" instead of simulating anything (R32).cpt_simulate() warns about recycled parameters for every change type,
not only "mean". Supplying fewer parameters than there are segments
reuses the last one, so the trailing entries of changepoints were
recorded in true_changepoints with no actual change behind them:
silently wrong ground truth for "var", "meanvar" and "slope" (R32).cpm ships
thresholds only for a fixed set of average run lengths; for any other
arl0 its processStream() prints "Error: No thresholds available for
selected ARL0" and returns an empty result instead of raising a condition,
so tryCatch() never saw it and the wrapper reported zero changepoints on
a series with an obvious one: the same trap the earlier audit found for
cpm_type = "GLRAdjusted", on a different argument. And kcp_wrapper()
with nperm below 2 either reported nothing (0 or negative) or died inside
the engine with an unreadable row.names error (1). Both are refused now,
with the supported arl0 values named in the message (R61).conf_level no longer hangs strucchange_wrapper().
stats::confint() on a breakpoints fit at level = 2 never returns, and
the tryCatch() already around that call cannot rescue a call that does
not terminate, so the session simply locked up. conf_level is now
required to lie strictly between 0 and 1 in both strucchange_wrapper()
and segmented_wrapper(). In the same sweep: bocpd_wrapper(hazard) and
cpop_wrapper(sd) must be positive, and wbs_wrapper(n_intervals) at
least 1: all previously accepted meaningless values (R60).cpt_simulate() refuses parameters that made it emit NaN. It is where
ground truth for every benchmark comes from, so a silent series of NaN is
the worst thing it can produce, and sd = -1, sd = NA, and |rho| >= 1
under the AR(1) model each did exactly that, with no error and no warning
(sqrt(1 - rho^2) is not a number outside the stationary range).
Non-positive n is refused too. rho is checked only for
noise = "ar1", so a stray value the chosen model ignores is still
accepted (R59).show_segments, show_ci, show_fit, show_line,
show_points and mosum_wrapper(multiscale) are all documented as
"Logical" but were read with isTRUE(), which treats everything that is
not TRUE as FALSE. So show_segments = 1, = "yes", = "TRUE" or
= NA quietly drew nothing, and show_line = 1 quietly removed the line
the user was asking to keep: three layers down to one. show_points = NULL
keeps its documented meaning of deciding from the series length (R58).stepR refuses an alpha outside (0, 1),
SNSeg an unlisted confidence) but ggchangepoint's were taken on trust,
and out-of-range values returned answers instead of errors:
cpt_metrics(margin = -3) scored a perfect segmentation as precision 0
and recall 0; cpt_stability(B = 0) produced a stability profile of NaN;
cpt_metrics(n = -10) a covering metric of −1; bcp_wrapper and
beast_wrapper with prob_threshold = 0 reported 239 changepoints in a
240-point series; kcp_wrapper(alpha = 2) and cpt_crops(pen_min = -5)
ran regardless. All are refused now, with the legitimate boundaries
(margin = 0, B = 1, n = 1, prob_threshold = 1) still accepted
(R57).mosum_wrapper()'s automatic bandwidth is never 1. min(n / 10, 100)
rounds to 1 for every n < 20, and a one-observation window leaves the
engine's studentised statistic undefined, so it warned "NaNs produced" and
returned spurious changepoints rather than failing. The automatic
bandwidth is floored at 2, and a series too short for any window gets an
actionable message (R29).npmojo_wrapper()'s default bandwidth is capped at n / 2, the largest
the engine accepts. The documented max(20, 0.1 * n) exceeded that for
every series shorter than 40, so the default always failed with
"Bandwidth is too large for the length of time series". Series of 40 or
more observations are unchanged (R30).cpt_wrapper(change_in = "np") refuses cp_method values other than
"PELT" up front. changepoint.np::cpt.np() implements PELT only, so
"BinSeg" and "SegNeigh" used to die on the internal Q clamp with
"unused argument (Q = 5)" and "AMOC" surfaced the engine's "Invalid
Method" (R27).autoplot(), ggcptplot() and ggecpplot() reject an index whose
length does not match the series, naming the argument at fault, instead of
surfacing dplyr's recycling error ("x must be size 200 or 1, not 10"),
which never mentions index (R26).cpt_penalty() enforces the argument ranges it documents (R28):
alpha > 1 for "sSIC" (at or below 1 it is weaker than BIC, so no
longer a strengthened SIC); n >= 3 for the log-based penalties
(log(n) is 0 at n = 1 and log(log(n)) is negative below n = 3, so
the "penalty" rewarded extra changepoints); and 0 <= k <= n for
"MBIC", whose log C(n, k) term is -Inf beyond that. "AIC", which
does not involve n, is exempt.cpt_batch() names the series that failed. It exists for panels of
hundreds of series, but an error in any one of them surfaced only as the
underlying complaint ("x must have at least 3 observations") leaving
the user to bisect the list to find which. The message is now prefixed
with the series name and its position, e.g. Series `short` (2 of 3):
(R49).cpt_detect() records the cpt_detect() call in $call.
It previously held the internal helper each branch happened to use, e.g.
wrap_cpt_to_ggcpt(x = data_vec, change_in = ci, ...), an unexported
function named with the dispatcher's local symbols, which a reader can
neither recognise nor re-run. Wrappers called directly still record
themselves (R33).cpt_cite() on a result with no method name says so, instead of surfacing
tibble's "Can't subset rows with refs$method == method" (R37).ggcpt_eval() no longer warns "No shared levels found ..." when there is
nothing to draw: a run with no predictions and no ground truth is a
perfect score, not a broken plot (R31).ggcpt_compare() pads its changepoint rules by a fixed amount on a flat
series, as ggcptplot() already did; a zero data range would otherwise
collapse them to invisible zero-height segments.glance() no longer carries an unreachable branch. It tested
inherits(fit, "cptrange"), but the changepoint class is cpt.range
(with a dot) so the branch could never fire, and its body used $ on an S4
object, which would have errored had it ever been reached. Removed; the
BinSeg/SegNeigh case is handled explicitly alongside the other engines
whose cost is on a different scale (R46).?new_ggcpt and ?ecp_wrapper explain why $fit is NULL for "ecp"
and only for "ecp": ecp::e.agglo() returns a cluster-progression matrix
that is quadratic in the series length, so retaining it by default would
make the result object explode on a long series: 207 kB of fit for a
1.3 kB series at n = 160 alone (R46).cpt_metrics()
compared every truth segment against every prediction segment, so scoring
a segmentation with many changepoints crawled: 7.5 seconds for 3000 of
them. Because both partitions tile the series and their breakpoints are
sorted, only the overlapping prediction segments can win, and two
findInterval() lookups locate them; the same case now takes 0.42
seconds. The numbers are unchanged: verified identical on 4010 cases
(4000 random plus adversarial partitions) and pinned in the tests against
an independent set-based statement of the definition (R42)..onLoad() is gone. It re-registered print, plot,
summary, tidy, glance, augment and autoplot at load time
(writing into base's and generics' S3 method tables) even though
NAMESPACE already declares every one of them, and it wrapped the lot in
suppressWarnings(), so a genuine registration failure would have been
invisible. It was a leftover from before @exportS3Method base::generic
was adopted in 0.3.0. Verified redundant before removing: all eleven
methods still dispatch with and without the package attached, and every
declared generic/class pair still resolves through getS3method() (R41).?ocd_wrapper says how long it takes. Nearly all of ocd's cost is Monte
Carlo threshold calibration, which happens before a single observation is
read: measured at mc_reps = 5, construction is about 3 s at p = 3, 9 s at
p = 10 and 55 s at p = 50, and four times that at mc_reps = 20, so the
default mc_reps = 100 extrapolates to roughly a quarter of an hour at
p = 50. The help now gives those numbers, notes that monitoring the
observations afterwards is comparatively free, and points at thresh,
which takes the three thresholds directly and skips calibration entirely.
That escape hatch had no test; it has one now (R56).stats is declared in Imports.cpt_metrics() and ggcpt_eval(), its segments
feeding geom_cpt_segment(), the object itself feeding cpt_cite(). The
chain was run for all 31 methods: it completes for every one, and 24 of
them recover both planted changepoints with precision, recall, F1 and
covering all exactly 1. The exceptions are all correct by construction:
amoc finds at most one changepoint, cpop and segmented are slope
engines being shown a step, ocd is online and reports declaration times,
and geomcp unions its distance and angle mappings. A six-method version
spanning the structural variety is now in the suite (R55).cpt_simulate() and every canonical signal was run
through all 31 methods to confirm none of them can produce input that
terminates the session. Three configurations do land in the degenerate
band and are now refused by hsmuce rather than crashing it: sd = 0 and
sd = 1e-9 for a change in mean, and (the one the audit turned up)
change_in = "slope" with sd = 0, whose consecutive differences are a
constant slope, so its point-to-point variation is floating-point residue
of about 1e-14 rather than zero. Nothing else crashes on any of them, and
the realistic settings and all five canonical signals are unaffected
(R54).change_in translations are tested. cpt_detect()
derives an engine-specific argument from change_in for not, cpm,
kcp, sn and fastcpd; the suite covered overriding those through
... but never the derivation, so a wrong translation would have silently
run the wrong analysis. Each is now checked against the equivalent explicit
call (R51).cpt_metrics()'s one-to-one matching is verified to be a genuine maximum
matching, which is what ?cpt_metrics claims and what precision and recall
are derived from: if the greedy scan ever fell short, both would be
silently understated. Checked against an exact maximum bipartite matching
on 300 random configurations plus seven clustered and interleaved patterns
chosen to break a greedy rule: it never falls short (R48).ggcpt_posterior() on a beast_wrapper() result (the help says it handles
both bcp and BEAST, but only the bcp branch of the profile extractor was
ever run), and every guard on ggcpt_posterior()/ggcpt_runlength():
non-ggcpt input, a result with no posterior, and a prob_floor that
leaves nothing to draw (R47).ggcpt_compare_table() and cpt_metrics_annotated()) alongside a set of
documented modes and arguments that nothing exercised:
ecp_wrapper(algorithm = "agglo"), sn_wrapper(parameter = "bivcor"),
cpt_simulate(noise = "ar1" | "rw"), signal_mix(),
autoplot(show_segments = TRUE), the "no changepoints detected" print
paths, and the sd/breaks/model_param/lambda/threshold/G
arguments of the cpop, strucchange, DeCAFS, inspect and mosum wrappers.
All of them worked; none of them was guarded against a future refactor
(R45).expect_error(fpop_wrapper(X), "univariate") and the fpop half of the
scale-sensitivity note) so on a machine with no fpop they met
"Package 'fpop' is required" instead of the message under test, which is
an ERROR rather than a skip on CRAN's noSuggests flavour. The earlier
_R_CHECK_DEPENDS_ONLY_ run had missed both because the fallback library
it used still exposed part of Suggests; the suite is now verified against
a library holding the Imports and nothing else. Both assertions are
guarded and the pelt half of each stayed unguarded, so the cases that need
no Suggests still run. A static sweep of every test_that() block for a
Suggests package used without a matching guard found no others (R62).ggcpt_compare() and ggcpt_compare_table() refuse a multi-column x
instead of flattening it. Both run univariate detectors but took
as.numeric(x) on trust, so a 160x2 matrix was unrolled column after
column and the join between the columns read as a level shift: the table
came back with changepoints at 80 and 160, and 160 is the seam, not a
feature of either series. Every wrapper already refused wide input through
the same check; these two entry points were the only ones that did not.
Non-numeric input now names the argument as well, rather than failing
inside as.numeric() with "cannot coerce type 'object' to vector of type
'double'". The message points at cpt_batch(), which is what runs a
detector over a panel (R63).ggcpt_compare() hands future.apply a documented future.seed value.
It passed seed straight through, and seed defaults to NULL, which is
not among the logical/integer/list values future_lapply() documents, so
every parallel comparison run without an explicit seed was outside that
contract. It now sends TRUE in that case, asking for parallel-safe
L'Ecuyer streams, which is what cpt_batch() already did. Sequential runs
are unaffected. Found by exercising the parallel branch of both functions
for the first time: it is documented in three vignettes and both help
pages, and no test had ever set a non-sequential future::plan(). The
branch is otherwise correct: same changepoints as the sequential path,
series names preserved, ... forwarded, and the "which series failed"
error still named (R64).?strucchange_wrapper says how large its result is. Measuring
object.size() for every engine on one series turned up a single outlier:
a strucchange result is quadratic in the series length, because
breakpoints() keeps RSS.triang, the triangular table of segment
residual sums of squares that lets it return the optimal segmentation for
any number of breaks without refitting. On a 3.2 kB series it comes to
1.7 MB at n = 200, 5.9 MB at n = 400 and 22.6 MB at n = 800 (about four
times larger per doubling) and the table's share of that grows from 85%
to 95% over the same range. One fit is nothing; a few hundred from
cpt_batch() are, so the help now says to keep $changepoints rather than
the whole list of results. Nothing changed in the object: this is the same
size-versus-usefulness trade-off already documented for ecp in the
opposite direction, and it was simply unstated. Every other engine is
ordinary: the median result across the other thirty is under ten times
the size of the series it was given (R65).cpt_methods() lists
gfpop, robust, focus and sbs with status = "planned", but
cpt_detect(x, method = "gfpop") went to match.arg(), whose message
enumerates the thirty-one wired methods, so it did not contain the name
the user had just read out of the table. The table said the name existed
and the dispatcher said it did not. It now reports what the method is
waiting on and which package it will be built on; an outright unknown name
still gets the ordinary list. In the same pass, sbs's entry was out of
date: it said "when on CRAN", but hdbinseg returned to CRAN as 1.0.3 in
September 2025, so the only thing standing between sbs and a user is the
wrapper. gfpop was removed from CRAN and robseg and FOCuS have never
been on it, so those three still read "when on CRAN" (R66).?cpt_detect gains a scale-sensitivity section, and the README and the
introduction vignette repeat it: pelt, binseg, segneigh and fpop
weigh the penalty against a raw segment cost when detecting a change in
mean, because changepoint's Normal cost fixes the noise standard
deviation at 1 and fpop's lambda penalises the residual sum of squares
directly. Neither rescales the data, so wider noise makes the penalty
negligible and the segmentation shatters: on one true changepoint with a
five-sigma jump, pelt returns 1 changepoint at sigma = 1, 29 at sigma = 3
and 138 at sigma = 10. The note gives the three remedies (standardise the
series, scale the penalty by the noise variance, or use
change_in = "meanvar") and records that every other engine estimates or
cancels the noise scale itself; both halves of it are pinned by a test
(R39). ?cpt_wrapper, ?fpop_wrapper and
?cpt_penalty point at it. Behaviour is unchanged; the trap was simply
undocumented, and the package's own examples all use unit-variance data,
so nothing exposed it.?cpt_detect, ?fpop_wrapper, ?cpop_wrapper and ?decafs_wrapper now
record that the dispatcher and those wrappers do not share a default
penalty. cpt_detect() resolves its "MBIC" default to a numeric value
that is stronger than the wrappers' own 2 * log(n) (19.9 against 11.8 at
n = 360) so cpt_detect(x, method = "decafs") reports 3 changepoints
where decafs_wrapper(x) reports 5 on the same series. Both defaults were
documented individually; that they differ was not. Passing penalty
explicitly makes the two entry points agree (R40).?npmojo_wrapper records that the engine calibrates its detection
threshold by bootstrap, so the value stored in the penalty descriptor
varies between runs unless set.seed() is called first (or a manual
threshold is passed through ...).?cpt_detect warns that a misspelt engine argument can pass unnoticed.
wbs, not, Rbeast, strucchange, segmented and fastcpd all end
their own signature in ..., so an unrecognised name forwarded through
cpt_detect()'s ... is discarded upstream and the engine quietly uses
its default. Intercepting it here would risk rejecting
arguments those engines legitimately forward deeper, so the behaviour is
unchanged and documented instead.geom_cpt_segment() example that could not run (it
was given xintercept, but the geom needs x/xend/y/yend); DeCAFS
and EnvCpt filed under multivariate methods when both are univariate;
is_ggcpt() demonstrated on the input series rather than the result; a
claim that only three engine packages are required; and a method-family
count that disagreed between the package help, the README and the
vignettes (all now six: the feature-tour vignette was the last straggler
and still said five).ocd_wrapper() test uses mc_reps = 10 rather than 50. Those
repetitions only calibrate the detection threshold, and the change the test
plants is far too large for the calibration to matter: 10 reps give the
same declaration as 50 and take 7 seconds instead of 36, cutting the whole
test suite from 74 to 42 seconds with the assertions unchanged.ocd_wrapper() example runs in 3.6 seconds instead of 20. It was by
far the slowest example in the package (ocd's Monte Carlo threshold
calibration scales with both the number of coordinates and mc_reps) and
a smaller, cleaner problem (100x3 with mc_reps = 5) demonstrates the
wrapper better anyway: it reports one declaration just after the true
change, where the old example also produced a spurious second one.cpt_cite("tguh") but 2022 (50(5), 2721-2761) in the
vignette bibliography: the same paper with two sets of coordinates; the
bibliography is corrected to match, and its key renamed accordingly.
?ecp_wrapper cited the arXiv preprint of the ecp software paper while
both vignettes cited its published form, so inst/REFERENCES.bib now
carries the Journal of Statistical Software version (62(7), 1-25). A new
test cross-validates all three sources: shared BibTeX keys must describe
the same publication, every \insertRef key must resolve in
inst/REFERENCES.bib, and every @key cited in a vignette must resolve in
the vignette bibliography (R44).?stat_changepoint says which geoms actually work with it. The stat emits
one xintercept per changepoint and drops x/y, so "vline" (the
default) and "rug" fit while "point" errors; the help previously read
as though any geom would do.?geom_cpt_ci no longer claims an x aesthetic is required. The layer is
a horizontal error bar, so it needs y, xmin and xmax; x is accepted
but unnecessary, and neither of the package's own call sites
(autoplot(show_ci = TRUE) and the feature-tour vignette) supplies it, so
the help contradicted the package's own usage (R43).?augment.ggcpt now says what the columns mean for a multivariate result:
every coordinate is returned and seg_id/is_changepoint apply to the
whole row, but .fitted and .resid describe the first coordinate only:
the same one $segments$param_estimate summarises.?cpt_penalty now records the one silent
substitution the dispatcher makes: changepoint does not implement MBIC
for Segment Neighbourhood, so cpt_detect(method = "segneigh") falls back
to "SIC" on the default penalty and its result is therefore not directly
penalty-comparable with a PELT one.gfpop engine (never wrapped) has been removed from it.vignette("ggchangepoint")) walking the full
exported surface, including the per-engine wrappers, theme_ggcpt(), and
annotate_segments().?ggchangepoint) was rewritten to describe the
unified ggcpt framework and the current 13-method engine list (it previously
still claimed "only three changepoint packages").cpt_methods() introspection helper returning a tibble of every known
method, its engine, availability status, and whether the engine is installed.ggcpt class: summary(), as_tibble(),
as.data.frame(), format(), and plot().cpt_penalty() gained a documented per-engine penalty-semantics section.cpt_detect() no longer advertises 13 methods that errored at runtime;
match.arg() now enumerates only the wired methods (B7).cpt_detect() validates method × change_in combinations and errors with a
clear message instead of silently mislabelling the result (B3).signal_blocks() now produces the correct Blocks signal; the segment levels
previously collapsed to a single step because the assignment loop ran in
reverse (B1).cpt_metrics() uses one-to-one matching, so recall and f1 can no longer
exceed 1 (B2), and no longer warns on empty pred/truth (B6).ecp_wrapper() returns a correct per-coordinate cp_value for matrix and
data.frame input instead of a column-major flattened scalar (B4); cpt_detect()
no longer flattens multivariate input before passing it to ecp.stat_changepoint() maps detected indices back to the x aesthetic so rules
land at the correct location on non-1:n axes (B5).glance.ggcpt() now reports a measured runtime and populates total_cost
from the underlying fit when available (B8).augment.ggcpt() renames data columns position-independently, so it no longer
breaks when the data carries more than two columns (B11).cpt_simulate() @return now documents the seg_id column it actually
returns (B9), and the dead show_segments parameter was removed from the
internal plot helper (B10).ggcpt S3 result class with tidy(), glance(), augment(), and autoplot() methodscpt_detect() unified dispatcher for changepoint methodsgeom_changepoint(), geom_cpt_segment(), geom_cpt_ci(), stat_changepoint()ggcpt_compare() and ggcpt_compare_table() for method comparisoncpt_metrics(), cpt_metrics_annotated(), ggcpt_eval()cpt_simulate()/rcpt() and canonical test signalscpt_penalty() helpertheme_ggcpt() and annotate_segments() for plot customisationecp_wrapper() no-change bug fixed: spurious boundary changepoints and NA no longer emittedsize → linewidth migration: cptline_linewidth replaces deprecated cptline_sizematch.arg() input validation added to all wrappers"_PACKAGE" sentinelchange_in = "np" alias added (keeps "cpt_np" for backward compatibility)show_points auto-off above 500 obsindex parameter for time-series axestestthat test suite with coverage for all new and hardened functionscpt_wrapper(), ecp_wrapper(), ggcptplot(), ggecpplot().