ggchangepoint 0.5.0
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 extension mechanism
The highest-leverage addition, and the one everything else leans on.
- New
cpt_register_method()/cpt_unregister_method()/cpt_registered_methods()teachcpt_detect()about a detector this package does not (and often cannot) depend on: an engine that is not on CRAN, a Python detector reached throughreticulate, a neural detector, a proprietary in-house method. The registered method then works withautoplot(), the geoms,tidy()/glance()/augment(),cpt_metrics(),cpt_consensus(),cpt_benchmark(),cpt_stability()andcpt_report(). - New
as_ggcpt()turns any set of changepoints (a published paper’s reported breaks, an analyst’s annotations, another package’s output) into a validatedggcpt, running the same contract checks as every built-in wrapper. - Registered methods are visibly user-supplied:
cpt_methods()gives themstatus = "registered",print()marks their results, andcpt_cite()returns the citation the registration supplied or states plainly that none was given.
The engine registry
- The wired-method table, the capability check and the dispatcher’s routing are now all derived from one declarative registry, so a new engine declares its capabilities once instead of in three places that had to be kept in agreement by hand.
-
cpt_methods()gains capability columns:multivariate,univariate,online,ci,fitted,posterior,statistic,path,scale_space.subset(cpt_methods(), ci)$methodanswers “which methods give me a confidence interval?” directly. - New
cpt_install_engines()installs a whole family of engines at once ("core","bayesian","nonparametric","highdim","functional","regression","inference","applied","time","reporting", or"all"), with adry_run.
fpop comes from R-Forge
-
fpopwas 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 inAdditional_repositories,cpt_install_engines("core")fetches it from there, and the “Package ‘fpop’ is required” error now gives theinstall.packages()call that works, because a bareinstall.packages("fpop")no longer finds it.fpopw, which exports anFpop()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, wherefpopwas exact in all of them.
Time indices and data structures
-
cpt_detect()gainsindex: detection still runs on positions (every wrapped engine assumes an equally spaced sequence) but the index is stored on the result and threaded throughtidy()(ascp_index),augment(),autoplot()(axis and labels),cpt_confint(),cpt_annotate_events()andcpt_report(). An index that is not equally spaced warns rather than silently mislabelling the axis. -
ts,xts,zooand (unkeyed)tsibbleobjects are accepted directly and their own index is carried through. Newas_cpt_series()is the one place that separates the values from the clock. - New data-frame interface:
cpt_detect(df, y = value, index = date), whereyandindexaccept a bare column name, a string or a position. A data frame passed withoutykeeps its 0.4.0 meaning.
Inference
- New
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. - New optional
regionsslot onggcpt, read withcpt_regions(), drawn by the newgeom_cpt_region()layer and byautoplot(show_regions =), which is on by default for a result that has regions. NSP’scpcolumn is the interval midpoint and says so, in thecp_sourcecolumn, inprint(), and in the documentation: the region is the inferential object, the midpoint is not an estimate. - New
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 asourcecolumn. - New
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. Aselection_adjustedcolumn 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.
Choosing the number of changepoints
- New
cpt_select()builds one candidate ladder and scores it by any of six criteria:"bic","mbic"(the real Zhang-Siegmund segment-length mBIC, whichcpt_penalty()cannot express),"aic","crops_elbow"(the knee rule made explicit and citable rather than eyeballed),"cv"(order-preserved cross-validation viacrossvalidationCP: 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.
Diagnostics
- New
cpt_influence()implements the Wilms-Killick-Matteson influence family: delete and outlier perturbation, re-rendered in ggplot2 withplot_type = "overview" | "location" | "parameter" | "map". It useschangepoint.influencewhere that applies and a generic recomputation everywhere else, so it works for every wired and registered method. - New
cpt_leverage()ranks observations by a composite influence score. - New
cpt_sensitivity()sweeps tuning parameters and shows the detected locations across the grid: the direct answer to “is this robust to the penalty?”. - New
cpt_statistic()/ggcpt_statistic()return and draw the detector’s criterion as a function of location; newcpt_solution_path()/ggcpt_solution_path()return and draw the order in which candidates entered the model; newcpt_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.
Supervised detection
- New
cpt_labels()andas_cpt_labels()build labelled regions: the ground-truth representation shared withcpt_metrics_annotated(), so the package has one notion of an annotation rather than two. - New
cpt_label_error()scores a segmentation in label errors;cpt_label_error_curve()traces them across a penalty grid and reports the target interval. - New
cpt_learn_penalty()fits the max-margin interval regression of Hocking et al. (2013), delegating topenaltyLearningwhen it is installed and falling back to a built-in squared-hinge fit. The result haspredict(), andcpt_detect(x, penalty = model)andcpt_penalty(model, series = x)accept it directly, as do the wrappers that take a numeric penalty. - New
geom_cpt_label()draws the labels, andscale_fill_cpt_label()colours them by assertion or by correct / false-positive / false-negative status.
Choosing and combining methods
- New
cpt_consensus()runs several detectors and reports the locations they agree on, with a vote count and the methods behind each. Matching reusescpt_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. - New
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.
Communication
- New
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. Newgeom_cpt_event()draws them. - New
cpt_report()assembles a reproducible artifact (method, citation, penalty, locations with intervals, regions, segments, optional stability and events, the call, andsessionInfo()) as markdown or plain text. - New
cpt_gt()renders a publication-ready changepoint table throughgt, degrading to a tibble with a note whengtis absent.
Benchmarking and evaluation
- New
cpt_benchmark()runs a method-by-dataset grid, scores every cell withcpt_metrics()(orcpt_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. - New
cpt_datasets()builds an offline, deterministic collection from the package’s own canonical signals, so the benchmark runs insideR CMD check. - New
cpt_load_tcpd()downloads and caches the Turing Change Point Dataset undertools::R_user_dir(), with its multi-annotator ground truth intact; newcpt_annotations()returns the per-annotator sets one row at a time, so the disagreement between annotators stays visible.
Streaming and online monitoring
- New
cpt_monitor()creates a stateful sequential detector, fed bycpt_update()and read withalarms(). Three methods:cpm,ocd, andedetector. - Selecting columns off one of the new result tibbles (
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 ownprint()method cannot read.dplyr::select()on one of these behaves the same way;filter()and row indexing keep the class, as they should. -
edetectoris 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 statisticM_tsatisfiesE[M_t] = t, so optional stopping at the alarm time gives a finite-sample lower bound of1 / alphaon 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. - A monitor re-learns its baseline after an alarm (
relearn), so a persistent change is reported once rather than on every subsequent observation. It is also dimensioned at construction: feedingcpt_update()a different number of coordinates is an error rather than a silent coercion. - New
cpt_replay()runs a whole series through a monitor; newcpt_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.
Simulation, power and study design
- New
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. - New
cpt_min_detectable()inverts it: the smallest change reaching a target power, for pre-registration and study design. - New
cpt_scenarios()builds a reproducible grid of simulation settings as data, ready forcpt_benchmark(). -
cpt_simulate()gainsseasonality(sine or sawtooth) andsd_trend(smoothly varying noise scale, distinct from the piecewise-constantchange_in = "var"), so the conditions the dependence-aware and seasonal engines exist for can actually be simulated.
Engine wave #2: 19 new methods
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.
-
Inference:
nsp(nsp). -
Bayesian:
mcp(mcp): formula-based multiple-changepoint regression with full posteriors. Needs JAGS, a system dependency, and says so plainly when it is missing. -
High-dimensional:
esacandpilliat(HDCD) for sparsity-adaptive mean changes;hdcov,network,varandhdreg(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. -
Functional and network:
fmeanandfcov(fChange);kwc(KWCChangepoint), robust depth-rank segmentation;fabisearch(fabisearch), network structure via non-negative matrix factorisation. -
Applied vocabularies:
pettitt,buishandandsnht(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. -
Nonstationary and fast:
wbsts(wbsts) for second-order changes;binsegrcpp(binsegRcpp) as a fast binary-segmentation path across several loss functions.
Accessibility
ggcpt_interactive()gainsengine = "ggiraph"alongside the existing path. ggiraph renders the ggplot itself to interactive SVG, so facets and every layer survive, which ’s own model does not always manage for a faceted multivariate result.autoplot()gainslabels =: pass acpt_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 aggcptcarries generated alt text, which knitr and Quarto pass through to the rendered image.
Fixes found in the post-implementation audit
- A method registered with
cpt_register_method()is now visible insidefutureworkers. The registry lives in the package namespace and a worker loads the package fresh, socpt_batch(),cpt_benchmark(),cpt_consensus(),ggcpt_compare(),cpt_influence(),cpt_sensitivity()andcpt_power()used to fail on a registered method underplan(multisession)with a misleading “‘arg’ should be one of” error. Each now carries a snapshot of the registry to the worker. -
cpt_benchmark()acceptschangepointsas ground truth alongsidetruthandannotations, treats a list-valuedtruthas 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 silentlyNA. -
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 is installed. First, the default segment model is plateau-only (list(y ~ 1, ~ 1)), so 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’stcolumn viapar_xunless the caller supplies their own. Second, having the package is not the same as being able to run it: installs on some platforms and only fails when it looks for the JAGS library at run time, in which casemcp::mcp()returns a fit with no posterior samples and a warning, andsummary()on that died with “subscript out of bounds”. The wrapper checks for the samples and reports the real cause. The documented claim that “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 askedrequireNamespace(), which loads the package, so building the table pulled in all thirty-five namespaces, including , by way of , 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 formethod = "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.HDCD1.1’sPilliat()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 atesac, which is unaffected; the refusal lifts automatically once a fixedHDCDis 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, underregion_start/region_end;cpt_confint()looked only forci_lower/ci_upperand so bootstrapped 200 re-runs of the detector to produce a weaker statement than the one already on the object. Thesourcecolumn now distinguishes"nsp_region"from"native", and reports NSP’s global level. -
bfast_wrapper(change_in = "seasonality")works. reports “no breakpoints in this component” as a bareNArather than an emptybreakpointsobject, so the declared capability errored with$ operator is invalid for atomic vectorson any series whose seasonal amplitude is stable. Asking for seasonal breaks withseason = "none"is now an error rather than a puzzle. -
binsegrcppno longer claims a variance-only change. has no variance-only cost, sochange_in = "var"was mapped to a distribution the engine does not have;"mean"and"meanvar"are what it offers. -
taylor_wrapper()validatesn_bootstrapsagainst the engine’s real range (100 to 1,000,000) instead of letting a smaller value fail insideChangePointTaylorwith a message about its own misspelled argument. - New
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,strucchangecosts about 135 MB (a triangular O(n^2) RSS matrix),bfast53 MB andbocpd31 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 anindexargument and inherits one from an indexedggcpt. 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_labelsandcpt_label_errorhad no method, sotidy()failed on half the surface;glance()also reports the one-row summary aggcpt_delayalready carries. -
cpt_monitor()names a missing value in the baseline instead of reporting it as zero variability, andcpt_delay()refuses atruththat falls past the end of the stream, is empty, or is non-positive, each of which used to be scored as a clean miss. - The e-detector’s average-run-length bound is attributed to optional stopping on (M_t - t) everywhere it is described. The README and the
alphaparameter’s documentation still credited Ville’s inequality, which is a different statement.
Fixes found in the pre-submission audit
-
plot()works on every result class. Thirteenplot()methods were missing, soplot()on a selection, monitor, batch, benchmark, influence, sensitivity, stability, path, power, delay, events or label-curve result fell through toplot.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 norautoplot(), arriving at the moment a new user is most likely to typeplot(result). -
plot()on a subclass now draws the subclass’s figure.plot.ggcpt()calledautoplot.ggcpt()by name rather than dispatching, soplot()on acpt_consensus()result silently produced the plain changepoint plot instead of the consensus one. - Every
plot()method now draws as a side effect and returns theggplotinvisibly, soplot()works inside a loop or a function whilep <- plot(result)still gives you the object to add layers to. - A factor series is refused instead of being detected on its level codes.
cpt_detect()coerces the series before validating it, socpt_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. - A logical series is accepted everywhere.
cpt_detect()coerced one before validating andcpt_select()validated before coercing, so a 0/1 series worked in one and was refused by the other. - Empty input is reported by this package rather than by base R:
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 reachedX[, 1]and gavesubscript out of bounds. - Loading an engine no longer warns about the machine.
mosumreachestcltkthroughplot3Dandmisc3d, so the firstcpt_scale_space()orcpt_statistic()call on any headless box (a server, a container, a CI runner, a cluster node) warnedno 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, sogeom_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()andggcpt_eval()say what is wrong when handed aggcpt. 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, andas.integer()answered it with'list' object cannot be coerced to type 'integer'. Aggcptis also a list, socpt_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 atidy()table. -
cpt_monitor()warns when a tuning argument the chosen detector ignores is explicitly supplied. The three detectors are calibrated in different currencies (edetectorbyalpha,cpmbyarl0,ocdbypatience) and each ignores the others’, socpt_monitor("edetector", arl0 = 5000)changed nothing at all.?cpt_monitorand the monitoring vignette both said so; now the call does too. The knobs that apply, and the defaults, stay silent. -
?cpt_monitormarksmethod = "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 requestedlocationis clamped into2..(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()validatesmarginthe waycpt_metrics()does. A negative margin was accepted and drew its tolerance rectangles inside out (xmin > xmax) while the metrics function refused the same value. - A registered method must detect on the series it was given. When the registered function returned a finished
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 andndescribed that series instead ofx: the wrong series to plot, the wrong number of rows fromaugment(), the wrongnfor every metric. The comment and?cpt_register_methodboth 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.cpwas coerced undersuppressWarnings(), soas_ggcpt(c("a", "b"), x)returned a clean-looking result with zero changepoints, andas_ggcpt(factor(c("60", "90")), x)returned changepoints at 1 and 2: the factor’s level codes. A logical vector is refused too, pointing atwhich(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-lengthfittedsignal. It was dropped silently, after whichautoplot(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()andfcov_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 ’s basis expansion, which stopped with base R’ssubscript 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. - A factor is refused wherever the package reads changepoint locations, instead of being read as its level codes.
as.integer()on a factor returns level positions (alphabetical unless the caller setlevels) socpt_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 bareas.integer():cpt_metrics()(predandtruth),cpt_metrics_annotated(),ggcpt_eval(),cpt_delay(),as_cpt_labels(),cpt_labels(),cpt_label_error(),cpt_simulate(),cpt_benchmark()’s dataset annotations andas_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. - A factor time index is read as labels rather than as level codes.
cpt_detect(x, index = month.abb)was accepted whilecpt_detect(x, index = factor(month.abb))was refused with “indexmust be non-decreasing”, because the codes of an alphabetically levelled factor are5, 4, 8, 1, 9, .... An ordered factor does carry its order in its codes and keeps the ordering and spacing checks. -
cpt_report()validatesfilebefore building the report. A path in a directory that does not exist, a directory,NAor a two-element vector each produced a base-R connection error naming neither the argument nor the package, andfile = ""printed the report to the console and wrote no file at all, leaving the caller with a report they believed they had saved.fileremains ignored forformat = "gt", as documented. - Four columns that were returned but never documented are now in their
@return:cpt_regions()carries through whatever extra columns the engine supplied (nsp_wrapper()addsvalue, the region’s statistic),cpt_scale_space()returnsdetectedalongsidesignificant(a location can clear the threshold without surviving the engine’s own pruning);cpt_label_error()returnsseries, andcpt_benchmark()returnsn_annotators. -
cpt_annotate_events()addscp_indexonly when the result carries a time index. It was created unconditionally and filled with a bareNA, so the same column was aDateon an indexed fit and a logical on an unindexed one, and every unindexed result carried a mystery all-NAcolumn.attach_index()andcpt_confint()both key on the column’s presence, so this now does too. Its@returnalso documentscp_indexandevent_value, which it had never named. -
autoplot()on acpt_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 whileautoplot()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. - A result in which every observation is its own segment now says so.
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; atn = 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: withpenalty = 0(which is whatpenalty = "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. - Vignette figures render at
dpi = 72rather 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 installeddocdirectory from 4.9 MB to 3.5 MB, with no visible change to the figures:html_vignettedisplays them at their natural size, so fewer pixels means a smaller file, not a smaller picture.
Fixes found in the final pre-submission sweeps
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.
Wrong answers
-
wbstscould not report more than one changepoint.wbsts::wbs.lsw()ends insuppressWarnings(if (is.na(OUT)) OUT = NULL), which was a warning before R 4.2 and is an error after it, andOUThas 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.seedand replays the engine’s own body withall(is.na(OUT))in place ofis.na(OUT)(the reading itssuppressWarnings()shows was intended) so the result is upstream’s answer retrieved, not a different one. -
hdcovandnetworkfailed on 44-96% of runs, at random.changepoints::thresholdBS()prunes withfor (i in 2:level_length), so a binary-segmentation tree with one level runs the body ati = 2,table(...)[2]isNA, and1:NAstops with base R’sNA/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:hdcovfailed on 23 of 25 runs at n = 120, p = 8 and 24 of 25 at n = 400, p = 20;networkon 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 reproducesthresholdBS()’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 bareas.numeric(), whichcpt_detect()has refused since 0.4.0: a factor member became its level codes (an alphabetical ordering of the labels) and a character member becameNAs. A matrix member was worse, becauseas.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, twentyNAs lost the changepoint entirely, and an all-NAsecond half reported two changepoints at 12 and 14 that the data does not contain.cpt_wrapper()andcpt_detect()had always refused this; only theecproute was open. -
cpt_wrapper()andggcptplot()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 conventionggecpplot()already had. -
cpt_replay(),cpt_monitor()andcpt_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.locationwent through no validation at all, and the scenario loop clamps withmax(2, min(cp, n - 2)), solocation = 1e6at 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 returningpower = 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 onecpt_detect()accepts) the rate ismean(all-NA), and aNaNis a number the caller could plot. The first engine error is now reported with it, andcpt_min_detectable()stops on it rather than reachingif (power < ...)and answeringmissing 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:wbstsandbinsegrcppwere markedpath = TRUEbut expose no solution path tocpt_solution_path().
Engine arguments, answered by name
A sweep of all 64 wrapper argument slots and of every wrapper against every argument its engine accepts.
-
Twelve wrapper/argument pairs collided with a value the wrapper pins for itself: SMUCE’s
jumpint, bocpd’sgetR,wbs2/tguh’ssolution.pathandmodel.selection,sn’splot_SN,envcpt’s andbeast’s narration switches,decafs’swarningMessage, and R answeredformal 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. -
Twenty-three pairs collided with an argument this package renames.
...is documented as reaching the engine, so the engine’s own name is the natural thing to pass:mindistformin_dist,Mforn_intervals,cpmTypeforcpm_type,ARL0forarl0,lambdaforpenalty, and so on. Each now redirects to the argument that does the job, or says the value comes fromxand is not the caller’s to set. -
Four entry points that are not engine wrappers were never swept, and had fifteen collisions between them.
cpt_crops()pins the two arguments that make its call CROPS at all (method,penalty) and the interval it sweeps (pen.value);cpt_wrapper()andggcptplot()rename the package’smethodtocp_method, somethod(the most natural name to reach for) was the one that broke; andcpt_monitor()renames or pins five ofcpm’s andocd’s (cpmType,ARL0,MC_reps,dim,beta), whichcpt_replay()inherits by forwarding...to it. Each now redirects by name, andggcptplot()names itself rather than the function it shares the rename with. -
Twenty-four arguments were forwarded to the engine unchecked, which reported them from deep inside itself as
missing value where TRUE/FALSE needed,negative length vectors are not allowedorNAs in foreign function call.burnin,min_size,cstar,lambda,alpha,patience,mc_reps,wsize,kmax,lag,npsi,minseglen,confidence,frequency,ord,N, the twothreshold_*constants,n_perm,sigma,df,seed,startupandarl0are all validated by name now. -
Seven engines refused a short series in their own vocabulary, two of them with their own typos:
notreportsmax.length must satisfy 3 < max.lenght <= n,envcptMinimum segment legnth is too large,wbstsbase R’ssubscript 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:bfastneeds2 * frequency,strucchangeneedsh * n,envcptmore than2 * minseglen. -
Five multivariate-only engines failed on a single column with a message that named nothing:
hdcovnon-conformable arrays,network'x' must be an array of at least two dimensions,varincorrect number of dimensions,kwcdim(X) must have a positive length, andgeomcpa clearer line that still named neither method nor argument. All five now match the four engines that always named the requirement. - Every refusal of a non-finite series now carries the count (one stray
NAin 10,000 points and a half-missing series are different problems with different fixes), from one definition rather than seven copies.networkwas the one high-dimensional route with no finiteness check at all, and a singleNAsurfaced asreplacement has length zerofrom inside its random edge-splitting. - A data frame with one non-numeric column is named rather than blamed whole:
as.matrix()returns an all-character matrix, sox must be numericleft the reader to find which column. The note also says what coercing a factor would have given. - Error messages name the argument the caller actually passed.
as_mv_matrix()saidxwhere the argument wasbaseline,new_obs,seriesorresponse;cpt_scale_space()reportedMethod `scale_space` is univariate, naming a method the caller had never heard of;cpt_replay()reported a non-finite value againstbaselineornew_obsdepending 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 atattempt to set an attribute on NULL. -
ggcpt_interactive()validateswidth_svg/height_svgon theggiraphpath, wheregirafe()answered`width` must be a scalar positive numberabout its own internal argument. -
cpt_monitor()refuses a multi-columnbaselinefor 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 abocpdormcpresult names the accessor that does work (ggcpt_runlength(), andci_lower/ci_upperrespectively) instead of being a dead end reached fromcpt_methods()’ ownposteriorcolumn.
cpm’s two thresholds
-
The documented
arl0grid was half the real one.cpmships 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 everycpm_type, and 50000 is the ceiling. -
cpm_type = "FET"needs alambdaand had no default. Without oneprocessStream()dies insidecpmwithonly 0's may be mixed with negative subscripts. It is now refused by name, with the two valuescpmships FET thresholds for (0.1 and 0.3). - The “No thresholds available” branch reads the printed line rather than assuming: the same message covers
arl0andlambda, and blamingarl0for alambdafailure sent the reader after an argument that was already correct. -
"ExponentialAdjusted"is offered again. It was withheld alongside"GLRAdjusted"as one of two typescpm’s own dispatch rejects; re-measured againstcpm2.3, it runs and returns changepoints. Only"GLRAdjusted"is genuinely rejected upstream.
The caller’s session
-
Two engines rewrote the search path and did not put it back.
fabisearchneedsNMFattached rather than loaded, and attaching it brings itsDepends(Biobase, BiocGenerics) and the foreach/doParallel/doRNG stack the engine registers: eight packages measured, where the wrapper detached onlyNMF; and the baseline was taken afterneed_pkg(), which is itself what attaches two of them.bcp::bcp()callsrequire(bcp)in its own body, so every call attachedpackage:bcpandpackage:grid. Both now restore exactly what the call added, leaving a package the user had already attached untouched. -
A detection call no longer announces someone else’s package loads.
require()speaks throughpackageStartupMessage(), so restoring the search path silently was not enough:bcp_wrapper()still printedLoading required package: bcpandLoading required package: gridon stderr. Only package startup messages are suppressed, so an engine’s ownmessage()andwarning()still reach the caller. -
inspect_wrapper()no longer prints an upstream loading diagnostic.InspectChangepoint::inspect()andsparse.svd()both callrequireNamespace("RSpectra")withoutquietly = TRUE, andRSpectrais only suggested there, so on a machine holding the engine and notRSpectraevery call wroteLoading required namespaceandFailed with error: there is no package called 'RSpectra'. The engine handles the absence itself by falling back tobase::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 stream
-
A
seedargument silently collapsed simulation studies to a sample size of one. Every one of the thirty-six sites that honoured aseeddid it withif (!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 laterrnorm()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 throughcpt_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.seedis 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 seedsays so: the seed is scoped to the call and does not pin the loop’s own stream.
Multi-annotator scoring
-
cpt_metrics_annotated()returns six fewer columns thancpt_metrics(), and@returnsaid only “a tibble with averaged metrics”. A call moved from one to the other silently losesn_truth,hausdorff,rand_index,annotation_error,mae_matchedandrmse_matched. The page now names the four it does average (precision,recall,f1,covering), says they are plain unweighted means overn_annotators, and lists what is gone. -
And why the distance metrics are gone, which is not obvious. They are
NAwhenever an annotator shares no matched pair with the prediction, so averaging them would quietly divide by fewer annotators thann_annotatorsreports. Measured on three annotators against one prediction:mae_matchedwas available for one of the three andhausdorfffor two, whilef1andcoveringwere finite for all three. The page says to score those per annotator withcpt_metrics()and combine them yourself, so the divisor is the caller’s choice, and notes thatcoveringandf1are the pair the Turing Change Point Dataset benchmark reports, which is why they are the ones averaged. - The averaging itself was verified against the per-annotator values, a bare vector is read as one annotator rather than split, degenerate annotator sets stay finite, and both shapes that could be misread as an annotator set (a
ggcpt, a data frame) are refused by name.
Influence diagnostics
-
cpt_leverage()ranked the most influential observation last.max_shift(how far each original changepoint had to move to find a match) andparam_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 outNAandorder(-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. - The
NAitself 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 itsdelta_n_cp, and?cpt_leveragenow explains both the ordering and how to read it. It also records that anNAhere always means this perturbation collapsed the fit: when the original fit found no changepoints,max_shiftis missing for every observation, the standardisation’s zero-variance guard returns zeros, and everyleverageis finite.
Supervised detection
-
coef()andpredict()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 whatcpt_penalty()andcpt_detect()consume. So a coefficient of −0.04 onlog_nis 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. -
A new “Reading the coefficients” section, because their signs usually mean nothing. A target interval is open above whenever the largest penalty on the grid still achieves the minimum label error: the common case, since a large penalty usually keeps the one changepoint the labels ask for. With every interval open above, any sufficiently large prediction is optimal, the problem does not pin the slopes, and the L2 term settles them near zero with whatever sign the optimiser reached. Measured on four series of very different length and noise: every non-intercept coefficient came out slightly negative, so the predicted penalty decreased with n: the opposite of the log n growth a reader would expect from BIC, and evidence of nothing. Every prediction was inside its target, which is the property the model is fitted for. The section says to widen
penaltiesuntil the largest one over-segments if the coefficients need to mean something.
Event annotation
-
tidy()on an events result has astatuscolumn 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, withstatusin"matched","unexplained_changepoint"and"undetected_event", and an"unexplained_changepoint"row carries a non-missingcp. Sosubset(tidy(x), !is.na(cp))returns the matched pairs and the changepoints no event explains, which silently overstates how much the events account for.@returndocumented the object’s three slots thoroughly and never said whattidy()does with them; it now gives the vocabulary, which column isNAin each row shape, and says to filter onstatusrather than onis.na(cp).
Benchmarking
-
The critical-difference diagram named Demšar and cited nobody.
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 neitherinst/REFERENCES.bibnor 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. -
The section says three things the diagram cannot. What rank 1 means and which direction each metric is ranked in; that an
NAtakes 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)withq_αthe Studentised range over√2. -
And the caveat it invites. Nemenyi is a post-hoc procedure, conventionally run only after a Friedman test rejects equal ranks;
autoplot()draws the diagram it is asked for and does not run that omnibus test. With the handful of datasetscpt_datasets()supplies N is small and CD correspondingly wide, so the page now says to read such a diagram descriptively. - The arithmetic behind all of it was checked rather than assumed: the critical distance agrees with Demšar’s Table 5 to three decimals for k = 2…10, the ranks respect each metric’s direction,
NAtakes the worst rank in both directions, ties share the average,mean_rankaverages over datasets, and an all-NAmetric returnsNULLrather than a table of ties.cpt_stability()’s frequencies are exact multiples of 1/B, sohitsreally is a count of replicates, which is what the clipping bug fixed earlier in this cycle had hidden.
Consensus
-
cpt_consensus()accepted amin_votesno location could reach, and returned an empty consensus without comment.min_votes = 3against 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). -
The count/proportion boundary falls exactly where a reader would write “unanimous”, and
?cpt_consensusnow says so. A value strictly between 0 and 1 is a proportion; anything else is a count. So with three methodsmin_votes = 0.99requires all three whilemin_votes = 1(and1.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.
Choosing K: the criteria say what they compute
-
?cpt_selectgave the formula for one of its three closed-form criteria and not the other two."mbic"was written out in full ();"bic"was “Gaussian BIC over the ladder” and"aic"“Gaussian AIC over the ladder”, which leaves thevaluecolumn 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 nandn 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 thecostcolumn. - The arithmetic was checked rather than assumed, against reference implementations written from the definitions and sharing no code with the package: all three criteria agree exactly at every rung of the ladder, the
costcolumn is the Gaussian profile cost at that rung’s own locations, exactly one row is markedchosen, and it is the argmin ofvalue. A test pins the formulas rather than recorded numbers, so a change to what a criterion means fails instead of passing. - The help page’s warning about
"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.
Cost, where it is large enough to look like a hang
-
fcovcosts minutes on a hundred-point series, and nothing said so. It is by a wide margin the most expensive engine in the package. Timed againstfmean_wrapper()on identical input (same package, same data)fcovtook 316 s at n = 60, p = 5 and 598 s at n = 120 againstfmean’s 4.5 s and 2.9 s: a factor of seventy to two hundred.?fcov_wrappernow 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.taylorandocdalready carried cost sections for the same reason; this is the third and the most extreme.
Simulation, which is the package’s own ground truth
-
cpt_simulate()dropped a surplusparamsentry silently.kchangepoints makek + 1segments, 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 firstk + 1and discarded the rest without a word. Socpt_simulate(400, changepoints = 200, params = c(0, 3, 9))returned an ordinary two-segment series and the9vanished, from a caller who had plainly meant two changepoints, and whosecpt_power()orcpt_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 everychange_inrather than only"mean". - The quantities are now tested rather than assumed, because everything downstream of
cpt_simulate()is scored against them: the realised mean jump matchesparamsto within a standard error at three sizes, the realised sd ratio matches at two,seg_idincrements at exactly the requested changepoints for three configurations,noise = "ar1"recoversrho, andnoise = "t"is materially heavier-tailed than Gaussian.
The extension mechanism
-
as_ggcpt()dropped a changepoint it could not use without saying so.ggcpt_build()discards an index that isNAor outside1..(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. Butas_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
ninvalid under"left"and1invalid 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 throughas_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.
Reproducibility under a parallel plan
-
cpt_power()’s seeded answer depends on thefuture::plan(), and?cpt_powernow 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 ’s L’Ecuyer streams (derived fromseed), and sequentially from the calling streamseedset. Both are deterministic and they are not the same numbers: measured, one and the sameseed = 11gavepower = 0, 1sequentially and0.125, 0.875on 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, whichmc_sequantifies. - The other six that dispatch on the plan (
cpt_benchmark(),cpt_batch(),cpt_consensus(),cpt_influence(),cpt_sensitivity()andggcpt_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.
Inference
-
cpt_confint(method = "auto")could answer at a different level than the one asked for, silently. Annspresult carries significance regions, so"auto"resolves to"native"and reports them at the level the engine already used:nsp_wrapper()’salpha = 0.1, i.e. 0.9, socpt_confint(res, level = 0.95)returned 90% regions. Thelevelcolumn said 0.9 throughout, which is honest but only if you inspect it, and?cpt_confintdocumentedlevelas ignored by"native"without noting that"auto"lands there whenever the engine supplied an interval. Supplying alevelthe answer does not carry now warns and names the route it took, the waycpt_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. - The bound invariants are now tested rather than assumed: across every provenance and every installed univariate engine,
ci_lower <= cp <= ci_upper, both bounds inside1..(n - 1), one row per changepoint, and a non-emptysource. Fifty-nine method/route pairs, no violations.
cpt_metrics() says what its twelve numbers mean
-
The help page listed twelve column names and explained three of them.
?cpt_metricsnow describes each, with the direction that is better (the same directionscpt_benchmark()ranks by) because a benchmark table of twelve unlabelled columns is not readable otherwise. -
annotation_erroris a count difference and nothing else, and that is now said where it can be seen. It isabs(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 givesannotation_error = 0alongsidehausdorff = 85andf1 = 0. The page now says to read it beside a location metric, never alone. -
The degenerate cases are documented, including the one place the row mixes conventions. With one side empty,
precision,recall,f1andrand_indexare0rather thanNA; buthausdorff,mae_matchedandrmse_matchedareNA, because they are distances with no pair to measure. So an all-wrong answer returnsf1 = 0andmae_matched = NAin the same row, andcpt_benchmark()ranks theNAlast rather than dropping it. Only the both-empty case was documented before. - The numbers themselves were checked rather than assumed:
covering,hausdorff,rand_indexand 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. ThefindInterval()fast path incalc_covering()is a performance change to a formula, so a test now pins it to the definition.
Testing
-
The tests CRAN runs now fit its time budget. The suite took 587s of win-builder’s 17-minute check of the first 0.5.0 submission, against a budget of about ten minutes for the whole check. Every test that took 0.2s or more under CRAN conditions, 201 of them, now starts with
skip_on_cran(), and so does every test that runsstepR: its Monte Carlo critical values are recomputed in every check, because R.cache keeps out of the user’s cache underR CMD check, and oncetcltkis loaded a fresh simulation costs 32s instead of 5s. The test step ofR CMD checkwent 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 whereverNOT_CRANis"true": on every CI runner, on every push, and underdevtools::test(). -
Eleven vignette chunks generated their data without seeding it, so their rendered output was a function of how much randomness every chunk above them happened to consume, and several of those chunks are conditional on an engine being installed, which means the vignette’s numbers and figures already differed between machines with different optional packages. Scoping the
seedargument (above) changed them again. Each of the eleven now seeds itself, invignettes/ggchangepoint.Rmd,vignettes/monitoring.Rmdandvignettes/supervised.Rmd, so a rebuilt vignette is a function of the vignette. No prose claim depended on the old values. -
Four visual snapshots were only reproducible by accident. The visual-regression file seeds once at the top and draws its shared series there, but three blocks generate their own data with a bare
rnorm(), so those snapshots were a function of test execution order, stable only because every block above them consumed randomness deterministically. Scoping theseedargument (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. -
The suite’s Suggests-guard check could name the wrong test. It locates each
test_that()block, and it used to end one at the line before the nexttest_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.
Printed output
-
Four
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, becauseChangepoints found:is longer than the pad the other five lines use;print.ggcpt_delay()had five of six lines right andAverage run length:one column out; andprint.ggcpt_path()leftDistinct segmentations:unpadded entirely.print.ggcpt()andprint.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. - Header lines no longer end in a space.
cat(" Label: ", value, "\n")puts the separator between the value and the newline, which left a trailing space on 5 of 13 lines of aggcptand 7 of a summary. (The tibble printed below the header pads its own columns; that is tibble’s output and is left alone.)
Documentation that had drifted from the code
- The engine lists in
autoplot(show_ci =),autoplot(show_fit =),augment.ggcpt()andcpt_confint(method = "native")were each short by three to four engines. They now name exactly the methodscpt_methods()marks in itsciandfittedcolumns, and say whynspis marked for uncertainty while being drawn byshow_regionsrather thanshow_ci. -
?cpt_detectdocuments the six method/change_inpairs 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, andwbsts’s"mean". The routing was never silent (the result’schange_inrecords 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_detectnames two more engines whose own signature ends in..., so a misspelt argument is discarded upstream rather than reported:fChangeandbfast. Every other wired method rejects an unknown argument by name: checked, rather than asserted. -
scale_spaceis not the capability column the docs said it was, on three help pages.?cpt_methods,autoplot(type =)and?cpt_scale_spaceall grouped it withstatisticandpathas an engine internal whose accessor errors without it, so a reader with apeltfit 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 apeltresult as readily as for amosumone. What the column marks is the two engines the sweep can be run with: the domain of that function’s ownmethodargument.statisticandpathdo gate their accessors, and still do, with the list of supporting engines in the message. -
cpt_methods()documents thatonlinedescribes the algorithm and not whatcpt_monitor()accepts. The two sets overlap without coinciding:bocpdis an online algorithm the monitor does not offer, andedetectoris native to this package and has no row in the table. Also documented: whatunivariate = FALSEmeans (a high-dimensional method, which is whatcpt_recommend()filters on: nine of the fourteen do error on one column, five run and would still be poor advice), whatci = TRUEcovers fornsp, and thatposterior = TRUEdoes not implyggcpt_posterior()can draw it. - Eight wrappers had no reference at all (
fpop,wbs,wbs2,not,mosum,idetect,tguh) andfcov_wrapper()credited the wrong paper:fChange’s package citation rather than the covariance-change method it wraps. - The package help page’s bold headings rendered as literal
**: 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 inmc_reps(the part worth planning around) separated from the absolute numbers. -
?taylor_wrappergains a section on series length, because the engine runs where R cannot look: asetTimeLimit()of 45 s was not honoured after 170, and Ctrl-C will not stop it either (R checks both at the same points).n_bootstrapsis the knob, and the cost is roughly linear in it. -
?wbs2_wrappergains 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.seedon 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,wbs2on one configuration was the only case, andtguh(same package) was stable throughout. -
?network_wrapperdocuments that the series the result carries (and so the oneautoplot()draws) is the mean edge weight per time point, not a coordinate. It is the one multivariate method with nodata_wideslot, because a p×p network has p² entries per time point. - The
fChangewrappers’ 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. - Seven help topics could not be reached from any other help page;
@seealsolinks now connect them.print.ggcpt(),is_ggcpt(),new_ggcpt()andalarms()gained a description distinct from their title (roxygen had been copying the title into\description) andprint.ggcpt()documents its return value, which no\valuesection had stated. - Comments that justified a workaround with a claim that is no longer true were corrected rather than left standing:
plotly::ggplotly()now survives a facet column namedvariable(the name stayscoordinatebecause that is what the column holds);EnvCpt’sarima()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 ofNULL; andocd’s single-column failure depends onthresh = "MC", which is why the shape is checked in the wrapper rather than left to the engine.
Fixes from the external pre-CRAN review
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_inwas inherited from a result object by nobody. Every entry point that takes raw data forwardschange_intocpt_detect(); every one that takes a finishedggcptreadobject$methodand leftchange_inat its own default of"mean". So avarormeanvarfit was silently re-detected as a change in the mean byconfint_bootstrap(),influence_recompute(),cpt_select()andcpt_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, andcpt_sensitivity()’s answer now moves with the penalty. A value passed through...still wins over the inherited one. -
cpt_load_tcpd()substitutedNAafterunlist(), by which point noNULLs remained (unlist(list(1, 2, NULL, 4))has length 3) so a JSONnulldid not becomeNA, it vanished, shifting every later observation down one and invalidating the human annotationscpt_benchmark()scores against. The Turing Change Point Dataset ships series with missing values. One statement out of order. -
augment()subtracted two different series..residwas computed againstX[, 1]for the multivariate engines whoseparam_estimateis a row mean, so.residdid not equalvalue - .fittedin the frame it was returned in. It is now computed against the seriesparam_estimatecame from in every case, and@detailsno longer claims the coordinate-one convention universally. -
A
ts’s seasonal frequency never reachedbfast.as_cpt_series()reduces every input to a bare numeric vector, which threw away the one thing BFAST cannot guess, and which?bfast_wrappertells the user to supply by passing atsfor exactly that reason. Socpt_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 explicitfrequencystill wins.
The rest.
-
cpt_monitor(method = "cpm")bypassed every guardcpm_wrapper()has. It built its model straight fromcpm::makeChangePointModel(), so a withheldcpm_type, a missing FETlambdaand an off-gridarl0each 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 subscriptsfor the third. The three checks now live in one place and both doors use them. -
The e-detector went permanently silent on overflow.
R <- (1 + R) * incgrows multiplicatively, so underreset = FALSEwithrelearn = 0(a configuration the arguments explicitly offer) it passes.Machine$double.xmaxa few hundred observations after a real change and the next product isInf. 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 (unlikeInf, which is absorbing) still decays when the stream returns to its baseline. -
The seed was not scoped on the parallel path.
future.applydocuments that for everyfuture.seedvalue exceptFALSE/NULLthe caller’s RNG state is forwarded one step, and it is: measured,future_lapply()leaves a different.Random.seedbehind for bothfuture.seed = 7andfuture.seed = TRUE.cpt_batch()andggcpt_compare()registered the restore handler only inside their sequential branch, so underplan(multisession)they broke the promise@param seedmakes verbatim. Both now register it above the branch, as the other three call sites already did. -
NSP regions that share a midpoint lost their changepoint row and kept their region. Nested intervals are the normal output of the narrowest- significance construction, and two can round to one midpoint;
ggcpt_build()dedups and range-filterscp,normalise_regions()does neither.print()then reported one fewer changepoint than the regions table below it andcpt_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 orundetected_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())isNULL,NULL$n_pred[1]isNULL, andtibble()drops aNULLargument, so the caller got a one-row tibble with then_predcolumn missing, fourNAmetrics 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 isNAat every rung with no changepoints, so an all-NAcurve makeswhich.max()returninteger(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") andpatchwork(in"reporting") appear nowhere else in the package (not inSuggests, not inR/, not in the tests or vignettes) and"reporting"installed seven packages where@param bundledocumented six. Both dropped; a test now asserts every bundled extra is declared inSuggests. - Six of the seven
import()directives were dead weight and are gone (@importFromwas already in place for everything actually called). The seventh,import(changepoint), is load-bearing and stays:glance()’s cost column calls barelogLik(fit), and the method for classcptis 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. - The declared
ggplot2floor moves from 3.4.0 to 3.5.0:discrete_scale()is called withoutscale_nameat three sites, and that argument only became optional in 3.5.0, soggcpt_compare(layout = "overlay")would have failed on the declared minimum. -
Depends: R (>= 4.0.0)added: fourteenS3method(base::plot, ...)entries cannot resolve before R 4.0.0, whereplotmoved to base. -
?cpt_delaypromised thatcpt_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. - And the one the review only half-named: inheriting
change_inincpt_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_innow says so and points at"cv"and"stability", which score by re-detection and carry no such assumption.
The rest of the external review
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 guardcpm_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 reportedPenalty: MBICwhateverbetathe call actually used. A numeric penalty is now forwarded asbeta, 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 whatprint()andglance()report. Measured on a four-segment series: 6 changepoints atpenalty = 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 unfilteredCUSUMvalanddepth, so oneNAfrom the engine misaligned both: silently, because the lengths still matched. -
The Chow F at an estimated break was flagged
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. NowFALSE, and the method string says “unadjusted”.segmented’s Davies test staysTRUE(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. -
All-failed datasets narrowed the Nemenyi critical distance. A dataset every method errored on ties them at the same worst rank (no information) and still incremented N, and
CDshrinks 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_datasetsnow counts the datasets that carry a score;n_datasets_totalrecords the rest. -
The segneigh solution path was missing candidates. Segment Neighbourhood re-solves its dynamic program at each K, so consecutive rows of
cpts.full()are not nested and can differ by more than one changepoint; keeping only the first dropped the rest and mis-numberedstep. -
A
ts’s frequency, and three labels the registry refused.not(contrast = "pcwsConstMeanVar"), astrucchangeformula fit andnsp(variant = "tvreg")each produced achange_inthatvalidate_method_change_in()would reject, so a result existed thatcpt_detect()could never be asked for. The registry now lists them, and thetvregvariant 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. Astrucchangeformula fit keeps neither the formula nordata, 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 giveargument is of length zero; it now says it could not score the ladder.cpt_label_error()with an unrecognisedchangevalue gavereplacement has length zero; the vocabulary is checked at both doors now.cpt_unregister_method(42)gave base R’sinvalid first argument.print(rec, top = -1)reachedseq_len(-1). A registeredsolution_pathwithout acpcolumn died inside the plot atidx_vals[path$cp]; it now goes through the same filtering, step-numbering andselectedcomputation as a built-in path. -
cpt_metrics_annotated()accepted an empty list (returning a malformed one-row tibble with then_predcolumn missing) and a data frame, and a data frame is a list, socpt_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")isNA, 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 neitherknoralpha:qtukey()is undefined belownmeans = 2and answers withNaN, which drew a diagram with aNaNrectangle 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 fixannotationswhen the diagnosis was in theerrorcolumn. -
cpt_label_error()scored every series’ labels against one fit when given the multi-series label setcpt_learn_penalty()takes; it warns now.cpt_learn_penalty()passed series with an unbounded target interval into the fit, where thepenaltyLearningpath 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-lengthfittedsignal without a word, after whichautoplot(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 anydownload.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 thatsample(x, n)meanssample.int(x, n)whenxis a single number ≥ 1, so a one-observation segment would resample1:round(resid). It was safe only because a length-1 segment’s residual against its own mean is exactly 0. All three now index withsample.int(). -
cpt_influence()/cpt_sensitivity()hard-codedfuture.seed = TRUEwhere the other three parallel call sites passseed %||% TRUE; reproducible today only because alocal_seed()call happens to precede them. -
ifelse(cp >= i, cp + 1L, cp)in the influence loop returnslogical(0)on an empty fit, mixing types in the list of segmentations. -
cpt_methods()’sifelse()evaluated both arms, sofind.package()ran for every planned and registered row (including a registration made withengine = NULL) and the answers were then overwritten withNA. -
cpt_register_method()validated capability flag names and not their values, socapabilities = list(ci = 1)registered a method reportingci = FALSEandcpt_confint()then told the user their own engine supplies no intervals. A non-stringcitationreachedcpt_cite()’scat().glance()’s duck-typed cost lookup could read a whole data frame column. -
cpt_regions()’s empty return was always three columns while its@returnpromises the index columns too, sorbind(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 largestnand then silently clamped to 48 in then = 50scenario. -
cpt_monitor()validated 3 of its 10 arguments.deltas = 0makes every likelihood ratio exactly 1, so the statistic grows on nothing and the monitor alarms att = 1/alphaon pure noise; and...reached the engine in two branches and vanished in the third. -
cpm_wrapper()’s siblingpilliat_wrapper()left one of its four threshold arguments unvalidated, under a comment asserting the set was complete. -
npmojo’sthreshold.valwas read with a bare$, where the same file argues twice for exact[[becausethresholdis the rule andthreshold.valthe number. -
cpt_simulate(n = 2)returned a tibble every consumer in the package then refuses. -
cpt_update()grew$dataone element at a time inside its loop, socpt_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()’sfalse_positive_rateis a count of extra changepoints, not a rate: on a scale-mismatched run it reads 137. Renamedfalse_positives. - The solution path’s
contrastcolumn carries a penalty value forbinseg/segneigh,|CUSUM|forwbs,|max.contrast|fornotand ’s own criterion forwbs2/tguh, all under one legend reading “Contrast”. The legend now names the quantity, and@returnsays the values are not comparable across engines. - The Zhang-Siegmund penalty was stated on the deviance scale in
?cpt_selectand 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_estimateis the segment mean for every method, including the variance and distribution detectors, and nothing said so, nor thataugment()’s.fitted/.resid,cpt_gt()’s level columns and the bootstrap’s residuals all inherit that convention. - The four functional/network engines reduce multivariate input to the cross-sectional mean, and that is the series
autoplot()draws. Forfcovthis 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. -
decafsandcpopreported their own internal default of2 log nas a user-supplied"Manual"penalty, so the object gave no way to tell it from the dispatcher’s stronger MBIC default. -
ocd_wrapper()’sdeclared_atcolumn is an exact copy ofcp, and@returnpresented it as additive: declares a change without estimating where it began, so there is nothing else for it to hold. Comparecpm_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 anindex, so atsis plotted in positions. Both now stated. -
cpt_simulate(change_in = "slope")restarts the time origin in every segment, solist(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 totype = "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, becauseinstalledisNAby design for registered methods andisTRUE(NA)isFALSE. -
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_cidocumentedgeom_errorbarh,?stat_changepointoffered"rug"(which consumesx/y, notxintercept),?is_ggcptsaid aggcptsubclass returnsFALSEwhen the one real subclass returnsTRUE,?cpt_install_engineslisted 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. - Two author names were ASCII-transliterated in
cpt_cite()’s table while spelled correctly in the roxygen prose; they now use\uxxxxescapes. -
geom_cpt_event()extractedcolourandlinetypefrom 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, whilealphaandlinewidthworked. -
autoplot.ggcpt_label_curve()’s band guard wasall(is.finite(tg)), which is vacuouslyTRUEon 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()calledautoplot.ggcpt()directly, bypassing the one class that both inheritsggcptand has its own method: the hazardplot_via_autoplot()was written to avoid, with a comment naming it, fixed in fourteen places and left standing in the fifteenth. - A short engine statistic was padded left, and a moving window trims both ends, so the pad shifted every value by the bandwidth, which is the mis-alignment the branch exists to prevent. Centred now, and the convention is written in
?cpt_register_methodalong with thesolution_pathcontract. -
cpt_install_engines()offeredtsboxandpatchwork, which appear nowhere else in the package. - A wide multivariate
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 quotedpower = 0, 1sequentially against0.125, 0.875on two workers for “two scenarios atn_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.
-
The scale-sensitivity counts in
?cpt_detect, the README and the introduction vignette were wrong and unreproducible. All three quoted “peltreturns 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 given, 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. - Two other documented measurements were re-run and are accurate:
?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). -
The
Install JAGS (Linux)CI step now cannot be taken down by an unrelated repository.apt-get updateexits 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 thejagsinstall 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.
- Earlier rounds swept the arguments of all 64 wrapper argument slots. Nothing had swept the data. A 30-method by 10-shape sweep (constant, two-valued, three observations, one
NA, oneInf, allNA, 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:-
snfails 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 predictgrid_size. -
buishandandsnhtboth standardise by the series’ own standard deviation, so a constant series divides by zero and the statistic reaches ’s Fortran routine asNaN:NA/NaN/Inf in foreign function call (arg 1). Both now say what is undefined and point attest = "pettitt", which is rank-based and (measured) runs on a constant series and reports no changepoint.
-
- The sweep is now a test, trimmed to the four shapes that discriminated so it runs every time, and the three messages are pinned separately: a future refactor could keep the sweep green by refusing every degenerate input with one generic complaint, which would lose the part that makes them useful.
- That test then found a fourth, on Windows and macOS but not on Linux:
bfastdecomposes a series into trend and season and a constant series has neither, so its iteration answered withmissing value where TRUE/FALSE neededfrom inside the optimiser. It now reports no breakpoints, the waysnalready did for a column that never moves, and the empty result is a usable one, withaugment(),glance()andautoplot()all working on it. - And a fifth, on macOS only:
wbstsdecomposes by wavelet scale, and a constant series has no spectrum. It now reports no changepoints for a flat series, likebfastandsn, and diagnoses the flat-stretch case by naming the longest run of identical values rather than letting the engine’smissing value where TRUE/FALSE neededthrough. -
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. - A related claim in the shared
fmean/fcovhelper 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 (fcovfails at two columns,fmeanreturns a fit at two as well) which is why the shared guard cannot be raised without refusing gridsfmeanhandles. -
The whole S3 surface, against R’s own conventions. 77 registered methods (19
print, 14tidy, 14plot, 14autoplot, 6[, 2glance, 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 forgetsinvisible(x)double-prints at the top level),glance()must be exactly one row andtidy()a tibble,[must keep the subclass when every required column survives and drop it when one does not, andautoplot()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 laterprint()fail. -
So does the search-path contract. Two wrappers attach packages the caller did not ask for:
bcpbecauserequire()inside the engine puts it and on the search path, andfabisearchbecause 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 throughon.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,bcprestores after both a successful and a failing call, andfabisearchrestores after a failure that happens after the attach. Another negative result, now a test. -
The
seedcontract 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 throughon.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.seedis still left without one after an error. A negative result, now a test. -
A failed TCPD refresh destroyed the cache it was refreshing.
download.file()opens its destination for writing before it knows whether the transfer will work, sotcpd_download()writing straight to the cache path truncated the cached file, and its ownunlink()then removed the remains. Measured: a 72-byte cachednile.jsonplus one unreachable URL left no file at all, so a singlecpt_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. -
A flaky vignette build, and the flake is a socket port. Running every Rd example three times in fresh processes found nothing: 118 of 118 pass, three times over. Running
R CMD buildtwice in parallel killed one of them:kcpRS::kcpRS()opens a PSOCK cluster unconditionally (kcpRS.default()callsmakeCluster(ncpu)wheneverncpu <= detectCores(), so no value ofncpu, not even1, avoids it), andbase::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, sincemakeCluster()picks a fresh port each time; a genuine engine or argument failure is still raised on the first attempt. Both parallel builds now complete. -
The fourth channel is clean, and looking at it found a latent CI flake. Engines can also just print, and this codebase knows that hazard: several wrappers wrap the engine in
capture.output(), and one of the review’s findings was a printed errortryCatch()never saw. Swept every engine and verb on clean input for stray stdout: all silent. What the sweep did turn up is ’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.samplesandmcmc.burninall gave a finite fit on seeds that had just failed). - The consequence mattered more than the note. The
beastexample, 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’shas_rbeastgate is no longer “is it installed” but “does it fit here”, tried once. -
And a leaked
message(), which is the easiest of the three to miss. Swept every engine and verb on a clean series: exactly one is not silent.bfastpulls in , which overwrites ’s S3 methods, so R printed a table of method names on stderr on everycpt_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: runningbfastfirst does not change whatstrucchangeanswers: the same changepoint and the same interval before and after, because is loaded but never attached. -
A leaked engine warning nobody had looked for. Errors stop; warnings do not, so a warning that leaks out of base R or an engine reaches the user as noise and nothing fails. The same provenance test applied to warnings (this package raises every one with a
NULLcall) found eleven leaks across roughly a thousand cells, and triage kept ten of them: ’s “increase Q” (the answer is censored, andQis an argument here), its SegNeigh cost advice, reporting that it adapted its lag, and ’s perfect-fit note. The one that had to go is ’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 formatbinsegrcpp_wrapper()does not accept (xis a numeric vector), andset=subtrainnames an internal split the caller never sees. Muffled by message, so everything else the engine says still gets through. -
The sweep test now checks provenance rather than phrasing. Every error this package raises uses
call. = FALSE, soconditionCall()isNULLfor a deliberate refusal and non-NULLfor 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 classifiedcpt_learn_penalty()’smissing values and NaN's not allowed if 'na.rm' is FALSEas deliberate and would have passed over it. A companion test asserts thecall. = FALSEconvention that makes the discriminator valid, since one barestop()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. - The higher-level verbs each normalise their own input, so
cpt_detect()being well-guarded says nothing about them, andcpt_batch,ggcpt_compare,cpt_simulateandcpt_label_errorare 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 withas.numeric()and nothing else, so anNAreachedcpt_features()and failed insidestats::mad()with base R’smissing 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 waycpt_batch()does, andpredict()’s bare-vector branch, which bypassed the shared coercion entirely, is guarded at the same standard. - The accessor face is clean too: 16 accessors against 9 degenerate result shapes (a changepoint at position 1, two adjacent ones leaving a single-observation segment, 58 changepoints on 60 points, a hand-built result with no fit) gave 124 ran, 20 named refusals and zero base-R errors in 144 cells. The nine value invariants that matter more than the absence of a crash (segments tile
1..nexactly,.residis a residual,param_estimateis 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. - The multivariate half of the sweep came back clean: 15 engines against 10 degenerate matrix shapes (a constant column, a duplicated column, a collinear column, p > n, a single column handed to a high-dimensional method, six observations) and zero base-R errors in 150 cells. Every refusal names the method or the argument.
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"onpeltcovered 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"onbcp1.000 at width 157. Every route is conservative; none under-covers.?cpt_confintnow carries the table, because “conservative” is the useful thing to know about an interval and nothing said it. -
The posterior route’s width is the engine’s noise floor, and now says so. Both supplying engines put roughly two-thirds of a window’s posterior changepoint mass at the estimate and spread the rest thinly over every other position, so
levelbehaves 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 thatcpt_confint()warns when an interval covers more than half its window and names the mass at the estimate, and?cpt_confintexplains 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.
-
Five Rd examples were over CRAN’s 5-second budget, and
--as-cranruns\donttest{}blocks, so being wrapped in one exempted none of them.ocd_wrapper10.4s → 4.2s (its Monte Carlo threshold calibration is nearly all of the cost and is linear inmc_reps, so the example uses 2),fmean_wrapper6.7s → 2.9s andfcov_wrapper6.1s → 2.8s (10 curves andM = 50rather than 20 and 200),cpt_min_detectable5.3s → 1.5s.fabisearch_wrapperwent from 27s to 5-6s, which is its floor:n_reps = 1fails inside fabisearch (its permutation test needs two) and a smaller matrix is not reliably cheaper, because the search then evaluates more splits relative tomin_dist. - That example’s own comment claimed its settings were “chosen to keep the example inside a check budget”. At 27 seconds it was not, and the comment now carries the measured number instead of an assurance.
- The review guessed the wrong topics here: it named
?ggcpt_plot_methods(1.3s) and the sharedcpt_influence/cpt_leveragepage 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.
-
Every README figure was named
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 nofig.althave one, which makes it 23 of 23. -
README.md had been stale for seven commits. It was last rendered before the
seedargument 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 aprint()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. -
The eight README chunks that generate their own data now seed themselves, so an example’s output depends on that example and not on how much of the stream the twelve chunks above it happened to consume. The
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.
- Six of the seven
import()directives were dead weight;import(changepoint)is load-bearing forglance()’slogLik()call on an S4 method. -
changepoints::CV.search.DP.VAR1()’scpt_hatis a matrix-list with the same column-major linearisation astest_error, socpt_hat[[which.min(unlist(test_error))]]indexes the intended cell. The reported risk of a nested sublist does not exist. - A row subset of a
ggcpt_benchmarkorcpt_label_errordoes carry its attributes through, so neitherprint()degrades; the%||%guards added are belt-and-braces and there are now tests that would notice. - No wrapper produces facets:
network_wrapper()deliberately carries nodata_wideand says so, andhdcovtakes an matrix. Thirty panels build in half a second, so the cost is readability rather than time.
Fixes found sweeping the extension mechanism and the metric layer
-
cpt_detect()on a registered method now reports what the contract had to change about the detector’s output.?cpt_register_methodpromises a registration gets “the same contract checks as every built-in wrapper”, and it did – but silently: a registration returningc(30, NA, 60)gave two changepoints and no warning, whereas_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. - An error that leaks out of a registered function is re-raised with the method name attached, so
sqrt("not a number")inside someone’s detector no longer arrives as a barenon-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@returnnamed 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:indexandvaluefor a univariate result,indexplus one column per coordinate for a multivariate one, and an extrafittedcolumn for the engines that supply their own fitted signal. -
cpt_monitor()’s whole@returnwas “Aggcpt_monitorobject.” It now describesalarms,t/offset,dataand the settings it fixes, says thatstateis engine internals rather than interface, and points atalarms()for the alarm columns.cpt_update()’s adds the thing a reader needs most: the monitor is a value, so an unassignedcpt_update(mon, y)is discarded. -
cpt_detect()’s@returnwas “Aggcptobject.” The structure was documented – onnew_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()andnew_ggcpt()cross-reference rather than repeat. -
tidy.cpt_labels()was registered, exported and aliased on no help page: its roxygen block carried@noRdand@rdname cpt_labels, and the@noRdwon.?cpt_labelsnow documents it, like the other 13tidymethods.cpt_batch()’s@returngained the columns itstidy()gives. -
cpt_test()now reportscp_indexwhen the result carries a time index, ascpt_confint()already did. A user with daily dates got2020-03-30from one and90from 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 tomosum::mosum()orCptNonPar::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 addingindex = datesdid not, becausemosumhas noindexargument. 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 saidIntervalRegressionCV()“failed on this training set”. Measured on four series whose every label is a"change", the engine’s actual complaint istarget.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. - Measured and not changed:
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 inmargin, the [0, 1] ranges) hold everywhere.cpt_metrics_annotated()is the plain unweighted mean it claims to be. - Measured and not changed: the
print()andsummary()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.
Fixes from the pre-CRAN bug hunt
-
Every tool that re-runs a finished fit now re-runs the same model.
cpt_confint(method = "bootstrap"),cpt_influence(),cpt_leverage(),cpt_sensitivity()andcpt_select()rebuilt the request from$methodand$change_inalone. The fit’s penalty was dropped, so apenalty = 40fit was bootstrapped under MBIC. Achange_inthe result records butcpt_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. -
A multivariate result is refused by the tools that re-fit one series. They re-ran the detector on
$data$value, the first coordinate: with the change in column 3 of anecpfit, the bootstrap interval had zero width and every observation looked maximally influential.cpt_test()tested coordinate one the same way. Each now says so, andcpt_stability()no longer calls a multivariate method “univariate”. -
cpt_test()on a regression-modestrucchangefit 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()andcpt_leverage()errored on everypelt,binseg,segneighoramocfit withchange_in = "var"or"meanvar":engine = "auto"sent them tochangepoint.influence, which supportscpt.mean()models only. Under"auto"an outlier diagnostic also ignoredoutlier_sdwhenever 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 thatcpt_detect()would refuse, instead of returning a K = 0 “selection” labelled with a typo, andmethod = "fpop", change_in = "var"no longer builds a variance ladder under the fpop label. Its"stability"criterion re-detected withchange_in = "mean"whatever was asked, though?cpt_selectrecommended 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 whichcpt_learn_penalty()reported that the labels were “satisfied at every penalty in the grid”. A factor member ofcpt_learn_penalty()’sseriesis refused rather than learned from as its level codes, and an emptycpt_label_error()keeps itsseriescolumn. -
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 nowNAand rank last, as documented. A matrixseries(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, andcpt_annotations()no longer mistakes a dataset namedseries_1for a single dataset. -
cpt_confint()warns when an explicitlevelmeets 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_monitordescribedtanddataas including the baseline andoffsetas a field of every monitor; it now describes the object the code builds. The"statistic"plot no longer recommendscpt_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”), convertscialong withcpundercp_convention = "right"(the interval sat one position off its changepoint), and keeps a usable region when another has a missing bound instead of failing. - The geoms let a mapped aesthetic win over their fixed styling:
geom_cpt_region(aes(fill = )),geom_cpt_label(aes(colour = ))and the labels ofgeom_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 aDateindex) instead of dropping it from all three outcomes. -
cpt_batch()refuses anindexlist that skips a series or mixes index types (either madetidy()andautoplot()fail with a base-R rbind error), and reads aPOSIXltindex as one index rather than as a list of its components. -
cpt_consensus()rounds a fractional count up:min_votes = 2.5resolved to a threshold of 2. -
cpt_power(change_in = "meanvar")simulated a pure mean change; it now moves the standard deviation too, the designcpt_scenarios()uses, and?cpt_powerstates the design for every change type.cpt_scenarios()crossesrhowith the other arguments instead of recycling it down the rows. -
?cpt_simulatehad 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()convertedmcp’s continuous change location withround(), butmcpstarts a segment atx >= cp, so the last observation before the change isceiling(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. -
envcpton a constant series returns no changepoints without fitting twelve exact models, which leakedsummary.lm()’s “essentially perfect fit” warnings: the only two warnings the test suite raised. - The multivariate
autoplot()warns aboutlabelsand unknown arguments it cannot use, as the univariate path does.ggcpt_posterior(),ggcpt_runlength()and thecpt_select()ladder draw on the result’s time index, andcpt_influence()’s overview subtitle no longer carries the line breaks and indentation of its source. -
R CMD checkis clean of package-side findings again:?cpt_labelsdocuments the arguments of thetidy()method it now lists, two “no visible binding” NOTEs are gone (one of them was the parallelseedabove), and the “Imports field not imported from: ‘Rdpack’” NOTE is resolved with Rdpack’s ownimportFrom(Rdpack, reprompt). - The message for a many-panel multivariate plot recommended
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 reports2cfor a change after itsc-th transition, whose last observation is2c + 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. -
esacandpilliatdropped a constant input column from the result as well as from the engine’s input: it vanished from$data_wide(so fromaugment()andautoplot()), and$data$valuemoved onto the next column. They now describe the input they were given, asinspect,kcpandnpmojodo. -
hdcov’s minimum spacing compared each candidate with its predecessor whether or not that one was kept, so 10, 14, 18 atdelta = 5kept only 10 although 18 is 8 from it. It is now measured from the last changepoint kept. -
fabisearchwarned, 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 a1 / n_repsfloor; 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_wrapperdescribes the test.n_reps = 1is 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 malformedpenaltyby name.NAor a misspelt name ("mbic") silently became thefpop,cpop,decafsorfastcpddefault, 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 own2 * log(n)default as a"Manual"penalty, ascpopanddecafshad until 0.5.0. -
wbsts_wrapper(cstar = 1e6)crashed the R session:cstaris 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_wrapperdescribescstarandlambdaas what they are (the unbalancedness parameter and the number of scales) rather than as “post-processing constants”. Alambdathat 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_level0.9 to 0.999,min_conf0.5 to 1,min_candidate_conf0.3 to 1), which it then refused under its own names (CI,min_tbl_conf), andsegmented_wrapper()refuses more breakpoints than a series can hold instead of running without returning atnpsi = 1e6. -
A
kcpcall broke parallel code for the rest of the session.kcpRS::kcpRS()registers adoParallelbackend on a cluster it then stops, so every later%dopar%failed with “invalid connection” or waited on the dead socket;fabisearchdid so from inside the test suite.kcp_wrapper()now gives the caller’s foreach registration back, and so doesfabisearch_wrapper(), whose engine registers one of its own whenn_core > 1. - Degenerate input that failed inside an engine is refused by name or answered:
inspecton 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”;hdcovon a constant matrix andnetworkon an all-zero sequence reachedthresholdBS()with a zero threshold and now report no changepoints;varon 5 rows failed with “Not a matrix.”;hdregon any series shorter than4 * delta + 4(24 at the default) failed with “replacement has length zero”; andwbstsbelow 8 observations ended in upstream’s “………Choose at least two scales………”. - The scale-sensitivity notes in
?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:geomcpruns PELT on its mapped series,decafsfloors its noise estimate at about 0.03, andbocpd’s default prior is on the data’s scale. The notes now name them.?bocpd_wrapperalso describedhazardas the hazard rate1/lambda; it islambda, the expected run length. -
cpt_penalty()checkskfor every penalty type; only MBIC did, so a negativekreturned a negative BIC, AIC, Hannan-Quinn or sSIC “penalty” that rewards changepoints.cpt_delay()checksmax_delay, which was compared straight against alarm times, andcpt_scenarios()refuses a missing or non-numericlocationorrhoinstead of generating a dataset with no change in it, labelled with a jump. - Twenty-six more engine arguments are checked by name, the ones the earlier measurement of wrapper arguments missed:
fcov’salpha(a bad value failed inside the engine and was blamed on the grid resolution, or ran and found nothing),bcp’smcmc,ocd’sbetaandtrain,strucchange’shandbreaks(breaks = 0fitted one break),mcp’siter,adaptandchains,inspect’slambdaandthreshold,mosum’s andnpmojo’sG,sn’sgrid_size,wbs’sthreshold(a string ran and found nothing),wbsts’sscales, thegamma_setandlambda_setgrids ofvarandhdreg(anNAin either ran and found nothing),fastcpd’sorder, andfabisearch’sn_runs,n_reps,n_core,alphaandrank(NMF refused a bad rank itself, but only after the call had attached NMF).
Corrections to the roadmap
-
hdbinsegis archived on CRAN again, contrary to the 0.5.0 roadmap’s note that it was back at 1.0.3.sbstherefore stays in the planned table, alongsidegfpop,robseg,FOCuSandchangeforest, and all five now read “when on CRAN”.cpt_register_method()is the supported route to any of them today.
Testing and infrastructure
- New
inst/CITATION. - The package opts into testthat edition 3 (
Config/testthat/edition: 3). The whole suite passes unchanged under it, and it is what makesannounce_snapshot_file()available, without which a plaintest_dir()deletes every visual snapshot as unused and the next run silently regenerates them. - New
vdiffrvisual-regression snapshots for everyautoplot()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. - Around 620 new expectations across seven test files (
test-050-registry.R,-index,-inference,-diagnostics,-supervised,-engines,-tools) plus 25 visual snapshots in-visual, all engine-dependent tests guarded withskip_if_not_installed(). The suite runs about 2350 assertions with the engines installed and about 1470 without them. -
stats,toolsandutilsare declared inImports; the new engines and extras are inSuggestsbehindrequireNamespace()guards, as before: 35 engines inside a 56-packageSuggestslist, and the package still checks clean with none of them installed.withrjoinsSuggests, which the tests already used, andrjags, whichmcp_wrapper()tests for because having does not imply JAGS can be reached. - Every parallel entry point is now tested under a real
future::plan(multisession), and the wrappers that the suite only ever reached throughcpt_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. - The suite is checked in two environments: the full one, and R 4.6.0 against a library holding the
Importsand none of theSuggests. The second is the only thing that exercises the no-Suggestspath the DESCRIPTION promises, and it caught a test that assertedgeom_cpt_event(repel = TRUE)builds: true only where is installed. That assertion now covers both worlds instead of one. -
tests/testthat/setup.Rsetsrgl.useNULL.fabisearchimportsrgl, 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.
Backward compatibility
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 newindexargument, 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 checksncol(), is not.cpt_methods(capabilities = FALSE)returns the 0.4.0 shape. -
cpt_detect()gainedindexandyas its fifth and sixth formal arguments, ahead of.... Named calls are unaffected. A call that passed a wrapper’s own argument positionally pastpenalty(which no example or vignette ever did, because...arguments have always had to be named to reach the right engine) would now bind it toindex.
ggchangepoint 0.4.0
CRAN release: 2026-08-24
The 0.4.0 engine wave
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 populateci_lower/ci_upperconfidence-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), withposterior_probcolumns and the posterior mean carried as a fitted signal. -
cpm_wrapper(): sequential distribution-free detection (cpm), with adetection_timecolumn. -
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.
New tools
- New
cpt_crops()computes the full CROPS penalty path and returns aggcpt_pathobject withprint(),tidy(), andautoplot(type = c("elbow", "path", "segmentations")). - New
cpt_batch()runs one detector over many series (matrix, data frame, or list) with optionalfutureparallelism; returns aggcpt_batchtibble withtidy()and a facetedautoplot(). - New
cpt_stability()bootstrap stability diagnostic: segment-preserving resampling with a detection-frequency profile andautoplot(). - New Bayesian displays:
ggcpt_posterior()(posterior mean + per-location changepoint probability) andggcpt_runlength()(the BOCPD run-length posterior heatmap). - New
ggcpt_interactive()renders any result as aplotlywidget. - New
cpt_cite()returns the verified methodological reference(s) behind a result or method name.
Visualisation
-
autoplot.ggcpt()gainsshow_ci(draws changepoint-location confidence intervals fromci_lower/ci_upper) andshow_fit(overlays the engine’s fitted signal), and renders multivariate results as faceted small-multiples. -
geom_cpt_ci()migrated off the deprecatedggplot2::geom_errorbarh()togeom_errorbar(orientation = "y"). - Unknown styling arguments passed through
autoplot()/ggcptplot()now warn instead of being silently discarded, and plotting an emptyggcpterrors cleanly instead of producing infinite axis limits.
Bug fixes (audit items C1-C20; regression-tested)
-
cpt_detect(penalty = <number>)works for the changepoint-package methods (pelt,binseg,segneigh,amoc): a numeric penalty is now translated to the engine’spenalty = "Manual", pen.value = <number>instead of erroring with “Unknown Penalty” (#2). -
binseg/segneighno longer crash on short series that pass validation; the maximum number of segmentsQis 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). - The
segmentstable’sstart/ncolumns are integer, matching the documented schema (#6). -
signal_mix()gains a minimum-nguard and filters its changepoint indices, sotrue_changepointsno longer contains 0,n, or duplicates for smalln(#7). -
autoplot()honours theindexargument for multivariate results (#8). -
cpt_batch()/ggcpt_compare()no longer crash with “factor level duplicated” when two series share a name, or themethodsvector repeats (#9). -
wbsreturns an empty result instead of erroring when a manualthresholdadmits no changepoints (#10). -
idetectreturns 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 reversedc(1, n+1)), and no longer drops genuine changepoints ine.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). - Univariate wrappers and
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 aspenalty$value(was the string “critical.value”) and implements its documentedmultiscaleargument viamosum::multiscale.localPrune()(C7, C8). -
cpt_detect()forwardschange_into 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 SICk * log(n)^alpha(was0.5 * k * log(n), weaker than BIC) (C11). - Metrics agree with the van den Burg-Williams conventions: an exactly correct empty prediction scores precision/recall/F1 = 1 (C12); empty predictions score the trivial-partition covering and chance-level ARI 0 (C13); out-of-range indices are dropped with a warning instead of crashing (C14);
ggcpt_eval()uses the same one-to-one matching ascpt_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 thexaesthetic before detecting (results were previously row-order dependent) and declaresdropped_aesso 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 matchessd(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;npresults reportchange_in = "distribution";meanvarresults stay"meanvar"in the user’s vocabulary;ggecpplot()handles multivariate input without crashing.
Bug fixes (pre-release audit; regression-tested)
- Every univariate wrapper now rejects multi-column input instead of silently flattening it column-major. The C3 fix had only reached the search-based wrappers, so
smuce_wrapper(),cpop_wrapper(),bcp_wrapper(),bocpd_wrapper(),beast_wrapper(),cpm_wrapper(),decafs_wrapper(),strucchange_wrapper(),segmented_wrapper()andenvcpt_wrapper()turned a 120x2 matrix into a 240-point series. The newcpt_crops()andcpt_stability()entry points guard the same way (R16). -
segneighno longer errors with “subscript out of bounds” on short series. TheQclamp added for #3 missed the engine’s real constraint: Segment Neighbourhood requiresQ >= 3regardless of length, so the clampedQof 1 or 2 failed for everyn < 8.Qis now clamped into the engine’s valid window (3 <= Q <= n - 2for a mean change,floor(n / 2) + 1when a variance is estimated per segment), and a series too short to admit any validQgets an actionable message naming the constraint instead of the engine’s internal error (R17). - A multivariate coordinate literally named
indexno longer crashesmv_data_wide()with “Column nameindexmust not be duplicated”; it is made unique against the position column, soecp,inspect,geomcp,ocd,npmojo,kcpandfastcpdall accept such data (R18). -
NAchangepoint indices from an engine are dropped rather than propagating intobuild_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 ofnandkalone. The computed value is unchanged; the documentation now states what it is (BIC plus a combinatorial placement term) and how it differs. - Passing a wrapper’s own argument through
cpt_detect()no longer errors with “formal argument … matched by multiple actual arguments”. The dispatcher derives some arguments fromchange_inand was passing them alongside the caller’s, so the documented...passthrough was broken fornot’scontrast,cpm’scpm_type,kcp’srunning_stat,sn’sparameter,fastcpd’sfamilyandhsmuce’sfamily. A value supplied by the caller now wins over the derived one (R20). - Two enumerated engine options that could never succeed were removed (R21):
smuce_wrapper(family = "poisson")(currentstepRaccepts no such family, so it always errored) andcpm_wrapper(cpm_type = "GLRAdjusted"), whichcpm::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 alambdavalue. -
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). - Degenerate input is handled the way the rest of the package already handled it. A constant series now returns the empty result instead of an opaque engine error (
sn,kcp,npmojo,inspect) or, forsegmented, a spurious kink recovered from a singular fit (R23). - A single constant coordinate no longer kills a multivariate run.
inspect,npmojoandkcpstandardise 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). -
kcpandsnexplain 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()andsummary()no longer render penalties at full double precision or with a placeholder value:Penalty: Manual = 17.8459510605346is nowManual = 17.846, and a penalty that carries no numeric value prints asMBICrather thanMBIC = NA.
Bug fixes (final pre-submission audit; regression-tested)
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.
-
hsmuceno 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 fromcpt_detect(x, method = "hsmuce"). Two regimes were measured as fatal: a globally flat series such asrep(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 whatcpt_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 namingfamily = "gauss", which handles the whole range. An exactly noiseless series is safe upstream and still works (R53). -
idetectno 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, …, whilerep(0, 100)errors andrep(-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 forsegmented,sn,kcp,npmojoandinspect:idetectwas 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.freqis described as “the proportion of replicates detecting a changepoint withinmarginof that index”, but the loop incremented once per changepoint, so a replicate whose detections had overlapping ±marginwindows 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, sofreqis a genuine proportion and needs no clipping (R38). -
glance()always returns the single row it documents.new_ggcpt()defaultedmethodandchange_intocharacter(0), sotibble()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 nowNA_character_, andglance()coerces the metadata fields to length one whatever the object carries (R37). - The
changepointengines (pelt,binseg,segneigh,amoc,np) keep their upstreamcptobject in$fit. It wasNULL, although$fitis documented as “the raw upstream object” and every other engine stored one, which also left theinherits(fit, "cpt")branch ofglance()unreachable, and with it a sign error and a wrong element index that had never run.glance()$total_costnow reports the unpenalised −2 log L for those engines wherechangepointexposes it on that scale, and staysNAwhere it does not, rather than mixing two scales in one column;?glance.ggcptspells out which cases are which (R35). -
ggcpt_interactive()works on multivariate results. The faceted small-multiple thatautoplot()builds for them used a facet column namedvariable, which is also the nameplotly::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 nowcoordinate; the facet strips are unchanged (R36). - Duplicate multivariate coordinate names no longer abort a run. A matrix may legally carry the same colname twice, which made
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 namedindexagainst the position column, not the coordinates against each other. All coordinate names are now made unique in one pass (R34). -
envcptno longer prints its engine’s internal failures as though the call had failed.EnvCptfits up to twelve models withtry(), and a non-silenttry()writes its error straight to stderr, so on a degenerate seriesenvcpt_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 withoutparams. 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 ofchangepointswere recorded intrue_changepointswith no actual change behind them: silently wrong ground truth for"var","meanvar"and"slope"(R32). - Two more ways to get a silent “no changepoints” are closed.
cpmships thresholds only for a fixed set of average run lengths; for any otherarl0itsprocessStream()prints “Error: No thresholds available for selected ARL0” and returns an empty result instead of raising a condition, sotryCatch()never saw it and the wrapper reported zero changepoints on a series with an obvious one: the same trap the earlier audit found forcpm_type = "GLRAdjusted", on a different argument. Andkcp_wrapper()withnpermbelow 2 either reported nothing (0 or negative) or died inside the engine with an unreadablerow.nameserror (1). Both are refused now, with the supportedarl0values named in the message (R61). - An out-of-range
conf_levelno longer hangsstrucchange_wrapper().stats::confint()on a breakpoints fit atlevel = 2never returns, and thetryCatch()already around that call cannot rescue a call that does not terminate, so the session simply locked up.conf_levelis now required to lie strictly between 0 and 1 in bothstrucchange_wrapper()andsegmented_wrapper(). In the same sweep:bocpd_wrapper(hazard)andcpop_wrapper(sd)must be positive, andwbs_wrapper(n_intervals)at least 1: all previously accepted meaningless values (R60). -
cpt_simulate()refuses parameters that made it emitNaN. It is where ground truth for every benchmark comes from, so a silent series ofNaNis the worst thing it can produce, andsd = -1,sd = NA, and|rho| >= 1under 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-positivenis refused too.rhois checked only fornoise = "ar1", so a stray value the chosen model ignores is still accepted (R59). - The logical switches refuse a non-logical value instead of silently doing the opposite.
show_segments,show_ci,show_fit,show_line,show_pointsandmosum_wrapper(multiscale)are all documented as “Logical” but were read withisTRUE(), which treats everything that is notTRUEasFALSE. Soshow_segments = 1,= "yes",= "TRUE"or= NAquietly drew nothing, andshow_line = 1quietly removed the line the user was asking to keep: three layers down to one.show_points = NULLkeeps its documented meaning of deciding from the series length (R58). - The package’s own arguments now enforce the ranges they document. The engines police their own (
stepRrefuses analphaoutside (0, 1),SNSegan unlistedconfidence) 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 ofNaN;cpt_metrics(n = -10)a covering metric of −1;bcp_wrapperandbeast_wrapperwithprob_threshold = 0reported 239 changepoints in a 240-point series;kcp_wrapper(alpha = 2)andcpt_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 everyn < 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 atn / 2, the largest the engine accepts. The documentedmax(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")refusescp_methodvalues other than"PELT"up front.changepoint.np::cpt.np()implements PELT only, so"BinSeg"and"SegNeigh"used to die on the internalQclamp with “unused argument (Q = 5)” and"AMOC"surfaced the engine’s “Invalid Method” (R27). -
autoplot(),ggcptplot()andggecpplot()reject anindexwhose length does not match the series, naming the argument at fault, instead of surfacing dplyr’s recycling error (“xmust be size 200 or 1, not 10”), which never mentionsindex(R26). -
cpt_penalty()enforces the argument ranges it documents (R28):alpha > 1for"sSIC"(at or below 1 it is weaker than BIC, so no longer a strengthened SIC);n >= 3for the log-based penalties (log(n)is 0 atn = 1andlog(log(n))is negative belown = 3, so the “penalty” rewarded extra changepoints); and0 <= k <= nfor"MBIC", whoselog C(n, k)term is-Infbeyond that."AIC", which does not involven, 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 (“xmust 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). - A result from
cpt_detect()records thecpt_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 withrefs$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, asggcptplot()already did; a zero data range would otherwise collapse them to invisible zero-height segments. -
glance()no longer carries an unreachable branch. It testedinherits(fit, "cptrange"), but thechangepointclass iscpt.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_ggcptand?ecp_wrapperexplain why$fitisNULLfor"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). - The covering metric no longer scales quadratically.
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 twofindInterval()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). - The redundant
.onLoad()is gone. It re-registeredprint,plot,summary,tidy,glance,augmentandautoplotat load time (writing intobase‘s andgenerics’ S3 method tables) even though NAMESPACE already declares every one of them, and it wrapped the lot insuppressWarnings(), so a genuine registration failure would have been invisible. It was a leftover from before@exportS3Method base::genericwas 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 throughgetS3method()(R41). -
?ocd_wrappersays how long it takes. Nearly all ofocd’s cost is Monte Carlo threshold calibration, which happens before a single observation is read: measured atmc_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 atmc_reps = 20, so the defaultmc_reps = 100extrapolates 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 atthresh, which takes the three thresholds directly and skips calibration entirely. That escape hatch had no test; it has one now (R56). -
statsis declared inImports. - The documented simulate → detect → evaluate → plot workflow is verified end to end. Each piece had its own tests, but not the chain: a result’s changepoints feeding
cpt_metrics()andggcpt_eval(), its segments feedinggeom_cpt_segment(), the object itself feedingcpt_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:amocfinds at most one changepoint,cpopandsegmentedare slope engines being shown a step,ocdis online and reports declaration times, andgeomcpunions its distance and angle mappings. A six-method version spanning the structural variety is now in the suite (R55). - Every configuration of
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 byhsmucerather than crashing it:sd = 0andsd = 1e-9for a change in mean, and (the one the audit turned up)change_in = "slope"withsd = 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). - The dispatcher’s
change_intranslations are tested.cpt_detect()derives an engine-specific argument fromchange_infornot,cpm,kcp,snandfastcpd; 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_metricsclaims 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). - The Bayesian displays’ remaining documented paths are tested:
ggcpt_posterior()on abeast_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 onggcpt_posterior()/ggcpt_runlength(): non-ggcptinput, a result with no posterior, and aprob_floorthat leaves nothing to draw (R47). - Test coverage rose to cover the exported surface that had none. A coverage run found two exported functions with no test at all (
ggcpt_compare_table()andcpt_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 thesd/breaks/model_param/lambda/threshold/Garguments of the cpop, strucchange, DeCAFS, inspect and mosum wrappers. All of them worked; none of them was guarded against a future refactor (R45). - The test suite now really does run with none of the Suggests installed. Two assertions reached a Suggests-only engine without a guard (
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 everytest_that()block for a Suggests package used without a matching guard found no others (R62). -
ggcpt_compare()andggcpt_compare_table()refuse a multi-columnxinstead of flattening it. Both run univariate detectors but tookas.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 insideas.numeric()with “cannot coerce type ‘object’ to vector of type ‘double’”. The message points atcpt_batch(), which is what runs a detector over a panel (R63). -
ggcpt_compare()handsfuture.applya documentedfuture.seedvalue. It passedseedstraight through, andseeddefaults toNULL, which is not among the logical/integer/list valuesfuture_lapply()documents, so every parallel comparison run without an explicit seed was outside that contract. It now sendsTRUEin that case, asking for parallel-safe L’Ecuyer streams, which is whatcpt_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-sequentialfuture::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_wrappersays how large its result is. Measuringobject.size()for every engine on one series turned up a single outlier: astrucchangeresult is quadratic in the series length, becausebreakpoints()keepsRSS.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 fromcpt_batch()are, so the help now says to keep$changepointsrather than the whole list of results. Nothing changed in the object: this is the same size-versus-usefulness trade-off already documented forecpin 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). - Asking for one of the four planned methods says so.
cpt_methods()listsgfpop,robust,focusandsbswithstatus = "planned", butcpt_detect(x, method = "gfpop")went tomatch.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”, buthdbinsegreturned to CRAN as 1.0.3 in September 2025, so the only thing standing betweensbsand a user is the wrapper.gfpopwas removed from CRAN androbsegandFOCuShave never been on it, so those three still read “when on CRAN” (R66).
Documentation
-
?cpt_detectgains a scale-sensitivity section, and the README and the introduction vignette repeat it:pelt,binseg,segneighandfpopweigh the penalty against a raw segment cost when detecting a change in mean, becausechangepoint’s Normal cost fixes the noise standard deviation at 1 andfpop’slambdapenalises 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,peltreturns 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 usechange_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_wrapperand?cpt_penaltypoint 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_wrapperand?decafs_wrappernow 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’ own2 * log(n)(19.9 against 11.8 at n = 360) socpt_detect(x, method = "decafs")reports 3 changepoints wheredecafs_wrapper(x)reports 5 on the same series. Both defaults were documented individually; that they differ was not. Passingpenaltyexplicitly makes the two entry points agree (R40). -
?npmojo_wrapperrecords that the engine calibrates its detection threshold by bootstrap, so the value stored in the penalty descriptor varies between runs unlessset.seed()is called first (or a manual threshold is passed through...). -
?cpt_detectwarns that a misspelt engine argument can pass unnoticed.wbs,not,Rbeast,strucchange,segmentedandfastcpdall end their own signature in..., so an unrecognised name forwarded throughcpt_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. - The README and all three vignettes were reviewed against the source and corrected. Notably: a
geom_cpt_segment()example that could not run (it was givenxintercept, but the geom needsx/xend/y/yend);DeCAFSandEnvCptfiled 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). - Figure alt text is now specific per figure instead of one generic string for every plot.
- The
ocd_wrapper()test usesmc_reps = 10rather 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. - The
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 andmc_reps) and a smaller, cleaner problem (100x3 withmc_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. - Two citations were wrong, and the package’s three citation sources now agree. The TGUH paper was dated 2018 (Annals of Statistics 46(6B), 3390-3421) by
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_wrappercited the arXiv preprint of the ecp software paper while both vignettes cited its published form, soinst/REFERENCES.bibnow 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\insertRefkey must resolve ininst/REFERENCES.bib, and every@keycited in a vignette must resolve in the vignette bibliography (R44). -
?stat_changepointsays which geoms actually work with it. The stat emits onexinterceptper changepoint and dropsx/y, so"vline"(the default) and"rug"fit while"point"errors; the help previously read as though any geom would do. -
?geom_cpt_cino longer claims anxaesthetic is required. The layer is a horizontal error bar, so it needsy,xminandxmax;xis 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.ggcptnow says what the columns mean for a multivariate result: every coordinate is returned andseg_id/is_changepointapply to the whole row, but.fittedand.residdescribe the first coordinate only: the same one$segments$param_estimatesummarises. - The penalty-semantics section of
?cpt_penaltynow records the one silent substitution the dispatcher makes:changepointdoes not implement MBIC for Segment Neighbourhood, socpt_detect(method = "segneigh")falls back to"SIC"on the default penalty and its result is therefore not directly penalty-comparable with a PELT one.
ggchangepoint 0.3.0
CRAN release: 2026-06-26
Documentation and coverage
- The README now introduces every exported function, grouped by role, and the over-claimed
gfpopengine (never wrapped) has been removed from it. - New feature-tour vignette (
vignette("ggchangepoint")) walking the full exported surface, including the per-engine wrappers,theme_ggcpt(), andannotate_segments(). - The package-level help (
?ggchangepoint) was rewritten to describe the unifiedggcptframework and the current 13-method engine list (it previously still claimed “only three changepoint packages”). - New documentation-coverage test asserting every export appears in the README.
New features
- New
cpt_methods()introspection helper returning a tibble of every known method, its engine, availability status, and whether the engine is installed. - New S3 methods for the
ggcptclass:summary(),as_tibble(),as.data.frame(),format(), andplot(). -
cpt_penalty()gained a documented per-engine penalty-semantics section.
Bug fixes
-
cpt_detect()no longer advertises 13 methods that errored at runtime;match.arg()now enumerates only the wired methods (B7). -
cpt_detect()validatesmethod×change_incombinations 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, sorecallandf1can no longer exceed 1 (B2), and no longer warns on emptypred/truth(B6). -
ecp_wrapper()returns a correct per-coordinatecp_valuefor matrix and data.frame input instead of a column-major flattened scalar (B4);cpt_detect()no longer flattens multivariate input before passing it toecp. -
stat_changepoint()maps detected indices back to thexaesthetic so rules land at the correct location on non-1:naxes (B5). -
glance.ggcpt()now reports a measuredruntimeand populatestotal_costfrom 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()@returnnow documents theseg_idcolumn it actually returns (B9), and the deadshow_segmentsparameter was removed from the internal plot helper (B10).
ggchangepoint 0.2.0
CRAN release: 2026-06-21
Major changes
- New
ggcptS3 result class withtidy(),glance(),augment(), andautoplot()methods - New
cpt_detect()unified dispatcher for changepoint methods - New geoms:
geom_changepoint(),geom_cpt_segment(),geom_cpt_ci(),stat_changepoint() - New first-wave wrappers: WBS/WBS2, NOT, MOSUM, FPOP, Isolate-Detect, TGUH
- New
ggcpt_compare()andggcpt_compare_table()for method comparison - New evaluation module:
cpt_metrics(),cpt_metrics_annotated(),ggcpt_eval() - New simulator:
cpt_simulate()/rcpt()and canonical test signals - New
cpt_penalty()helper - New
theme_ggcpt()andannotate_segments()for plot customisation
Hardening (bug fixes)
-
ecp_wrapper()no-change bug fixed: spurious boundary changepoints and NA no longer emitted -
size→linewidthmigration:cptline_linewidthreplaces deprecatedcptline_size -
match.arg()input validation added to all wrappers - Changepoint convention documented and aligned
- “sytle” typo fixed → “style” in documentation
- roxygen modernised to
"_PACKAGE"sentinel -
change_in = "np"alias added (keeps"cpt_np"for backward compatibility) - Full-height changepoint rule default;
show_pointsauto-off above 500 obs - Optional
indexparameter for time-series axes
ggchangepoint 0.1.0
CRAN release: 2022-02-24
- Initial release to CRAN.
- Exported functions:
cpt_wrapper(),ecp_wrapper(),ggcptplot(),ggecpplot().
