
Naming Conventions and the Analysis/Tidying/Plotting Pipeline
Source:vignettes/naming-and-analysis.Rmd
naming-and-analysis.RmdWhy this exists
exuber started as one test (radf(), the
recursive ADF/SADF/GSADF/BSADF statistic of Phillips, Shi & Yu 2015)
and grew, through a long research programme, into roughly 25 functions
covering a dozen papers’ worth of related-but-distinct methodology:
alternative tests, dating procedures, monitoring schemes, root
inference. Every one of them used to be named
radf_<something>(), which was accurate for some and
misleading for others – a radf_ prefix reads as “this is a
recursive-ADF statistic,” but several of these functions are not that at
all. This vignette documents the naming scheme that replaced it, and –
more usefully – which functions can actually be plugged into the shared
summary()/datestamp()/tidy()/autoplot()
pipeline built for radf(), and which have their own,
differently-shaped output instead.
The naming scheme
| Pattern | Means | Examples |
|---|---|---|
radf_ prefix |
Genuinely built on the recursive-ADF core: calls radf()
directly, or reuses its badf/bsadf
recursion |
radf(), radf_tt(),
radf_sign(), radf_common(),
radf_kp(), radf_recovery(), and the
_cv/_mc/_sb/_wb
critical-value engines |
_test suffix |
A standalone hypothesis test with its own null distribution, not built on the recursive-ADF core |
lbi_test(), ssu_test(),
quantile_test(), cobubble_test()
|
dating_ prefix |
Point-estimation / model-selection dating, no formal hypothesis test at all |
dating_hls(), dating_hlw(),
dating_knp(), dating_pdc()
|
monitor/monitor_ prefix |
Real-time/sequential detection – grouped by what it does,
not by internal mechanism. monitor() is this family’s
flagship, the same role radf() plays for the
radf_ family: it reuses
badf/bsadf directly (genuinely ADF-family
internals) but carries no radf/sadf token at
all, specifically so it reads as “the real-time monitor,” not as a
radf_ variant – see below |
monitor(), monitor_cusum(),
monitor_lbi(), monitor_quantile()
|
root_ prefix |
Confidence-interval inference on the magnitude of the explosive root, not a test for its presence |
rootstamp() (two S3 methods: default for a single
sub-sample, radf_obj to run every datestamp()
episode at once) |
| stands alone | A point-estimation tool, not a test | contagion_reg() |
Naming prefixes are a convention, not a contract – they’re easy to
misremember and, as monitor() shows, sometimes trade off
against each other (grouped with its fellow monitors under a name that
deliberately doesn’t advertise its ADF-family internals, so it can’t be
mistaken for a radf_*() variant). For anything
programmatic, don’t parse function names: call
exuber_functions(), which returns the same categorization
as actual, queryable data.
exuber_functions(family = "monitor")
#> # A tibble: 4 × 3
#> name family description
#> <chr> <chr> <chr>
#> 1 monitor adf,monitor Real-time monitoring (Family A); reuses radf()'s…
#> 2 monitor_cusum monitor CUSUM/CUSUMV real-time monitoring, closed-form b…
#> 3 monitor_lbi monitor Sequential extension of lbi_test(), constant-bou…
#> 4 monitor_quantile monitor QPWY recursive quantile-regression monitoring, e…Two names that look related but aren’t: the dating_*()
family above are standalone SSR/BIC procedures called directly on raw
data – they take no critical value at all. datestamp() (see
below) is a different thing entirely: the generic that applies PSY’s own
threshold-crossing rule to any radf_obj +
radf_cv pair.
One exception to “datestamp() always needs a
radf_cv”: datestamp(object, option = "svadf")
runs Sarkar & Wells (2026)’s asymmetric-threshold dating directly
off object$badf, no critical value at all – see
vignette("experimental-methods").
What actually plugs into
summary()/datestamp()/tidy()/autoplot()
These four generics are built around one shape: a
radf_obj (from radf()) paired with a
radf_cv that carries a time-varying
boundary (badf_cv/bsadf_cv, one critical value
per recursion point) as well as the three scalar sup-statistic critical
values (adf_cv/sadf_cv/gsadf_cv).
Only functions whose result actually carries the radf_obj
class – and whose paired _cv() actually computes that
time-varying boundary – get the full pipeline. Three tiers, in
practice:
Full support: radf_common(), radf_kp(),
radf_tt(), radf_sign(),
radf_sign_dm(), radf_sbz()
radf_kp()/radf_common() literally return
radf()’s own output (purged of volatility, or computed on a
PCA factor, respectively, then radf() unmodified), so every
generic works exactly as it does for plain radf():
res <- radf_kp(sim_data, minw = 20)
cv <- radf_mc_cv(n = attr(res, "n"), minw = 20)
summary(res, cv = cv)
#>
#> ── Summary (minw = 20, lag = 0) ────────────────── Monte Carlo (nboot = 1000) ──
#>
#> psy1 :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -0.439 -0.423 -0.152 0.677
#> 2 sadf 0.175 1.07 1.40 1.78
#> 3 gsadf 1.67 1.58 1.90 2.51
#>
#> psy2 :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -2.40 -0.423 -0.152 0.677
#> 2 sadf 0.918 1.07 1.40 1.78
#> 3 gsadf 2.03 1.58 1.90 2.51
#>
#> evans :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -1.93 -0.423 -0.152 0.677
#> 2 sadf -0.714 1.07 1.40 1.78
#> 3 gsadf 0.489 1.58 1.90 2.51
#>
#> div :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -2.32 -0.423 -0.152 0.677
#> 2 sadf 0.584 1.07 1.40 1.78
#> 3 gsadf 0.900 1.58 1.90 2.51
#>
#> blan :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -2.47 -0.423 -0.152 0.677
#> 2 sadf -1.46 1.07 1.40 1.78
#> 3 gsadf 0.452 1.58 1.90 2.51
datestamp(res, cv = cv)
#>
#> ── Datestamp (min_duration = 0) ───────────────────────────────── Monte Carlo ──
#>
#> psy2 :
#> Start Peak End Duration Signal Ongoing
#> 1 22 30 36 14 positive FALSE
#> 2 60 65 70 10 positive FALSE
tidy(res, cv = cv)
#> # A tibble: 5 × 4
#> id adf sadf gsadf
#> <fct> <dbl> <dbl> <dbl>
#> 1 psy1 -0.439 0.175 1.67
#> 2 psy2 -2.40 0.918 2.03
#> 3 evans -1.93 -0.714 0.489
#> 4 div -2.32 0.584 0.900
#> 5 blan -2.47 -1.46 0.452
autoplot(res, cv = cv)
The other three are different: they carry the radf_obj
class but build their statistic on gls_dfstat_grid() (a
no-intercept, GLS-demeaned recursive-DF grid, fed the raw series for
radf_tt(), its cumulated sign for radf_sign(),
a recursively demeaned cumulated sign for radf_sign_dm())
rather than calling radf() directly. Until 2026-08-18 all
three had a real gap: their _cv() functions only ever
computed the three scalar critical values
summary()/tidy() need, discarding the
badf/bsadf path gls_dfstat_grid()
already computes per replicate, so
datestamp()/autoplot() (which need a
time-varying boundary) always errored. Fixed for all three the
same way, once the pattern was confirmed in radf_tt_cv()
first and then checked to hold for the other two as well: unlike
radf_mc_cv()’s own bsadf_cv (a
cummax()-across-replicates shortcut around the base C++
engine’s output shape), gls_dfstat_grid()’s
bsadf is already the genuine sup-over-all-window-starts
statistic at each point, so no shortcut derivation was needed – just the
per-time-point quantile across replicates, the construction
radf_mc_cv() uses for its own bsadf_cv.
Validated per function: badf_cv’s last row is bit-identical
to adf_cv (adf is literally
badf’s last point, per replicate – a hard identity, not an
approximate check, and true regardless of which series feeds
gls_dfstat_grid()); empirical false-alarm rate under
H0 is at or below nominal (radf_tt 3.3%,
radf_sign 5.5%, radf_sign_dm 3.5%, all at
nominal 5%, n=100, minw=20); and detection power on an identical
synthetic bubble is in the same range as the established
radf()/radf_mc_cv() baseline (16%) rather than
suspiciously higher or lower (radf_tt 18%,
radf_sign 20%, radf_sign_dm 8% – the
sign-based tests trading power for their heteroskedasticity invariance
is itself the paper’s own documented finding, not a validation red
flag).
res <- radf_tt(sim_data, minw = 20)
cv <- radf_tt_cv(n = 100, minw = 20)
summary(res, cv = cv)
#>
#> ── Summary (minw = 20, lag = 0) ────────── Time-Transformed MC (nboot = 2000) ──
#>
#> psy1 :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -1.04 0.947 1.34 2.05
#> 2 sadf 1.27 2.18 2.51 3.40
#> 3 gsadf 2.20 2.81 3.15 4.04
#>
#> psy2 :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -0.860 0.947 1.34 2.05
#> 2 sadf 2.60 2.18 2.51 3.40
#> 3 gsadf 3.50 2.81 3.15 4.04
#>
#> evans :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -1.33 0.947 1.34 2.05
#> 2 sadf 1.66 2.18 2.51 3.40
#> 3 gsadf 1.88 2.81 3.15 4.04
#>
#> div :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf 0.722 0.947 1.34 2.05
#> 2 sadf 2.34 2.18 2.51 3.40
#> 3 gsadf 2.34 2.81 3.15 4.04
#>
#> blan :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -1.34 0.947 1.34 2.05
#> 2 sadf 0.500 2.18 2.51 3.40
#> 3 gsadf 1.54 2.81 3.15 4.04
datestamp(res, cv = cv)
#>
#> ── Datestamp (min_duration = 0) ───────────────────────── Time-Transformed MC ──
#>
#> psy2 :
#> Start Peak End Duration Signal Ongoing
#> 1 21 27 35 14 positive FALSE
#> 2 55 55 73 18 positive FALSE
tidy(res, cv = cv)
#> # A tibble: 5 × 4
#> id adf sadf gsadf
#> <fct> <dbl> <dbl> <dbl>
#> 1 psy1 -1.04 1.27 2.20
#> 2 psy2 -0.860 2.60 3.50
#> 3 evans -1.33 1.66 1.88
#> 4 div 0.722 2.34 2.34
#> 5 blan -1.34 0.500 1.54
autoplot(res, cv = cv)
res <- radf_sign(sim_data, minw = 20)
cv <- radf_sign_cv(n = 100, minw = 20)
summary(res, cv = cv)
#>
#> ── Summary (minw = 20, lag = 0) ──────────────── Sign-Based MC (nboot = 2000) ──
#>
#> psy1 :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -0.152 0.920 1.28 1.99
#> 2 sadf 0.937 2.26 2.62 3.39
#> 3 gsadf 2.02 2.89 3.31 4.45
#>
#> psy2 :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf 2.56 0.920 1.28 1.99
#> 2 sadf 6.42 2.26 2.62 3.39
#> 3 gsadf 14.0 2.89 3.31 4.45
#>
#> evans :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf 4.85 0.920 1.28 1.99
#> 2 sadf 5.76 2.26 2.62 3.39
#> 3 gsadf 6.85 2.89 3.31 4.45
#>
#> div :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf 1.13 0.920 1.28 1.99
#> 2 sadf 2.79 2.26 2.62 3.39
#> 3 gsadf 2.95 2.89 3.31 4.45
#>
#> blan :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf 3.38 0.920 1.28 1.99
#> 2 sadf 3.38 2.26 2.62 3.39
#> 3 gsadf 3.68 2.89 3.31 4.45
datestamp(res, cv = cv)
#>
#> ── Datestamp (min_duration = 0) ─────────────────────────────── Sign-Based MC ──
#>
#> psy2 :
#> Start Peak End Duration Signal Ongoing
#> 1 21 40 100 80 positive TRUE
#>
#> evans :
#> Start Peak End Duration Signal Ongoing
#> 1 21 84 100 80 positive TRUE
#>
#> blan :
#> Start Peak End Duration Signal Ongoing
#> 1 31 43 73 42 negative FALSE
#> 2 77 78 79 2 positive FALSE
#> 3 80 100 100 21 positive TRUE
tidy(res, cv = cv)
#> # A tibble: 5 × 4
#> id adf sadf gsadf
#> <fct> <dbl> <dbl> <dbl>
#> 1 psy1 -0.152 0.937 2.02
#> 2 psy2 2.56 6.42 14.0
#> 3 evans 4.85 5.76 6.85
#> 4 div 1.13 2.79 2.95
#> 5 blan 3.38 3.38 3.68
autoplot(res, cv = cv)
radf_sbz() is a fourth, separate case: it builds its
statistic (supBZ) on wls_dfstat_grid(), a
WLS/kernel-volatility-weighted no-intercept recursive-DF grid, not
gls_dfstat_grid() – but the same fix applies for the same
reason, since wls_dfstat_grid() already returns the full
badf/bsadf path per replicate.
radf_sbz_cv()’s wild bootstrap is therefore built the same
way as radf_tt_cv()/radf_sign_cv()’s Monte
Carlo simulation: per-time-point quantile across replicates, no
cummax() shortcut needed. Validated the same way:
badf_cv’s last row is bit-identical to adf_cv;
empirical false-alarm rate under H0 is 5.0% at nominal 5%
(n=100, minw=20, 200 replications); and it does reject on a sufficiently
strong deterministic explosive path, though its kernel-volatility
weighting trades away enough power on sim_data’s milder
bubbles that none of the five reject at nboot=100-200 – the same
power/robustness trade-off already documented for
radf_sbz_union()’s supBZ leg below, not a new
finding specific to the split.
res <- radf_sbz(sim_data, minw = 20)
cv <- radf_sbz_cv(sim_data, minw = 20, nboot = 200, seed = 1)
summary(res, cv = cv)
#>
#> ── Summary (minw = 20, lag = 0) ────────── Wild Bootstrap (SBZ) (nboot = 200) ──
#>
#> psy1 :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -1.22 0.544 0.801 1.22
#> 2 sadf 0.280 1.45 1.63 2.04
#> 3 gsadf 1.05 2.03 2.76 3.27
#>
#> psy2 :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -1.07 0.481 0.583 0.745
#> 2 sadf 1.53 1.76 2.72 3.49
#> 3 gsadf 1.56 2.35 3.43 4.43
#>
#> evans :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -2.95 0.599 0.846 1.26
#> 2 sadf -1.00 4.35 6.07 8.57
#> 3 gsadf 1.70 4.54 6.22 8.57
#>
#> div :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf 0.660 1.10 1.48 1.97
#> 2 sadf 2.26 2.25 2.59 2.98
#> 3 gsadf 2.26 2.60 2.82 3.53
#>
#> blan :
#> # A tibble: 3 × 5
#> stat tstat `90` `95` `99`
#> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 adf -4.14 0.745 0.939 1.59
#> 2 sadf 1.40 2.65 3.63 5.65
#> 3 gsadf 2.38 3.82 4.72 6.89
tidy(res, cv = cv)
#> # A tibble: 5 × 4
#> id adf sadf gsadf
#> <fct> <dbl> <dbl> <dbl>
#> 1 psy1 -1.22 0.280 1.05
#> 2 psy2 -1.07 1.53 1.56
#> 3 evans -2.95 -1.00 1.70
#> 4 div 0.660 2.26 2.26
#> 5 blan -4.14 1.40 2.38datestamp()/autoplot() need at least one
rejection to have anything to show (they error otherwise, same as for
any other radf_obj/radf_cv pair) – none of
sim_data’s five series clear supBZ’s bar
above, so here’s a series built to:
set.seed(7)
n <- 120; te <- 70
y <- cumsum(rnorm(n))
y[(te + 1):n] <- y[te] * 1.15 ^ seq_len(n - te)
res2 <- radf_sbz(y, minw = 20)
cv2 <- radf_sbz_cv(y, minw = 20, nboot = 100, seed = 1)
datestamp(res2, cv = cv2)
#>
#> ── Datestamp (min_duration = 0) ──────────────────────── Wild Bootstrap (SBZ) ──
#>
#> series1 :
#> Start Peak End Duration Signal Ongoing
#> 1 27 28 29 2 positive FALSE
#> 2 64 120 120 57 positive TRUE
autoplot(res2, cv = cv2)
No support: everything else
The remaining ~15 functions (lbi_test(),
ssu_test(), quantile_test(),
cobubble_test(), the dating_*() family, the
monitor()/monitor_*() family (including
monitor() itself, ADF-family internals notwithstanding –
see above), contagion_reg(), radf_recovery(),
rootstamp(), radf_sbz_union()) each return
their own class with their own print() method, because
their output genuinely doesn’t fit the radf_obj shape – a
dating table isn’t a per-series sup-statistic, a monitoring alarm isn’t
a critical value grid. Trying to force them through
summary()/datestamp()/tidy()/
autoplot() isn’t a documentation gap to close; the right
call is their own presentation, shown directly:
rootstamp() is the one exception worth flagging: it’s
grouped under Analysis in the reference index and the README’s
workflow list, right after datestamp(), since that’s
genuinely where it belongs in the sequence of steps (detect →
date → measure growth rate) – but that’s a workflow position, not an
S3-support tier. It’s still its own class with its own
print() method, same as everything else in this section;
see vignette("root-inference").
dating_hls(sim_data$psy1, trim = 0.05)
#>
#> ── dating_hls (n = 100, trim = 0.05) ───────────────────────────────────────────
#>
#> series model origination collapse recovery
#> series1 4 41 55 62
ssu_test(sim_data$psy1, level = 0.95)
#>
#> ── ssu_test (n = 100, minw = 19, level = 95%, crit = 3.3) ──────────────────────
#>
#> series sadf detected
#> series1 4.251 TRUESummary
| Tier | Functions | summary() |
datestamp() |
tidy() |
autoplot() |
|---|---|---|---|---|---|
| Full |
radf(), radf_common(),
radf_kp(), radf_tt(),
radf_sign(), radf_sign_dm()
|
yes | yes | yes | yes |
| Standalone | everything else | own print()
|
– | – | – |