Changelog
Source:NEWS.md
hvtiRbootstrap 0.9.6
-
boot_pool_chunks(),boot_chunk_files()andboot_shortfall()have help-page examples. They pool two small hand-built chunks, find chunk files in a throwaway folder undertempdir(), and report a pool that is a chunk short.
hvtiRbootstrap 0.9.5
Now requires R 4.4.0 or newer, up from 4.1.0, to match the rest of the HVTI family.
hvtiR::install()installs the members together, and several already required 4.4.0, so on an older R the install failed whatever this package declared.DESCRIPTIONnow declares the Quarto command line tool inSystemRequirements, which the new vignette needs to build.New overview vignette,
vignette("hvtiRbootstrap"): one small simulated screen run fromboot_select()throughboot_summary(),boot_clusters()and the reporting layer, withboot_predict_ci()alongside. On the pkgdown site, articles put the table of contents on the left and use the full window width, as the rest of the HVTI family does.
hvtiRbootstrap 0.9.4
- The
boot_select()help page now describes the shippedboot_bag()builder instead of saying it has not been written. The README function reference and displayed development version now agree with the package.
hvtiRbootstrap 0.9.3
sleandslsnow select, which they never did. They were accepted, recorded on$control, written into a bag asslentry/slstayand printed byboot_provenance()in everybl/br/bcreport – and never reached the screen, becausestats::step()selects on AIC. AIC retains a term at p < 0.157 wheresls = 0.05asks for 1.96 standard errors. Selection frequencies change, and by a lot: measured against the SAS run of the same data, the old behaviour sat a median 32.7 points away. A bag produced before this release and one produced after are not comparable.The screen now starts from no candidates and adds, as
SELECTION=STEPWISEdoes. It previously started from the full model and dropped, which is a different algorithm that can settle on a different set.Each fitter uses its own procedure’s criteria.
fit_linear()tests partial F,fit_logistic()enters on the score chi-square and removes on Wald. Divergence:fit_cox()enters on the likelihood ratio, because R has no score test for a Cox model –anova.coxph()acceptstest = "Rao"and silently ignores it. Removal is Wald, matchingPROC PHREG.No function gained a
criterionargument, and none will:sleandslsmean whatSLE=andSLS=mean in the job being ported.The cost curve broke, not just the walltime.
stats::step()refits every candidate in both directions at every step, which cost roughly the 3.72 power of the candidate pool – measured at 0.2s/fit for 10 candidates, up to 188.3s/fit at 80. The new driver measures 0.13s/fit at 10 candidates, up to 8.81s/fit at 172, a log-log slope of 1.50. It still refits every remaining candidate at each forward step, the same countadd1()did – the win is that the search starts from an intercept-only model instead of the full one, and that the backward step forfit_logistic()andfit_cox()is Wald, which needs no refit at all. Atn_rep = 500and 172 candidates that is the difference between about 450 hours and about 1.2 hours for the same screen. The exponent is the number that matters – a constant-factor speedup would not have made a 172-candidate, 500-replicate screen reachable at all.The interval branch is built.
boot_predict_ci()is the R port of%BNMNRand%BNPREV: it resamples, computes astatisticon each replicate, and reports percentile bands. It is not variable selection – nothing is chosen, the replicates are a distribution rather than a vote, and there is noNAsemantics.statisticis the fitter contract with the selection semantics removed: a named numeric vector, orNULLwhen the replicate failed. The names are the quantities banded, so a caller wanting a curve evaluates it on their own grid insidestatistic– which is what the macro does too, in aPROC NLMIXEDblock the analyst edits.iddraws whole units and renumbers them.%BNMNRbootstraps patients and assigns_PTID=_COUNTERbefore joining their repeated records, so a patient drawn twice enters the model as two patients. Without that the two copies share a random effect and the resample understates between-unit variance. The redrawn unit is in a.boot_unitcolumn.No function in this package takes a coverage level.
boot_predict_ci()returns both the 95% and the 68% band in columns named for their coverage –cll_p95,cll_p68,median,clu_p68,clu_p95– because that is what everybn.*macro returns, and a function that takes no level cannot be handed 95 where it wanted 0.95.The resampling loop
%bootregand%BNMNRshare is now one internal rather than two copies.boot_select()’s behaviour is unchanged.The
NEWS.mdversion test skips headings that carry no version, so merged work can sit under this heading without movingDESCRIPTION. The test read the first heading in the file and compared it againstDESCRIPTION, which fails as soon as an unreleased heading is on top. It also reads only level-one headings, matching the family convention settled on 2026-09-01, and no longer accepts#hvtiRbootstrap, which CommonMark does not treat as a heading at all.boot_pool_chunks()no longer reports a confidence interval it never computed.ci_lowerandci_upperwere quantiles taken over the replicates in which the term was selected, becauseboot_bag()writes$boot$replicateswithNAdropped. For a term chosen in 30% of replicates that was an interval over 30% of them, and the weaker the term, the narrower its interval looked. They are nowsel_q025andsel_q975, named for what they are: a sibling ofmean,sd,minandmax, which are conditional on selection in exactly the same way.boot_bag()andboot_pool_chunks()now build$boot$summarythrough one constructor, so the two cannot disagree. They had been building it separately, in two shapes with two different key columns –variablefrom the bag,parameterfrom the pool – so a renderer saw a different shape depending on whether the run happened to be chunked, andboot_bag()contradicted the key thatboot_validate()’s own documented example uses. Both now key onparameter.boot_summary()is unchanged and keepsvariable: it is the standalone%SUMBOOTport, not a bag.boot_validate()checks$boot$summary’s columns, not merely that the slot is filled. Nothing in the package reads that slot –boot_frequencies()rebuilds from$boot$replicates– so without this the two constructors could drift apart indefinitely without a test failing.-
A bag saved by hvtiRbootstrap 0.9.2 is now refused by the whole reporting layer.
boot_validate()’s new column check requires the keyparameter, and 0.9.2’sboot_bag()wrote$boot$summarykeyedvariable– soboot_frequencies(),boot_dropped(),boot_health(),boot_provenance(),boot_seeds()andboot_concepts(), which all gate onboot_validate()first, now refuse a bag that release wrote. The numbers in it were never wrong: nothing in the package reads$boot$summary, andboot_frequencies()rebuilds its table from$boot$replicatesinstead. Fix a saved bagbwithor re-run
boot_bag(). Quantiles on the selection branch use
stats::quantile(type = 4), which is SASPROC STDIZE’sPCTLDEF=1, rather than R’stype = 7default.
hvtiRbootstrap 0.9.2
Finishes 0.9.1’s sweep. That release made every optional bag field read by exact name; this one does the same for the fields nothing was protecting, and one of them was worse than 0.9.1’s note claimed. It also gives a finished run a way to say what it was, and a way to become a report.
New features
-
A
boot_select()run now records its own settings. The returned object carries$control:method,sle,sls,max_steps,fraction,seed,n_rows,n_terms,elapsed_minsand thepackageversion.$callcannot do that job, for a reason that is invisible until it bites.match.call()omits every argument left at its default, so a screen run the ordinary way – takingsle,slsandmax_stepsas given – has a$callnaming none of the thresholds that produced it. A provenance table built from it prints blanks exactly where the screen’s criteria belong, and the alternative is asking the caller to retype settings the run already knew.Recording them on the object means whatever assembles a bag can read the run rather than interview it.
-
boot_bag()assembles it. It converts aboot_select()result into the bagboot_validate()accepts and every reporting function reads.The two ends of this package were built against different studies and had never met.
boot_select()returns a wide object, one row per replicate and one column per term withNAwhere a term was not selected. The reporting layer reads a long one, because it was extracted from a hazard runner that writes long. Nothing converted between them, so the package’s own screen function could not reach its own report, and every study would have hand-written the pivot plus nine provenance fields, differently each time.The pivot drops
NArather than recording it, and that is the whole design rather than a shortcut:boot_frequencies()counts a term’s rows againstn_boot, so a row written for an unselected term would be counted as a selection and every frequency in the report would rise.Four arguments, which are the facts a screen cannot know about itself: which terms are the base model, how many candidates were offered before any were dropped, the dataset manifest, and what was dropped. Everything else comes from
$control, so a bag cannot claim an entry level the screen did not use. The result is validated before it is returned, so an invalid bag is never emitted to be refused three chunks into a render.A Cox screen has no intercept.
base_params = "(Intercept)"copied from a logistic runner names nothing there, and is refused rather than silently excluding no terms and reporting the base model as a candidate. boot_provenance()readsengine. Its “Fitting engine” row looked forth_shaand thenth_version, both written by TemporalHazard’s hazard runner. A bag this package assembled has neither, so the row that exists to say which codebase ran printedNAon everyboot_select()screen. That is the blank-where-provenance-belongs failure the table was built to prevent, occurring in the table itself.boot_bag()writesengine; the third branch reads it.
Bug fixes
-
A bag missing
base_paramsno longer validates.boot_validate()read it with$, and.chk_any()asks only whether there is a value – so a runner that wrotebase_params_originaland nobase_paramsproduced a bag that passed validation with the required field absent. 0.9.1’s note said an absent required field was “reported as absent either way, only the message misdescribes what was found”. That was true ofmanifestandboot. It was not true of this one.The consequence was silent rather than loud.
base_paramsis whatboot_frequencies()subtracts to get the candidate set and whatboot_health()takes the free base parameter from, so the sibling substituted into both and every table downstream was computed against a base set the screen never had. -
boot_pool_chunks()’s consistency gates read their fields by exact name. This function calls no validator, so nothing stood between a chunk and a prefix match. Read by prefix, a gate compares siblings – and when two chunks agreed on the sibling, the gate passed on evidence it never had. A pair of chunks recordingth_sha256and noth_shawere pooled with their fitting engine reported as verified.That inverts what the gates are for.
boot_pool_chunks()refuses to pool chunks that cannot be shown to be draws from the same screen, and theno chunk recordsbranch exists precisely so that a check which did not happen is never mistaken for one that passed.Every chunk field is now exact: the six gated ones (
slentry,slstay,base_params,usable,max_steps,manifest), the engine provenance pair (th_sha,th_version), and the ungated reads that pooled a wrong number rather than refusing (seed,n_boot,elapsed_mins, and the per-chunk success and failure counts). boot_validate()’smanifestandbootmessages now name what is actually wrong. Both errored before, but described the sibling they had found rather than the field they had not.
hvtiRbootstrap 0.9.1
Two defects in the reporting layer, and one mistake made twice: a field read under an assumption about its shape that nothing enforced.
Bug fixes
-
boot_dropped()no longer refuses adroppedfield that is absent (#21).$on a list falls back to prefix matching, so a runner that recorded onlydropped_collinearresolvedbag$droppedto that character vector, andboot_dropped()rejected it as the wrong type – for a field the runner never wrote. The message namedbag$droppedand sent its reader hunting for something that was not there.The shape it broke on is the one the documentation goes out of its way to promise: a screen that dropped nothing reports no rows rather than a fault. Any bag carrying a real
droppedfinds the exact match first and never saw this, which is why it took a constructed case to surface it, and why it is the worst distribution a defect can have – invisible in every bag in hand, fatal in the case that was advertised.Every optional field is now read by exact name:
dropped,free_sd,n_chunks,seeds,th_shaandth_version. The class is gone rather than the instance. -
A per-phase
free_sdis refused byboot_validate()rather than fatal inboot_health()(#22).boot_health()branches onfree_sdwith a scalarif, so a length-2 value killed the call with a message naming neither the field nor the function – and the validator had passed the bag happily on the way in.free_sdis scalar by contract. It is the standard deviation of the first free base parameter, andboot_pool_chunks(), its only writer, computes one number. So the validator now says so, alongsiden_bootandseed, and fails with the field named. Absent stays valid: a single unchunked run went through no pooling and carries none.
hvtiRbootstrap 0.9.0
The version moves from 0.1.x to 0.9.0 because the package has arrived at the scope it was designed for. boot_select() and the fitters run the screen, boot_summary() and boot_clusters() summarise it, boot_pool_chunks() makes a run of days restartable, and this release adds the reporting layer that turns a finished screen into tables. A study can now go from a candidate pool to a finished report without leaving this package.
It is not 1.0.0, and the reason is specific: the hazard fitter is still deferred. fit_hazard() does not exist, because the variants deserve reading before an API is fixed, and a 1.0.0 that then grows a new fitter family would be making a promise it has not earned. Chunk pooling has also been exercised on one real shape rather than many.
Nothing in 0.1.2 was ever tagged or released, so its notes are carried here rather than split across two version numbers.
New features
-
The reporting layer:
boot_validate(),boot_provenance(),boot_seeds(),boot_frequencies(),boot_dropped(),boot_concepts()andboot_health(). Each replaces a chunk that every bootstrap report inhvtiRtemplatescarried, so the logistic, linear and Cox reports can be thin templates rather than near-copies of the ~825-line hazard one. Four hand-synced copies of a report is a shape this family has watched drift before.The three that group their output —
boot_frequencies(),boot_concepts()and, through them, everything built on their rows — take an optionalphase: a function mapping a term to its phase. WithNULLthere is no phase dimension; a multiphase hazard screen passes its own term-splitting rule. That one argument is what lets a single code path serve four reports, and the alternative — the package serving three of them while the fourth keeps its own copy — reintroduces exactly the duplication the extraction removes.boot_dropped()andboot_health()take nophase, and that is not an omission. A candidate dropped before screening never became a model term, sobag$droppedcarries its ownphasecolumn written by the runner that dropped it; and a health check is screen-wide. An argument that cannot do anything is worse than an absent one. -
boot_validate()checks field shapes, not just presence, and it is the piece of this that would have prevented the defect that motivated it. The chunk it replaces checked that eleven fields existed. It passed a real screen happily whilerequestedwas a length-2 vector the report could not render, and that shipped in three releases: a length-2 value against a length-13 column does not recycle, it errors, so the provenance table could not build on any multiphase screen.So
requestedandusablemay be vectors and are checked as such, whilen_bootandseedmay not.integer(0)is refused on both sides of that line: it satisfies “is present” and then contributes no element rather than one, which collapses the table built from it. Every failure is reported at once, because an author fixing a runner wants the whole list. A selection frequency now carries its error.
boot_frequencies()reportsmc_errorper variable rather than one figure for the table: the Monte-Carlo error is largest at 50% and shrinks towards either end, so a single quoted value overstates it at the top of the table and understates it in the middle — and the middle is where the retention decision is being made.near_thresholdmarks the variables whose retention would not survive a rerun with different seeds.boot_concepts()answers the question a per-variable frequency cannot. Competing forms of one concept split replicates between them, so each form’s own frequency understates the concept;pct_anycounts a replicate that took two forms once, for the same reasonboot_clusters()’sn_anydoes. Two forms at 30% each are anywhere between 30% and 60% of replicates depending on how often the same replicate took both, and no marginal percentage records that — so it is computed from the replicates, and the computation isboot_clusters(), called rather than repeated.boot_health()calls an empty screen a failure. A screen that selected nothing is the signature of a formula that did not survive the per-replicate rewrite: the refit errors, the error is caught, the step accepts nothing, and the run halts with no warning andn_failed = 0. The summary then reads as a table of perfectly reliable variables. It returns its findings rather than stopping, so a report decides how to present them.
Under the hood
nandpctareboot_summary()‘s, and the at-least-one count isboot_clusters()’. Neither is reimplemented in the new layer. Those two are the invariants this package is judged on —NAmeans a term was not selected, and “at least one” is not a sum — and each keeps exactly one implementation.Reporting reads a wide replicate matrix while a runner writes a long one, so the pivot moves here from the template. Its row count is
n_boot, not the number of replicate ids present: a replicate that selected nothing outside the base model writes no rows at all, and counting ids would use a smaller denominator and inflate every frequency in the report. A replicate id outside1..n_bootis refused, becauseboot_pool_chunks()checks that only for the chunks it stacks and an out-of-range id lands on a neighbour’s row rather than erroring.
hvtiRbootstrap 0.1.1
CI
-
The PDF-manual (
check-manual.yaml) and pkgdown gates arrive here, bringing this package level with the rest of the family.AGENTS.md’s workflow table is updated to match — it had a standing note to do exactly that when these files appeared.The site builds into
pkgdown-site/, not the defaultdocs/, which holds this project’s plans and specs.build_site_github_pages()takesdest_dir = "docs"as an explicit default that overridesdestination:in_pkgdown.yml, so the workflow passes it directly. -
Pooling for chunked bootstrap runs:
boot_pool_chunks(),boot_chunk_files()andboot_shortfall(). Ported from a study’s localR/, where they were unreachable by anything else.A screen that writes nothing until its last replicate is unrestartable — a run that dies at 90% yields nothing — and on one real study a single forward selection over 160 candidates per phase was still running at 22 minutes, with 501 of them to do. Chunking makes such a run restartable; these fold the chunks back into one object of the same shape, so a report reads a pooled screen and a single-run screen the same way.
Pooling is only legitimate when every chunk drew from the same data, ran the same screen, and no two shared a seed. Each is checked rather than assumed, because none fails loudly on its own: two chunks sharing a seed hold literally the same replicates, so pooling counts each twice and reports a Monte-Carlo error smaller than the run has; a dataset rewritten mid-run gives chunks that each look fine and describe different cohorts. Neither shows up as an error in a frequency table — both show up as a slightly different number.
Absence is checked before agreement, and that order is the point. A field no chunk records formats identically in every chunk, passes unanimously, and hands back
NULLas the agreed value. A gate that cannot tell “everyone agrees” from “nobody recorded it” is worse than no gate, because it reads as a check that happened. That is not hypothetical:max_stepswas absent from every chunk in one fixture and the suite was green.Replicate ids are offset before stacking, since they run
1..n_bootinside each chunk and would otherwise merge — a variable selected in two chunks counting once instead of twice, understating every frequency.boot_shortfall()answers whether the pool is the run that was launched. Pooling mid-run is reasonable; the hazard is that a partial pool produces a report wrong in no visible way, where every check passes and only the denominator is not the intended one. The expected totals must come from outside the chunks, because nothing in a chunk knows how many siblings it was launched alongside.
hvtiRbootstrap 0.1.0
- Initial development version. Selection core only:
boot_select(),boot_summary(),boot_clusters(), with logistic, linear and Cox fitters. -
boot_select()no longer reports a factor predictor as never selected. Its candidate columns are the dummy-coded coefficient names the fitters return, so a factorsexappears once assexMrather than also as an all-NAsexcolumn withn = 0. -
boot_select()restores the caller’s RNG state on exit, so passingseedno longer changes later random draws in the calling script. - The fitters keep a converged model that merely warned. Previously any warning - notably
"fitted probabilities numerically 0 or 1"on a quasi-separated replicate - discarded the replicate, which biased selection frequencies against strong predictors. Errors and logistic non-convergence still reject. - A model that selected no terms is now a valid replicate rather than a failed fit. This only affected Cox, which has no intercept to survive selection, and it had been inflating every variable’s reported frequency.
-
boot_clusters()rejects duplicated cluster names, andboot_summary()rejects a matrix without column names, instead of failing later with an unrelated message.