Roadmap¶
The plan to 1.0.0 and the work done towards it, from the package's ROADMAP.md.
This document tracks where the package is going. It is updated alongside
meaningful code changes. Milestones are ordered, not dated. The history of what
changed and why lives in NEWS.md; the statistical evidence lives in
inst/validation/.
Where things stand (0.17.0)¶
- On CRAN. 0.11.2 was accepted in September 2026, after one review round on
0.11.1; 0.12.0, 0.13.0, 0.14.0 and 0.15.0 are GitHub releases. 0.16.0 was
accepted on 2026-10-29, after its auto-check. From 0.16.0 releases go to
CRAN again, about one to two months apart (decided 2026-10-28), so not
every GitHub release goes. CRAN's additional check of 0.16.0 with Intel MKL
as BLAS failed on tests that compared two computations of the same numbers
bit for bit. 0.17.0, published on CRAN on 2026-11-01, fixes that check: on
CRAN the tests compare such numbers to a tolerance of
1e-10, off CRAN still bit for bit, and
cran-blas.ymlchecks the package under MKL, OpenBLAS, BLIS, R without long doubles and a noisy BLAS on the pull requests that change it (#172, under 0.11.x). - The validation matrix is complete for every exported
perform*Test(). The sweep found six broken procedures, all corrected or withdrawn, and twenty-five of the twenty-six heteroscedasticity tests hold their nominal level (inst/validation/README.md). The tests added since have studies of their own:inst/validation/harrison-mccabe-size-power.Randinst/validation/glejser-skewed-errors.R. - Weighted fits were outside that matrix, and wrong. Every design in it
fitted an unweighted model. On a weighted fit the tests read the raw
residuals, whose variance differs across observations by assumption, and
rejected a correctly weighted model every time. 0.12.0 corrects that and
adds weighted designs to the matrix (
inst/validation/weighted-fits-size.R). In 0.13.0 the bootstrap tests accept a weighted fit too, with designs of their own in the same study, and since 0.15.0 they estimate the weights of afitWLS()fit again in each replicate rather than take them as known. - Reference equivalence is asserted to
1e-8againstlmtest,carandplmwherever a reference implementation exists.lmtest::bptest()is not a reference for weighted fits: it tests the raw residuals. - The public surface has been reviewed. 0.14.0 gave each of the 129
exports of 0.13.0 a verdict (the export inventory under 0.14.0): 104 are core
API, 22 are deprecated and warn, and 3 move to a companion package that has
no name yet. With
bootstrapTestStatistic(),simulate_size()andsimulate_power(), andrunAdvancedDiagnostics()in 0.15.0, the package exports 133 objects, andtest-public-api.Rasserts the 107 planned for 1.0.0. 0.16.0 adds no export: it registers eight S3 methods,print()andsummary()of the suites ofrunHeteroTests()andprint()andcoef()of their summaries. 0.17.0 changes no export, argument or default:NAMESPACEholds the same 133 exports. - The interface matches its description. In 0.15.0 every test takes
(model, data, ...),performBoxMTest(data, group)being the documented exception,studentizechooses the studentized statistic throughout,?test_resultsdocuments what every test returns, and every test refuses the same bad inputs with one message. The old call forms warn in 0.15.x and stop working in 1.0.0. The defaults changed with it: 999 replicates for every resampling function, White and Koenker as the default battery, and the corrected Glejser statistic. - The tests read the model they are given. In 0.16.0 the tests of a
model, but the resampling tests, read the response, the design, the
residuals, the weights, the offset and the rank from one validated model
context per call, built from the stored fit, and match
datato its observations, named data in any order and numbered data in the order of the fit (steps 1 to 7 ofdocs/design/model-context.md). White's degrees of freedom, Box's correction factor, Szroeter's null moments, Pesaran's test on an unbalanced panel and the ties of the Fligner-Killeen test are corrected, and the caches follow a contract. In 0.17.0 the other four steps are done: the resampling tests refit their replicates on the stored design (step 8), a fit withna.action = na.excludeis read as the same fit withna.omit(step 9), the runners and the functions outside the tests matchdataas the tests do (step 10), and aMASS::rlm()fit is refused and a penalisedmgcv::gam()fit tested or refused by what each test assumes (step 11). What stands between 0.17.0 and 1.0.0 is the internals and the defect backlog (0.18.0), the evidence at the release gate and the open decisions (0.19.0), and the freeze.
What 1.0.0 means¶
1.0.0 is a promise rather than a feature count. From 1.0.0:
- Exported names, argument names and order, and defaults change only in a major release, and only after a minor release in which the old form still works and warns.
- Every exported test returns an
htestobject with a documented, uniform set of fields. - A correction to a statistic, its degrees of freedom or its p-value is a bug
fix. It is never deferred for the sake of compatibility. It bumps the minor
version and gets its own
NEWS.mdsection, as 0.11.0 did.
Version numbers follow from that: a patch release changes no reported value; a minor release adds functionality or corrects a statistic; a major release breaks the API.
Everything below exists to make that promise safe to give. The sequencing rule this file has always followed still holds: statistical correctness first, then the API, and never both in one release. 0.12.0 reopened correctness for weighted fits and closed it again, and 0.13.0 corrected what was found after it, so the API is next.
Guiding principles¶
- Statistical correctness first. Every test must reproduce an established reference (or a defensible derivation) and be covered by a test that checks behaviour (size and power), not just object structure.
- A reference comparison is not a substitute for a size check. Four of the six faults the sweep found were in procedures with no reference to compare against, and the two that had one agreed with it.
- Guards are simulated, not stored. A stored-value regression test would have frozen each of those faults rather than caught it.
- One consistent interface. All diagnostics return base-R
htestobjects and follow theperform*Test(model, data, ...)convention. (Since 0.15.0 every exported test does, exceptperformBoxMTest(data, group), which tests the data rather than residuals; see 0.15.0.) - Scale and robustness as first-class concerns: streaming implementations for large data, resampling and robust variants for small or non-normal samples.
Milestones¶
0.11.x: CRAN maintenance (now)¶
- Watch the CRAN check results across all flavours and fix anything flagged within the deadline CRAN sets.
- README: install from CRAN first (
install.packages("heteroTests")), with GitHub as the development route; add a CRAN badge and use the canonical URLhttps://CRAN.R-project.org/package=heteroTests. - The README feature list is out of date. It still advertises an HC0–HC4
covariance test and Cameron–Trivedi, both removed in 0.8.0. Bring it in line
with the exports, and extend
test-documentation-consistency.Rso a README or vignette that names a removed function fails. The guard went intotest-public-api.R, beside the list of removed exports, and also matches the names prose gives the removed tests. - Documentation drift: the
performStudentizedBPTest()help page describesperformKoenkerTest()as "the absolute-residual variant", which predates the Breusch–Pagan/Koenker correction. Koenker is the studentizedn R^2form. - Check that the Zenodo record picked up 0.11.2, and cite the CRAN release
in
inst/CITATIONandCITATION.cff. Zenodo archives 0.11.2 and 0.12.0; only the 0.12.0 record links to CRAN. - Close tracking issue #38 as On CRAN. It was closed on 2026-09-27.
- Reinstate a release gate.
R CMD check --as-crancan stay weekly rather than per merge, but it must pass in full on the tagged commit (vignettes and--run-donttestincluded) before any submission, alongside win-builder (R-devel).release.ymlnow runs--as-cran --run-dontteston the tarball it is about to tag and creates no tag if the check warns or errors; win-builder stays a manual step (cran-comments.md). - The gate failed. The weekly
--as-cranjob stopped with an error on 2026-09-28 and on 2026-10-05: the\donttest{}example ofperformWhiteTestBootstrap(parallel = TRUE)startsdetectCores() - 1processes, three on the runner, and--as-cranallows two. The example is skipped on Windows, so a local check there does not see it. Fixed in 0.13.0 by changing the example, which now runs the call only where it starts at most two processes; the "CRAN submission checks" job passed on the branch on 2026-10-11 and onmainon 2026-10-12. The function still usesdetectCores() - 1workers by default; since 0.15.0n_coressets them, and the result no longer depends on how the draws are split. - CRAN's check of 0.16.0 with Intel MKL as BLAS failed on 2026-10-30
(
https://www.stats.ox.ac.uk/pub/bdr/Rblas/MKL/heteroTests.out, R-devel r90786):[ FAIL 179 | WARN 17 | SKIP 96 | PASS 15260 ], where the eight regular flavours that had checked it were OK. Every failure was anexpect_identical()between two computations of the same numbers: a cached result and an uncached one, a call withdataand one without, an lm and the Gaussian glm of the same model, an old call form and the new one, the streaming runner and the exact one, a test and its registered name. They agree to the last bit on R's reference BLAS; under MKL they differed by up to 1.6e-12 relative (performVarianceFormTest(), 0.008329376120413 against 0.008329376120400), and MKL gave different last bits for the same call made twice. In one the last bits changed a discrete outcome: on every row ofiristwice, which absolute residuals of Fligner-Killeen's test tie, 8.133 for the lm against 8.149 for the glm, a fragility ofperformFlignerKilleenTest()itself (under 0.18.0). Fixed in #172. The tests compare such numbers withexpect_identical_numbers()(tests/testthat/helper-numbers.R), bit for bit off CRAN, as in CI and on the author's machine, and on CRAN, as testthat'son_cran()decides it, withexpect_equal()to a tolerance of 1e-10: waldo's, the mean absolute difference of the elements that differ relative to their mean expected size, or absolute where that size is at most 1e-10. That is 64 times the largest difference under MKL. It is used at 235 sites in 30 files, found by running the suite as CRAN runs it with everyexpect_identical()of two non-integral numbers recorded, and by the noisy BLAS below. Six blocks whose ties, merged residuals or cache hits turn on the last bits run off CRAN only, the Fligner block among them: 27 of the expectations CRAN ran onmain. On CRAN, the F of the variance form test on the cells of the matrix with missing rows and White's test on a design 1e6 from zero are compared to 1e-8 (blas_tolerance()), Harrison-McCabe's two-sided p-value with twice the one-sided one on the scale of the integral both come from, and the p-values of Box's M, about 1.8e-10, by their logs.cran-blas.ymlrunsR CMD check --as-cranwithNOT_CRANunset in the R-hub containers for MKL (CRAN's version, 2026.2), OpenBLAS, BLIS and R without long doubles, and with a BLAS that moves its results by up to one unit in the last place (tools/noisy-blas.c, on three seeds), on pushes tomain, weekly, and on the pull requests that change the package or its tests. Onmainbefore the fix MKL passed, 2025.3 and 2026.2 alike: on the runners' AMD processors MKL runs its generic code, whose last bits do not depend on where the data sit in memory, where CRAN's machine runs its code for Intel processors. The first version of the noisy BLAS failed 1,923 expectations, at the sites of all 179 of CRAN's; OpenBLAS, BLIS and noLD passed. Since the review of #172 the noisy BLAS leaves alone the integral results no IEEE BLAS changes and what the reference BLAS leaves exact, and moves equal results of one call alike. A real BLAS can break such a tie, when a row falls to a peel or tail loop whose kernel rounds otherwise; the tests whose ties turn on it were reviewed by hand, and those of Szroeter's test run off CRAN only. With the fix every job passes, the noisy BLAS on each of its three seeds.
0.12.0: weighted fits and the shape of the variance (done)¶
This release was not on the roadmap. It took the 0.12.0 number because a corrected statistic bumps the minor version, and the milestones below moved up by one.
- Weighted fits. On an
lmfitted with weights, every residual-based test is computed from the Pearson residualssqrt(w) * e. Goldfeld–Quandt and RESET refit with the weights. Unweighted fits return bit-identical values. -
performVarianceFormTest(), a test whose null hypothesis is a variance function (exponential or power) rather than constant variance. -
fitWLS()takesvar_formulaandform, and records the variance function it fitted. - Evidence.
inst/validation/weighted-fits-size.Randinst/validation/variance-form-size-power.R, with simulated guards intest-weighted-fits.Randtest-variance-form.R.
Found on the way and left for their own changes:
- Resampling on weighted fits.
performWildBootstrapTest(),performWhiteTestBootstrap(),rbootstrap_test_statistic()andperformQuantileRegressionTest()refused a weighted fit, because they refitted without the weights. Resolved in 0.13.0 for the first three: they resample the Pearson residuals, return each draw to the scale of its observation and refit with the weights. That change leaves an unweighted fit as it was; the corrections 0.13.0 also makes to unweighted refits are listed under 0.13.0 below. Ininst/validation/weighted-fits-size.R, at 2000 replications withB = 499, they reject a correctly weighted fit 0.045 to 0.054 of the time in two designs.performQuantileRegressionTest()still refuses one: on the weighted scale its slopes differ across quantiles under the null hypothesis, and a version for weighted fits needs its own construction. A lead: the quantile regressions ofsqrt(w) yonsqrt(w) Xwith a column of ones added have equal slopes under the null hypothesis, which is the location-shift form the test assumes. It needs a derivation of the reference distribution and a size study before it can ship. -
runSurveyHeteroTests()did not use the survey weights. It calledstats::lm(formula, data = data, weights = weights), whereweightswas a local variable.lm()looks the name up indataand then in the environment of the formula, foundstats::weights, failed, and thetryCatch()refitted without weights. Resolved in 0.13.0 by making the function say what it does, because using the weights would have been worse: sampling weights are not inverse error variances, and a test on a fit that uses them rejected a homoscedastic model 97% to 100% of the time when the weights varied with the regressor.runHeteroTests(formula, design)did exactly that through asvyglm()fit, and now takes the unweighted route too. Evidence:inst/validation/survey-designs-size.R. A design-based test is under Candidate tests. -
.ht_fit_from_formula()had the same scoping fault and always failed withinvalid type (closure) for variable '(weights)', so the per-group refits built from anlm,glmor parsnip fit could not run. Fixed in 0.13.0: each group is refitted by evaluating the model's own call on it, which gives it its own rows of the weights and keeps the family of aglm. -
tidy()returned one row per degree of freedom. A result with two parameters, such as the F test ofperformVarianceFormTest()or the wild bootstrap with itsdfandB, was tidied into two rows, and the grouped and suite methods stacked them. Fixed in 0.13.0: one row per test, with the second parameter in a column of its own,parameter2. -
autoplot()of a test suite did not show significant results. The bars were p-values on a linear axis from 0 to 1, so a test with p = 0.002 had no visible bar, and the chart was empty exactly when the tests rejected. The highlighting of p < 0.05 never applied either: the condition was writtenisTRUE(df$p.value < 0.05), which isFALSEfor a suite of more than one test. The grouped method had the same condition and added a facet for thesuggestionscolumn. Fixed in 0.13.0: the bars are-log10(p)on an axis labelled with p-values, with a line atalphaand the p-value printed beside each bar. - An additive variance function,
sigma^2 = a + z'b, was implemented forfitWLS()and the form test and withdrawn before release. Under its own null, at 5000 replications, the test rejected 6.1% of the time at the 5% level with 150 and with 400 observations, outside the release gate, and the fitted variances were not all positive in 17% of samples at 50 observations and 6% at 150. It needs an estimator that keeps the fitted variances positive before it can come back. -
Tests of constant variance on a
fitWLS()fit take the estimated weights as known. With the variance function right and Gaussian errors, Koenker rejects 8% of the time at the 5% level with 150 observations and 11% with 600, White 6% and 8%, and Harvey never. That is documented, andperformVarianceFormTest()is the calibrated alternative, but a correction for the estimation step would letcompareModelDiagnostics()report calibrated p-values for a weighted remedy.The bootstrap tests hold the weights fixed and share the error: the wild bootstrap rejects 0.086 with 150 observations and 0.105 with 600, where it rejects 0.054 and 0.048 with the true weights in the same design. A bootstrap that runs
fitWLS()again on every replicate is such a correction. In the same designs it rejects 0.042 to 0.060 with 150 observations for the wild bootstrap, the White bootstrap andrbootstrap_test_statistic()with Koenker's statistic, and 0.051 with 600 for the wild bootstrap (inst/validation/weighted-fits-size.R). Resolved in 0.15.0 for the bootstrap tests, as proposed: the argumentreestimate_weightsof the three functions refits the variance functionfitWLS()recorded on the model, with the samevar_formulaandform, in each replicate, by default on a fit that carries that record; weights the user supplied are known and stay fixed (0.15.0, below). The tests of constant variance themselves still take the weights as known.
0.13.0: what was found after 0.12.0 (done)¶
This release was not on the roadmap either. It took the 0.13.0 number because it corrects statistics and adds functionality, and the milestones below moved up by one again.
- Values that changed. The bootstrap tests keep an offset given as an
argument when they refit an unweighted model,
rbootstrap_test_statistic()matches the rows of the data to the fit by name and leaves out rows the fit dropped, andperformWhiteTestBootstrap()regenerates a transformed response, where its p-value was 1.fitWLS()keeps the offset of the model it refits, andperformVarianceFormTest()changes with it on a model with an offset. The Szroeter test thatrunHeteroTests()runs by name orders the observations by the fitted values instead of by the second column of the data.runHeteroTests()andrunDiagnostics()no longer test a survey design through a fit weighted by its sampling weights.tidy()returns one row per test. - What was found on the way to 0.12.0: the resampling tests accept a
weighted fit, and the survey designs, the grouped refits and
autoplot()are fixed (above). - Two tests from the candidates:
performHarrisonMcCabeTest()andperformGlejserTest(robust = TRUE). Two more were measured and not adopted, the modified Wald test and stage 1 of the Dette, Neumeyer and Van Keilegom test (Candidate tests). -
listDiagnostics(), and the release gate inrelease.yml(0.11.x, above).
0.14.0: review the public surface, deprecate and remove nothing (done)¶
Goal: every one of the 129 exports gets a verdict, which is one of core API, renamed, internal or moved out. Nothing is removed in this release. Everything that will go warns.
The author accepted the four proposals for this release on 2026-10-14: the
dashboard and the benchmark suite move to a companion package and the
recommendation engine stays, with its return format frozen;
performCookWeisbergTest() stays, with an optional var_formula, and
performNCVTest() is deprecated; test() is folded into
summary.HeteroDiagnostic(); and simulate_*() and sigma_*() stay as a
documented snake_case family while the one-offs are renamed. The companion
package has no name yet, so the dashboard and benchmark functions are not
deprecated until it has one.
- Duplicate exports. Each duplicate below is deprecated in favour of
the name it duplicates.
performStudentizedBPTest()agrees withperformKoenkerTest()to1e-8except on a model with an offset, which it drops (listed under 0.16.0). It keeps its implementation, so no value changed in 0.14.0; on afitWLS()fit it read stale weights until 0.15.0 fixed that (listed under 0.16.0, done in 0.15.0).performBreuschPaganTest()isperformBPTest(), assigned atR/performBPTest.R:232.performStudentizedBPTest()is a separate implementation that matchesperformKoenkerTest()in every column of the full sweep. Confirm the statistics agree to1e-8, then keep one.performCookWeisbergTest()delegates toperformNCVTest()with the fitted values as the variance regressor.performCookWeisbergTest()becomes the one entry point for the Cook–Weisberg score test, withvar_formulaas an optional argument, andperformNCVTest()is deprecated (decided above).*Enhancedpairs:checkModel()/checkModelEnhanced(),plotResidualsFitted()/plotResidualsFittedEnhanced()andplotDiagnosticSuite()/plotDiagnosticSuiteEnhanced(). Merge each improvement into the base name.
- Internal machinery that is exported:
std_error(),std_warning(),validateTestInputs(),checkData(),TestFactory/test_factory(), the cache functions (cachedTest(),clearTestCache(),clearAnalysisCache()) and theht_*logging functions. Unexport them unless a user-facing reason exists; for logging, keep at mostht_set_log_level(). Deprecated in 0.14.0 and unexported in 1.0.0, except the cache functions, which stay for now (decided), andht_set_log_level(). - The five
r-prefixed exports (rbootstrap_test_statistic(),rcalculateEffectSize(),restimate_test_power(),rrunAdvancedDiagnostics()andrvalidate_against_reference()) follow no naming scheme in the package. Rename each to the house convention or unexport it. Four are done in 0.14.0:rbootstrap_test_statistic()is renamedbootstrapTestStatistic()and the other three are deprecated as internal helpers.rrunAdvancedDiagnostics()stays core API (decided), and was renamedrunAdvancedDiagnostics()in 0.15.0. -
test()is a generic with a very common name. Attached afterdevtools, it masksdevtools::test(). Fold it intosummary.HeteroDiagnostic()(decided above). Deprecated in 0.14.0, namingrunDiagnostics(x$model, x$data, tests);summary.HeteroDiagnostic()callsrunDiagnostics()and returns what it returned. - Naming convention. camelCase dominates (
performXTest(),fitWLS(),plotX()). The simulation family (simulate_hetero(),sigma_*()) is declared a deliberate snake_case family and documented as one (decided above). The one-offs (run_benchmark_suite(),generate_benchmark_report(),simulate_type_I_errors(),simulate_power_analysis()) are renamed. The last two aresimulate_size()andsimulate_power()in 0.14.0; the first two move out with the benchmark suite instead. - Scope. The dashboard (
R/dashboard.R, 698 lines) and the benchmark suite (R/benchmarking_system.R, 798 lines) move to a companion package (decided above), once it has a name. Between them they account for most of the optionalSuggests(shiny,DT,plotly,htmlwidgets,bench). Open: the package has no name, so they are not deprecated in 0.14.0 (see Decisions needed). - Related model checks (
performRESETTest(),performVIFDiagnostic(),performInfluenceDiagnostics(),performScatterDiagnostic()) are not heteroscedasticity tests. Keep them, grouped in the reference index as model checks, so they are not mistaken for tests of the variance. - Mechanism. Use base
.Deprecated(), so no new dependency is needed. Each call warns and names its replacement.NEWS.mdcarries an old → new mapping table. - Exit criterion. An export inventory with a verdict for every one of
the 129 exports, and
test-public-api.Rextended to assert the planned 1.0 export list, so that an accidental new export fails CI.
Export inventory¶
Every export in NAMESPACE on 2026-10-14, with a verdict. Of the 129, 104 are
core API, 10 are renamed, 12 become internal and 3 move out. The S3 methods
registered in NAMESPACE are not exports and are not listed;
S3method(test, HeteroDiagnostic) goes with test(). The verdicts apply the
decisions of 2026-10-14 as written, and the author's decisions on this
inventory, taken the same day, are marked "decided".
For each renamed or internal export, the last column lists every caller found
by a search of R/ (code, not comments or the definition), vignettes/ and
inst/validation/, adds README.md where it names the export, and counts the
lines in tests/ that call it. The notebooks in inst/tutorials/ call none of
these exports. The article was not searched: it moves to the 1.0 API after
1.0.0. An export that becomes internal keeps working for its callers in R/,
and for the tests, which run inside the namespace; a renamed export keeps
working under its old name and warns. What has to change in each pull request
are the callers outside R/, and the package's own calls of a name that will
warn.
| Export | Area | Verdict | Evidence that the change is safe |
|---|---|---|---|
performWhiteTest() |
test | core API | |
performWhiteTestStreaming() |
test | core API | |
performWhiteTestRobust() |
test | core API | |
performWhiteTestBootstrap() |
test | core API | |
performBPTest() |
test | core API | |
performBPTestStreaming() |
test | core API | |
performBPTestRobust() |
test | core API | |
performBreuschPaganTest() |
test | renamed: performBPTest(), the same object |
R/: none (an alias, assigned in performBPTest.R). Vignettes: using_heteroTests.Rmd:128. inst/validation/: full-sweep-size-power.R:169; 2 lines of README.md. Tests: 3 lines. |
performKoenkerTest() |
test | core API | |
performKoenkerTestStreaming() |
test | core API | |
performStudentizedBPTest() |
test | renamed: performKoenkerTest(), once item 2 shows they agree to 1e-8 (provisional, decided) |
R/: registry name "student_bp" (diagnosticsRegistry.R:70, test_factory.R:105); performBPTestRobust(studentized = TRUE) (robust_implementations.R:249); rvalidate_against_reference() (:455). Vignettes: none. inst/validation/: full-sweep-size-power.R:171, weighted-fits-size.R:138; 4 lines of README.md. Tests: 8 lines. |
performGQTest() |
test | core API | |
performParkTest() |
test | core API | |
performSzroeterTest() |
test | core API | |
performHarrisonMcCabeTest() |
test | core API | |
performWildBootstrapTest() |
test | core API | |
performQuantileRegressionTest() |
test | core API | |
performRankPermutationTest() |
test | core API | |
performHighDimensionalTest() |
test | core API | |
performSpatialHeteroTest() |
test | core API | |
performSpearmanTest() |
test | core API | |
performLeveneTest() |
test | core API | |
performGlejserTest() |
test | core API | |
performBrownForsytheTest() |
test | core API | |
performFlignerKilleenTest() |
test | core API | |
performBartlettTest() |
test | core API | |
performHarveyTest() |
test | core API | |
performArchLMTest() |
test | core API | |
performMcLeodLiTest() |
test | core API | |
performHartleyFmaxTest() |
test | core API | |
performOBrienTest() |
test | core API | |
performCookWeisbergTest() |
test | core API | |
performNCVTest() |
test | renamed: performCookWeisbergTest(), which gains an optional var_formula (decided) |
R/: registry name "ncv" (diagnosticsRegistry.R:67); performCookWeisbergTest() delegates to it (performCookWeisbergTest.R:69); the benchmark suite (benchmarking_system.R:368). Vignettes: none. inst/validation/: full-sweep-size-power.R:165, pass-a-size-power.R:87, weighted-fits-size.R:133,196; 6 lines of README.md. Also README.md:34. Tests: 20 lines. |
performSpreadLevelTest() |
test | core API | |
performPesaranTest() |
test | core API | |
performBPRandomEffectsTest() |
test | core API | |
performDavidianCarrollTest() |
test | core API | |
performVarianceFormTest() |
test | core API | |
performBoxMTest() |
test | core API | |
rbootstrap_test_statistic() |
test | renamed: bootstrapTestStatistic() (decided) |
R/: performWhiteTestRobust() and performBPTestRobust() (robust_implementations.R:135,270). Vignettes: none. inst/validation/: offset-refits-size.R, weighted-fits-size.R, labels in make-table.R; 22 lines of README.md. Also the Decisions needed row on fitWLS() fits. Tests: 60 lines. |
performScatterDiagnostic() |
model check | core API | |
performVIFDiagnostic() |
model check | core API | |
performRESETTest() |
model check | core API | |
performInfluenceDiagnostics() |
model check | core API | |
checkModel() |
model check | core API | |
checkModelEnhanced() |
model check | renamed: merged into checkModel() |
R/: performWhiteTestStreaming() (performance.R:267). Vignettes: none. inst/validation/: none. Tests: 8 lines. |
runHeteroTests() |
runner | core API | |
runHeteroTestsParallel() |
runner | core API (decided) | |
runDiagnostics() |
runner | core API | |
runDiagnosticPlots() |
runner | core API | |
registerDiagnostic() |
runner | core API | |
listDiagnostics() |
runner | core API | |
registerPlot() |
runner | core API | |
runMultivariateTests() |
runner | core API | |
runTimeSeriesTests() |
runner | core API | |
runPanelTests() |
runner | core API | |
runSurveyHeteroTests() |
runner | core API | |
compareModelDiagnostics() |
runner | core API | |
compareTestResults() |
runner | core API (decided) | |
analyzeMLResiduals() |
runner | core API | |
HeteroDiagnostic() |
runner | core API | |
test() |
runner | renamed: its warning names runDiagnostics(x$model, x$data, tests); summary() is unchanged (decided) |
R/: summary.HeteroDiagnostic() calls it (HeteroDiagnostic.R:59). Vignettes: real_world_case_studies.Rmd:110, tutorial.Rmd:32, using_heteroTests.Rmd:100,105. inst/validation/: none. Also README.md:143. Tests: 19 lines. summary.HeteroDiagnostic() will call runDiagnostics() directly. |
fitWLS() |
remediation | core API | |
fitRobust() |
remediation | core API | |
autoTransform() |
remediation | core API | |
suggestRemediation() |
remediation | core API | |
autoCompareRemediations() |
remediation | core API | |
analyseDatasetCharacteristics() |
recommendation | core API | |
suggestDiagnosticsForProfile() |
recommendation | core API | |
interpretHeteroTestResults() |
recommendation | core API | |
recommendRemediationStrategies() |
recommendation | core API | |
warnDiagnosticAssumptions() |
recommendation | core API | |
buildDiagnosticDecisionTree() |
recommendation | core API | |
composeDiagnosticNarrative() |
recommendation | core API | |
generateHeteroRecommendations() |
recommendation | core API | |
simulate_hetero() |
simulation | core API | |
simulate_arch1() |
simulation | core API | |
sigma_linear() |
simulation | core API | |
sigma_exponential() |
simulation | core API | |
sigma_group() |
simulation | core API | |
sigma_piecewise() |
simulation | core API | |
sigma_poly() |
simulation | core API | |
sigma_sin() |
simulation | core API | |
sigma_multiplicative() |
simulation | core API | |
sigma_spatial() |
simulation | core API | |
sigma_logistic() |
simulation | core API | |
sigma_inverse() |
simulation | core API | |
sigma_power() |
simulation | core API | |
sigma_step() |
simulation | core API | |
sigma_u_shape() |
simulation | core API | |
sigma_exp_decay() |
simulation | core API | |
sigma_gaussian_peak() |
simulation | core API | |
sigma_piecewise_linear() |
simulation | core API | |
simulate_type_I_errors() |
simulation | renamed: simulate_size() (decided) |
R/: none. Vignettes: none. inst/validation/: none. Tests: 3 lines. |
simulate_power_analysis() |
simulation | renamed: simulate_power() (decided) |
R/: none. Vignettes: named in the prose of troubleshooting.Rmd:44. inst/validation/: none. Tests: 3 lines. |
plotResidualsFitted() |
plotting | core API | |
plotSpreadLevel() |
plotting | core API | |
plotResidualDensity() |
plotting | core API | |
plotResidualQQ() |
plotting | core API | |
plotBubbleVariance() |
plotting | core API | |
plotDiagnosticSuite() |
plotting | core API | |
plotBeforeAfter() |
plotting | core API | |
plotResidualsFittedEnhanced() |
plotting | renamed: merged into plotResidualsFitted() |
R/: plotDiagnosticSuiteEnhanced() (plotDiagnostics.R:279). Vignettes: none. inst/validation/: none. Tests: 5 lines. |
plotDiagnosticSuiteEnhanced() |
plotting | renamed: merged into plotDiagnosticSuite() |
R/: generateDiagnosticReport() (reporting.R:53); the dashboard (dashboard.R:536). Vignettes: none. inst/validation/: none. Tests: 4 lines. |
theme_hetero() |
plotting | core API | |
scale_colour_hetero_diagnostic() |
plotting | core API | |
scale_fill_hetero_diagnostic() |
plotting | core API | |
generateDiagnosticReport() |
reporting | core API | |
downloadTeachingData() |
reporting | core API | |
ht_set_log_level() |
infrastructure | core API | |
std_error() |
infrastructure | internal | R/: 92 lines in 24 files, all internal callers, which keep working. Vignettes: none. inst/validation/: none. Tests: 9 lines. |
std_warning() |
infrastructure | internal | R/: performance.R:181, performWhiteTest.R:232, validation.R:675. Vignettes: none. inst/validation/: none. Tests: none. |
validateTestInputs() |
infrastructure | internal | R/: generateHeteroRecommendations() (intelligent_recommendations.R:686), the test factory (test_factory.R:80). Vignettes: none. inst/validation/: none. Tests: 21 lines. |
checkData() |
infrastructure | internal | R/: 16 lines in 7 files (advancedDiagnostics.R, advancedRemediation.R, fitWLS.R, HeteroDiagnostic.R, intelligent_recommendations.R, reporting.R, runPanelTests.R). Vignettes: none. inst/validation/: none. Tests: 9 lines. |
TestFactory |
infrastructure | internal | R/: only test_factory.R:98, which builds test_factory. Vignettes: none. inst/validation/: none. Tests: 2 lines. |
test_factory |
infrastructure | internal | R/: through its alias .test_factory, in .run_registered_diagnostic() (performance.R:610-612) and the dashboard (dashboard.R:53). Vignettes: none. inst/validation/: none. Tests: 10 lines. |
cachedTest() |
infrastructure | core API for now (decided); revisited at 1.0.0, once 0.16.0 bounds the caches | |
clearTestCache() |
infrastructure | core API for now (decided); revisited at 1.0.0, once 0.16.0 bounds the caches | |
clearAnalysisCache() |
infrastructure | core API for now (decided); revisited at 1.0.0, once 0.16.0 bounds the caches | |
ht_enable_log_capture() |
infrastructure | internal | R/: none. Vignettes: none. inst/validation/: none. Tests: none. |
ht_log_history() |
infrastructure | internal | R/: none. Vignettes: none. inst/validation/: none. Tests: none. |
ht_clear_log_history() |
infrastructure | internal | R/: none. Vignettes: none. inst/validation/: none. Tests: none. |
rcalculateEffectSize() |
infrastructure | internal | R/: performWhiteTestRobust(), performBPTestRobust() (robust_implementations.R:158,292), restimate_test_power() (bootstrap_methods.R:599). Vignettes: none. inst/validation/: none. Tests: 4 lines. |
restimate_test_power() |
infrastructure | internal | R/: performWhiteTestRobust(), performBPTestRobust() (robust_implementations.R:159,293). Vignettes: none. inst/validation/: none. Tests: 3 lines. |
rrunAdvancedDiagnostics() |
runner | core API (decided); its name is still open, see the item on the r-prefixed exports |
|
rvalidate_against_reference() |
infrastructure | internal | R/: none. Vignettes: none. inst/validation/: none. Tests: 2 lines. The reference comparisons live in tests/testthat/. |
launchDiagnosticDashboard() |
dashboard and benchmarks | moved out (companion package not named yet; not deprecated in 0.14.0) | R/: none outside dashboard.R. Vignettes: none. inst/validation/: none. |
run_benchmark_suite() |
dashboard and benchmarks | moved out (companion package not named yet; not deprecated in 0.14.0) | R/: none outside benchmarking_system.R. Vignettes: none. inst/validation/: none. |
generate_benchmark_report() |
dashboard and benchmarks | moved out (companion package not named yet; not deprecated in 0.14.0) | R/: none outside benchmarking_system.R. Vignettes: none. inst/validation/: none. |
Notes on the verdicts:
performStudentizedBPTest()is provisional. Item 2 first asserts that it agrees withperformKoenkerTest()to1e-8over the designs of the full sweep, weighted, rank-deficient andfitWLS()fits included. Disagreement anywhere is a finding, not a rename, and is reported before anything else.test()warns and namesrunDiagnostics(x$model, x$data, tests), which returns whattest()returns.summary.HeteroDiagnostic()keeps its return value, so no value changes in 0.14.0.- Registry names.
"student_bp"and"ncv"stay registered and keep calling what they call now, so their output does not change. Each warns once per call ofrunHeteroTests()orrunDiagnostics(), not once per group or bootstrap replicate, and goes in 1.0.0 with the function behind it. - The cache functions (
cachedTest(),clearTestCache(),clearAnalysisCache()) stay exported and are not deprecated in 0.14.0. They are revisited at 1.0.0, once 0.16.0 bounds the caches. rrunAdvancedDiagnostics(),runHeteroTestsParallel()andcompareTestResults()stay core API. The name ofrrunAdvancedDiagnostics()is still open under the item on ther-prefixed exports.- The simulation renames.
simulate_size()andsimulate_power()join the documented snake_case family, since both simulate under thesigma_*()functions. bootstrapTestStatistic()takes a test function as its first argument, so it is not aperform*Test(model, data, ...); the new name says what it does in the house camelCase.- The dashboard and the benchmark suite are not deprecated in 0.14.0,
because the companion package has no name yet. They call
plotDiagnosticSuiteEnhanced(), the test factory andperformNCVTest(), which change before the code moves. analyzeMLResiduals()andanalyseDatasetCharacteristics()spell the verb two ways. Both are core API; the roadmap names no change for them.
0.15.0: make the interface match its description (done)¶
These changes break existing calls. They have to happen before 1.0.0, not after it.
-
The
(model, data, ...)convention. Nine exported tests did not follow it:- no
dataargument:performArchLMTest(),performMcLeodLiTest(),performCookWeisbergTest(),performNCVTest(),performSpearmanTest(),performSpreadLevelTest(),performDavidianCarrollTest()andperformHarveyTest(); - no
modelargument:performBoxMTest(data, group).
Decided on 2026-10-15 as proposed: the eight take
data = NULLin second position, recovered from the model frame when omitted, andperformBoxMTest()is the documented exception, since it tests the data rather than residuals. A call that passes the old second argument by position (performArchLMTest(m, 3),performCookWeisbergTest(m, ~x),performHarveyTest(m, "fitted")) is recognised by its type, works in 0.15.x, warns with the new call and stops working in 1.0.0;NEWS.mdhas the table. No value changes: every old and new form returns what the old form returned in 0.14.0, checked withidentical()against 0.14.0. Extended on 2026-10-15 to every other test:performRESETTest(model, power)as the eight, and the 29 tests that requireddatanow default it toNULL, the model frame. A call that worked returns what it returned, exceptperformRESETTest(m, NULL)andperformRESETTest(m, <data frame>), which read the second argument aspowerand returnedNaNor a meaningless F. - [x] Argument names. Harvey usedstudentizeandperformBPTestRobust()studentized. Standardised onstudentize, which matcheslmtest::bptest();studentizedis accepted with a deprecation warning in 0.15.x and stops working in 1.0.0, and therobust_details$studentizedfield of the result keeps its name. No value changes. - [x] The name ofrrunAdvancedDiagnostics(). RenamedrunAdvancedDiagnostics()(decided 2026-10-15), the house convention for a runner. The old name warns with.Deprecated()in 0.15.x and goes in 1.0.0 with the names deprecated in 0.14.0. No value changes. - [x] Resampling defaults.Bwas 499 for the wild bootstrap, 999 for rank permutation, and 1000 for the White bootstrap andbootstrapTestStatistic(). Under the(1 + #) / (B + 1)convention, a test at levelalphais exact only whenalpha * (B + 1)is an integer, and 1000 fails that at 5%. Done (decided 2026-10-15:B = 999): every resampling function draws 999 by default, the robust tests withbootstrap = TRUEandrunAdvancedDiagnostics()(500 before) andperformSpatialHeteroTest()(499 permutations before) included. Default p-values change;NEWS.mdhas an example for each. Of the recorded evidence only the spatial row of the full sweep used a default, and it was regenerated: size 0.045 to 0.050, power 0.362 to 0.350. - [x] Wild bootstrap multiplier (Rademacher or Mammen). Settle it by a size study at smallnwith skewed errors, and record the result ininst/validation/.Measured on 2026-10-15 in
inst/validation/wild-multiplier-size.R: the cross-sectional design of Pass A at 30, 50 and 100 observations, with Gaussian,t5and centred exponential errors, 5000 replications andB = 999, both multipliers on the same samples. The decision rule was written into the script before the full run started; the script was first committed with its results. Neither multiplier holds the level in more cells, and no paired difference is outside its 99% interval (within Monte Carlo error). Both are inside the release gate in the same six of the nine size cells, those with Gaussian andt5errors, and outside it in the same three, those with exponential errors, where Rademacher's multiplier rejects 0.082, 0.073 and 0.061 of the time and Mammen's 0.078, 0.070 and 0.059: Mammen's rate is lower in all three cells, each difference inside its 99% interval. The interval around 0.049, the rate of an exact test withB = 999, gives the same split. Againstsigma^2 = exp(0.4 x1)the power of the two differs by less than one point at eachn. Done (decided 2026-10-16: keep"rademacher", as the study recommended); no value changes. The help page said the bootstrap controls the size for heavy-tailed errors; that holds for thet5errors of the study and not for the skewed ones, with either multiplier, and #125 corrected the page. A wild bootstrap without a multiplier, which may hold the level better under skewed errors, is to be measured in 0.16.0 (below). - [x] Reproducibility underparallel = TRUE. A search ofR/finds noclusterSetRNGStream()and no L'Ecuyer set-up on themclapply()andparLapply()paths, soset.seed()does not make parallel resampling results reproducible. Done: replicateidraws from streamiof a L'Ecuyer-CMRG generator seeded by one draw from the session's, on both paths, inbootstrapTestStatistic(),performWhiteTestBootstrap()andrunHeteroTestsParallel(), so the value does not depend on the number of workers. Serial calls are unchanged; parallel and serial values differ and are each valid (test-parallel-reproducibility.R).runHeteroTestsParallel()with one worker runs in the session on the same streams, so its value does not depend onn_coreseither, and changes. The White bootstrap runs on a socket cluster on Windows, where it stopped, and gainsn_cores. The forked path is exercised by CI on Linux and macOS only. - [x] Bootstrap tests on afitWLS()fit. Accepted for 0.15.0 as proposed under Decisions needed, and moved from 0.14.0 because it changes values.performWildBootstrapTest(),performWhiteTestBootstrap(),bootstrapTestStatistic()and its deprecated name takereestimate_weights.NULL, the default, runsfitWLS()again on each replicate of a fit that carries its record of the variance function and holds the weights of any other model fixed;FALSEholds them fixed, as 0.13.0 and 0.14.0 did;TRUEon a model without the record is an error. Each replicate is fitted as the modelfitWLS()was given, the recorded variance function is fitted again on the recorded regressors, and the replicate is refitted with the weights that gives; one whose variance function cannot be fitted counts as a failed replicate. The default p-values on afitWLS()fit change, announced in bold inNEWS.md. Anlm, a weightedlmand a Gaussianglmreturnidentical()values, and so does afitWLS()fit withFALSE. Ininst/validation/weighted-fits-size.R, at 2000 replications withB = 499, the wild bootstrap rejects 0.0605 and 0.0515 of the time at 150 and 600 observations, the White bootstrap 0.0415 andbootstrapTestStatistic()with Koenker's statistic 0.045, against 0.0865, 0.105, 0.064 and 0.0825 with the weights held fixed. Those four rows were measured by the prototype of 0.12.0 and not run again: the package reproduces it on 200 samples of each, p-values, replicates and random draws (inst/validation/weighted-fits-refit-reproduction.csv). The wild bootstrap at 150 observations is outside the release gate's interval[0.042, 0.058]and the White bootstrap just below it; a study at 5000 replications is listed under 0.19.0. - [x] The default battery.runHeteroTests(),test()/summary()onHeteroDiagnostic, the advanced diagnostics and the dashboard all defaulted toc("white", "breusch_pagan"), and"breusch_pagan"is the classical statistic. The package's own sweep shows it rejecting 28.75% of the time under at5null, against 4.75% for Koenker. Done (decided 2026-10-15: White and Koenker): the default isc("white", "koenker")in those functions and inrunDiagnostics(),compareModelDiagnostics(),compareTestResults()andrunSurveyHeteroTests(), and the classical test runs by name.suggestRemediation()reads"koenker"as well as"breusch_pagan", andsuggestDiagnosticsForProfile()recommends Koenker's test as the baseline.runAdvancedDiagnostics()already computed the studentized statistic by default and is unchanged. No recorded number moves. - [x] The default ofrobustinperformGlejserTest(). It wasFALSE, Glejser's (1969) statistic, which ininst/validation/glejser-skewed-errors.Rrejects a true null hypothesis 8.8% to 17.7% of the time at the 5% level when the errors are skewed, at every sample size from 50 to 1000. Done (decided for 0.15.0 as proposed:TRUE): the default is the corrected statistic of Im (2000) and Machado and Santos Silva (2000), added in 0.13.0. From 150 observations it holds its level under every error distribution measured, inside the release gate in all 27 of its null cells; at 50 observations it rejects 5.7% to 6.6% of the time under skewed errors, four of those six cells above the gate, and gives up 1.1 and 1.9 points of power under Gaussian errors. Default values change;NEWS.mdhas an example.robust = FALSEreturns what the default returned in 0.14.0, checked withidentical()against 0.14.0. Of the recorded evidence the full sweep and the weighted fits call the default, and their two rows were regenerated from the same seeds: size 0.0600 to 0.0575 and 0.0475 to 0.0450. Pass A and the Glejser study name Glejser's statistic and now giverobust = FALSE; their numbers do not move. - [x] Return contract. Document the fields everyperform*Test()returns, both thehtestcore and the package's additions, on one help page. Check every exported test against it in a single parametrised test. This also retires the hand-maintained printer and metadata drift listed under the old technical debt. Done:?test_resultsandtest-return-contract.R. Five tests gained the namedfof theirparameterandperformBoxMTest()itsdata.name; no number changed. - [x] Uniform input validation across all exported tests. The README lists its absence as a known limitation.test-input-validation.Rgives every test aglmthat is not a least-squares fit, a model with anNAcoefficient, data that lack rows of the fit and data that lack a variable, and every test stops on each with the same message. Refusing the inputs some tests returned a value for was decided on 2026-10-15: 36 test names refuse aglmof a family other thangaussianor a link other than the identity, five refuse anNAcoefficient, and the variance-form, Pesaran and random-effects tests refuse data that cannot be matched to the fit (NEWS.mdlists the calls). A Gaussianglmwith the identity link is accepted and gives the result of thelm. With prior weights the GQ, RESET, White bootstrap and wild bootstrap tests, which read the weights of anlm, read them since then, decided on 2026-10-15, where they took theglmfor an unweighted fit, and the quantile regression test refuses it as it refuses a weightedlm; those calls change value. No recorded number moves. - no
0.16.0: the model context, part 1 (increments 1–7), and statistics fixes (done)¶
Planned as one milestone for the statistics evidence, the dependencies and the
internals, with no API change. The author split it on 2026-10-26: 0.16.0 is
the first seven steps of the model context and the statistics fixes found on
the way, and the rest of the plan moves to 0.17.0 (the model context, part 2),
0.18.0 (internals, dependencies and the defect backlog) and 0.19.0
(release-grade evidence and the open decisions). No export or argument
changes; the default of chunk_threshold_mb in runHeteroTests() and
runDiagnostics() becomes Inf, and the print and summary methods of the
suites, approved on 2026-10-21, are added.
-
performBoxMTest()is conservative, with size 0.012. It uses the chi-square approximation. Compare it against Box's F approximation, then either switch or document the conservatism on the help page. Done in 0.16.0 (decided 2026-10-25: fix the factor, keep the chi-square). The comparison,inst/validation/box-m-approximations-size.R, found that the 0.012 is not the approximation's: the correction factor was(g - 1)times Box'sc1, so the statistic was too small with three or more groups, and on the sweep's design,xuniform and so light-tailed, Box's chi-square and his F both reject 0.017 of the time at 5000 replications, against 0.016 with the factor of 0.15.0. The factor is now Box's: two groups giveidentical()values, and three or more a larger statistic, 140.9430 onirisby species, the value ofbiotools::boxM(),heplots::boxM()andrstatix::box_m(), where it was 135.2229 (announced in bold). Under Gaussian data Box's chi-square is inside the release gate in 60 of 67 cells; it is liberal when the groups are small for the number of variables (0.062 to 0.106 at 5% in the four cells of groups of 10 with 4 or 6 variables outside the gate), not robust to heavy tails (t5coordinates: 0.26 to 0.48) and conservative with light tails, all now on its help page. The F approximation, closer to the level in the small-sample cells, changes the statistic and its parameters, and was not adopted. -
performSzroeterTest()on a weighted fit. Ininst/validation/harrison-mccabe-size-power.R, on a correctly weighted fit with Gaussian errors, it rejects 0.067 of the time at 40 observations, outside the release gate, and 0.056 and 0.055 at 100 and 400; without weights it is inside the gate at all three sizes. Its null mean and variance,(n + 1) / 2and(n^2 - 1) / (6 n), do not depend on the design, and in that weighted design the start of the ordering has the larger leverages. Either take its null distribution from the weighted design, asperformHarrisonMcCabeTest()does, or document the restriction on the help page. Done in 0.16.0 (decided 2026-10-25: the exact moments, the normal reference kept).inst/validation/szroeter-design-null-size.Rcompared, on the samples of that study, the moments ofhgiven the design with the exact null from the eigenvalues: within 0.0006 of each other in every cell, and both inside the gate in all six Gaussian cells. The test now standardiseshwithE h = tr(M A) / nuandVar h = 2 (nu tr(M A M A) - tr(M A)^2) / (nu^2 (nu + 2))of the weighted, ordered design, taken asperformHarrisonMcCabeTest()takes it, at a cost ofO(n k^2): weighted 0.0494, 0.0470 and 0.0498 at 40, 100 and 400 observations, unweighted 0.0482, 0.0498 and 0.0494.hisidentical(); everyQand p-value changes (announced in bold), unweighted by a median 0.006 at 40 observations and 0.0006 at 400. - The wild bootstrap without a multiplier.
performWildBootstrapTest()already resamples the leverage-standardised residuals i.i.d., which imposes the null hypothesis, and multiplies them byvon top. Ininst/validation/wild-multiplier-size.Rit is outside the release gate with centred exponential errors at 30, 50 and 100 observations with either multiplier. An exploratory check by the reviewer of that study, on seeds of its own and not to evidence grade, suggests that the excess comes from the multiplier step: with no multiplier (v = 1, a plain residual bootstrap) the rejection rates were 0.048, 0.053 and 0.062 where Rademacher's gave 0.064, 0.067 and 0.077 on the same samples, with exponential errors at 50 and 30 observations, paired differences of about -1.4 to -1.6 points with standard errors of about 0.0018. Measure a no-multiplier option to evidence grade in the design of that study. The option, and still more a default, changes values the function returns, so it is the author's decision. The help page's claim about heavy-tailed errors is corrected in #125. Measured in 0.16.0 byinst/validation/wild-no-multiplier-size.R, on the samples of that study at 5000 replications and at 200 observations as well: with exponential errors Rademacher's multiplier rejects 0.0820, 0.0734, 0.0608 and 0.0526 of the time at 30, 50, 100 and 200 observations, Mammen's 0.0782, 0.0698, 0.0586 and 0.0534, and no multiplier 0.0648, 0.0562, 0.0482 and 0.0466, inside the gate from 50 observations where the multipliers are from 200; with Gaussian andt5errors all three are inside it. No multiplier costs 1.4 points of power at 30 observations and none at 100. Decided on 2026-10-25: document it now, with no change of value in 0.16.0 (the help page's Size section has the table), and offer it asdistribution = "none"in 1.0.0, listed there. -
runAdvancedDiagnostics(tests = "bp")stops with "Unsupported test requested: bp", although its help page offersc("white", "bp")as a subset. The check compares the names asked for with the values of its lookup table ("white","breusch_pagan") rather than with its names, which include"bp". Found on 2026-10-15 and recorded, not fixed, in 0.15.0. Done in 0.16.0: the names asked for are matched to the names of the table, so"bp"runs what"breusch_pagan"runs, under the namebreusch_pagan, as its help page says and asrvalidate_against_reference()already took"bp"; every other name is refused with the message it had (announced in bold). -
runAdvancedDiagnostics(auto_enhance = FALSE)silently runs the classical Breusch–Pagan test.auto_enhancealso setsstudentizeforperformBPTestRobust(), so turning the small-sample bootstrap off turns Koenker's studentization off with it, and the result, namedbreusch_paganeither way, then assumes normal errors, which the default battery stopped doing in 0.15.0. Either separate the two settings or say on the help page whatauto_enhance = FALSEruns. Found on 2026-10-15 and recorded, not fixed, in 0.15.0. Done in 0.16.0 (decided 2026-10-23): the two settings are separate.auto_enhanceturns the small-sample bootstrap on and off and nothing else, and the Breusch-Pagan entry is Koenker's studentized statistic either way, the default ofperformBPTestRobust(); the classical test is stillperformBPTestRobust(studentize = FALSE). Withauto_enhance = FALSEthe entry changes, from 3.135 (p = 0.2086) to 3.086 (p = 0.2138) onlm(mpg ~ wt + qsec, data = mtcars)(announced in bold). -
checkModel(<glm>, data, enhanced = TRUE)stops with "missing value where TRUE/FALSE needed", Gaussian and binomial alike: the enhanced check reads the R-squared ofsummary(), which aglmdoes not have. Since 0.15.0performWhiteTestStreaming()refuses aglmbefore calling it; the check itself should refuse it with a clear message or skip that step. Found on 2026-10-15 and recorded, not fixed. Done in 0.16.0 by the model-eligibility work (#136), and found done in the 0.16.0 release audit: the check reads the R-squared of the least-squares fit, so on a Gaussianglmit warns about a near-perfect fit as on thelm, and on aglmof another family it skips that check (R/utils.R,test-model-eligibility.R). - A weighted fit without
data. On a fitlm(..., weights = w)whosewis a column of the data, the tests that rebuild the design withmodel.matrix(model, data = data)(performWhiteTest(),performBPTest(),performKoenkerTest()and others) stop with "object 'w' not found" whendatais omitted, as they do when given the model frame, which holds the weights as(weights). Found on 2026-10-15; no value changes. The eight areperformWhiteTest(),performWhiteTestRobust(),performWhiteTestBootstrap(),performBPTest()andperformBreuschPaganTest(),performKoenkerTest(),performRankPermutationTest()andperformHighDimensionalTest(), and a fit withoffset = o, held as(offset), stops them the same way (found in the 0.15.0 release audit). ThroughrunHeteroTests()andrunDiagnostics()such a fit does not stop: White's and Koenker's tests fail and both names hold the Cook-Weisberg test the runner falls back on (below). A fix changes what those runner calls return. Done in 0.16.0 by the first step of the model context (below): the five tests that rebuilt the design read it, the weights and the offset from the fit, so the eight give the value they give withdata, and the runners return White's and Koenker's tests under their names (announced in bold). - The fallback of
runHeteroTests(). When a test stops, the runner runs the substitute registered for it in.ht_fallback_registry(for"white": White's test without cross products, then the classical Breusch-Pagan, Koenker and Cook-Weisberg tests) and returns it under the name asked for, tagged byattr(, "diagnostic")andattr(, "extras")$status == "fallback"and announced in aWARNlog line.$whitecan then hold another test. Documented on?runHeteroTestssince the 0.15.0 release audit; whether a failed test should rather be returned as failed is an interface decision for 1.0.0. Removing the fallback changes what such calls return.compareModelDiagnostics()reportsNA, with a warning, whenattr(, "diagnostic")names another test, but White's test without cross products keeps the name"white", and its table has no place for the tag: on 22 observations of six regressors, whose White's test is refused, it reports 15.98, the statistic of the fallback, in the columnwhite, with no tag and no warning. Found by the review of the follow-up of N2; the same decision settles it, and until then it does not change. Decided on 2026-10-23, for 1.0.0: a failed test keeps its name and is returned as failed, and the substitute is returned under its own name (the item under 1.0.0). Ticked in the 0.16.0 release: the decision is taken, and the work is that item. - A
glmof another family through the registry. The registry names deprecated in 0.14.0,"ncv"and"student_bp", called.ncv_test()and.studentized_bp_test()directly, and the refusal of aglmthat is not a least-squares fit was inperformNCVTest()andperformStudentizedBPTest()only. SorunHeteroTests(m, d, tests = "ncv"),cachedTest("ncv", m, d)andrunHeteroTestsParallel()returned a value form <- glm(high ~ x1 + x2, family = binomial), and, because"ncv"is the last fallback of"white","koenker"and"breusch_pagan",runHeteroTests(m, d)with its default battery returned the Cook-Weisberg statistic under the nameswhiteandkoenker. Found in the 0.15.0 release audit. Done in 0.15.0 (decided 2026-10-16): the two registry entries, the"student_bp"entry oftest_factoryand the"studentized_bp"key ofrvalidate_against_reference()refuse such a fit with the message of those functions, sorunHeteroTests(m, d)stops with White's refusal and no name holds a fallback computed on it. On anlm, a Gaussianglmand a weightedlmevery value isidentical()to before. - The panel tests on a fit that dropped rows. On
lm(y ~ x1 + x2, data = d)withNAin three rows ofx2,performPesaranTest(m, d, "id", "time")stops with "requiresdatawith 117 observations to match the fitted model, got 120" (since the 0.15.0 guard on the order of the rows; "arguments imply differing number of rows: 120, 117" before it), and with only the complete rows with "number of items to replace is not a multiple of replacement length";performBPRandomEffectsTest(m, d, "id")refuses the full data with "requiresdatawith 117 observations to match the fitted model, got 120", where the other tests match the rows by name. As in 0.14.0; found in the 0.15.0 release audit. A fix makes such calls return a value. Done in 0.16.0 by the fifth step of the model context for the random-effects test and the scatter diagnostic, which give the value of the fit to the complete rows (2.016, p = 0.156, for the random-effects test) on the full data, shuffled or as a tibble. Pesaran's test now reaches the panel of the fit, which is unbalanced, and refuses it (the item below); on a fit that drops every row of a unit, whose panel stays balanced, it gives the value of the fit to the complete rows. Since the pull request of N12 (#160) it gives the value of the fit to the complete rows on the unbalanced panel too: 0.189 (p = 0.850) withx2missing in rows 3, 7 and 11 ofcontract_data(), the value ofplm::pcdtest()on the rows of the fit. - Pesaran's test on an unbalanced panel (N12 of the design of the
model context).
performPesaranTest()fills a matrix of periods by units with the residuals of each unit in the order of its periods, and a unit observed in fewer periods than the panel holds does not fit its column: the assignment stops with "number of items to replace is not a multiple of replacement length", or, when the number of periods of the unit divides that of the panel, recycles its residuals over the periods it lacks and returns a wrong value without an error. On the 120 observations ofcontract_data()without the first five rows, unit 1 keeps 5 of its 10 periods, and the test returns 0.234 (p = 0.815), whereplm::pcdtest(model = "pooling"), which correlates each pair of units over the periods both hold (Pesaran 2004, Section 9), gives -0.373. As in 0.14.0; the fifth step of the model context lets more data reach it: the full data of a fit that dropped half the rows of a unit, which the test refused for their number of rows, would give the recycled value, as the complete rows of such a fit did. Decided by the author on 2026-10-24: refuse now, fix later. Since the fifth stepperformPesaranTest()refuses a panel whose units are not all observed at the same periods ("needs a balanced panel, every unit observed at the same periods", of classheteroTests_model_context_error), on every route,runPanelTests()included, so it never returns the recycled value, and the error of the assignment can no longer be reached; a fit that drops every row of a unit keeps a balanced panel and is tested. The fix, a pull request of its own, computes the statistic of an unbalanced panel on the periods each pair of units shares, asplm::pcdtest()does (Pesaran 2004, Section 9), and removes the refusal; it changes what such calls return, announced in bold. Done in 0.16.0:CD = sqrt(1 / P) sum_{i<j} sqrt(T_ij) rho_ij, withrho_ijthe correlation of the residuals of unitsiandjover theT_ijperiods both are observed at, each centred at its mean over them, andPthe number of pairs that share two periods or more, asplm::pcdtest(test = "cd")computes it; a pair that shares fewer is left out with a warning, as plm leaves it out, and a panel in which no pair shares two periods is refused; a correlation that is not defined, a unit constant over the periods of a pair, adds nothing and the pair stays inP, with a warning, where plm returnsNA. The example gives -0.373 (p = 0.709), and every value equals plm's to1e-10on 252 panels, 228 of them unbalanced, in 5,040 comparisons: late entry, early exit, gaps, 1 to 20 rows missing at random, a unit observed once, balanced panels, weighted fits and a Gaussianglm, the full data of a fit withna.omit, and throughrunPanelTests(). On a balanced panel every value isidentical()to before. A fit withna.action = na.excludethat dropped rows is read by position until step 9, and an unbalanced panel of the rows ofdatastays refused there, with a message that points tona.omit; one that dropped no row is tested as any other. Since step 9 (0.17.0) such a fit is tested as the same fit withna.omit, its panel balanced or not, and the refusal is gone. - The wild bootstrap in the advice for a small sample. 0.15.0 corrected
the warning of
warnDiagnosticAssumptions()for a sample of fewer than 100 observations, but two other strings still recommend the wild bootstrap for such a sample:suggestDiagnosticsForProfile()adds it with the rationale "Bootstrap inference performs well in small samples." (R/intelligent_recommendations.R:183), and step 1 ofbuildDiagnosticDecisionTree()says "Emphasise wild bootstrap and rank permutation tests." (:575). Ininst/validation/wild-multiplier-size.Rthe wild bootstrap does not hold its level with skewed errors at 30, 50 and 100 observations: with centred exponential errors it rejects 0.082, 0.073 and 0.061 of the time at the 5% level with the Rademacher multiplier. Say what the warning says, or drop the claim. Recorded, not fixed, in 0.15.0; only the text changes. Done in 0.16.0: both say what the warning says. The rationale reads "In the package's validation study, at 30 to 100 observations, the wild bootstrap rejected a true null hypothesis 4.3% to 5.1% of the time at the 5% level with normal and t5 errors, but 6% to 8% of the time with skewed errors. ...", and step 1 "Add the wild bootstrap and rank permutation tests; with skewed errors ...", the caution of the warning (rwild_small_sample_caution()). The same tests are recommended and the same steps given (test-intelligent-recommendations.R). -
performFlignerKilleenTest()and a tie at the median. The statistic depends on rounding when the number of residuals is even: the two in the middle tie at exactly half their difference from the median, and which ranks first decides about1e-6of the statistic. Found on 2026-10-15 comparing a Gaussianglmwith the samelm. Average the ranks of values tied to within a tolerance. Changes a value, by that much. Done in 0.16.0: absolute deviations from the group medians closer than8 * eps * s, witheps = .Machine$double.epsandsthe largest absolute residual of the group (the smaller of the two for deviations of different groups), are ranked as tied (rfligner_rounding_ties()). The centring opens a gap of at mosteps * sbetween the two in the middle of a group. The scale is that of the group, so that a group far from zero does not widen the tolerance of the others, as one shifted by 1e8 did with the largest residual of all (decided after the review of #156). Onlm(y ~ x1 + x2)with four groups of 30 thelmgave 9.3670076662 and the Gaussianglm9.3670316854; both give 9.3670316854,identical(), as the matrix of the model context now checks (context_fligner_tolerance, 1e-4 before, 1e-10 now;identical()on Windows). In the identity harness of the model context (scripts/model-context-identity.R), 128 of the 351 calls of the test change, by 1.1e-6 to 4.4e-6 of the statistic, and the 91 of them whose cell has another fit of the model as its reference are nowidentical()to it; the other 19,628 calls areidentical()to main (test-fligner-killeen-ties.R,test-pass-b-reference.R). -
performFlignerKilleenTest()on residuals whose ties the fit broke. The tolerance of the item above is that of the centring. On an integer response regressed on a factor, residuals that are equal in exact arithmetic are computed equal by the Gaussianglmbut not by thelm, whose QR decomposition leaves them apart by more than the tolerance, which catches some of them only. In 20 fits ofy <- rpois(n, 3)on four equal groups the largest gap was 17 timeseps * max(abs(r))at 60 observations, 400 times at 600 and 6,900 times at 6,000, and 208 times at 60 withrpois(n, 300): the rounding of the fit is relative to the size of the response rather than of the residuals, and grows with the number of observations, so no fixed multiple ofeps * max(abs(r))covers it. The statistic of thelmthen stays off that of exact ties: fromset.seed(3)with 60 observations in groups of 15, 6.10, where theglmandstats::fligner.test()of the responses give 6.66 (4.49 before 0.16.0). Found in 0.16.0 with the item above. Done in 0.16.0, at the source (decided after the review of #156): the test ranks the residualssqrt(w) * (y - X b - o), computed observation by observation withX bsummed in the same order for every row (rfligner_residuals()), as the Gaussianglmcomputes them, then applies the tolerance of the item above; where neither changes a rank it passes the residuals of the fit on as before. Observations with the same values of the model's variables, weight and offset are given one residual, that of the first of them (rfligner_same_observations()), where their residuals agree to within 64 timesepsof the size of the numbers they are computed from, so that a term of the row position (seq_along(x),cumsum(x)) keeps them apart; this needsdata, or a model frame that holds the variables. It is done sincepoly()builds its basis with a QR decomposition of its own and can give them design rows that differ in their last bits (from the confirmation review of #156: on 70 observations repeating 30,y ~ poly(x, 3)gave 0.80190 wherey ~ x + I(x^2) + I(x^3)gives 0.91385; both now give 0.91385). On 480 fits oflm(y ~ g)with integer responses (rpois(n, 3),rpois(n, 300),1e4 + rpois(n, 3), four groups, balanced and unbalanced, 60 to 60,000 observations, all accepted: the smallest group has 6 observations) the statistic equals that of exact ties in all 480, where through the package it did in none before 0.16.0 and in 38 with the tolerance alone (at 99d446e). Onsleep,PlantGrowth,warpbreaks,InsectSpraysandiris[-c(50, 100, 150), ]it equalsstats::fligner.test()of the responses scaled to integers (sleep0.154 before, 0.213 now;warpbreaks3.44 and 3.90). On 3,000 continuous fits of 20 to 10,000 observations no two deviations that differ in exact arithmetic are tied, and thelmand theglmgive the same value, unless the residuals of a group are 1e8 times their spread: there the tolerance tied such deviations in 41 of 200 fits of 1,000 and 10,000 observations, and thelmand theglmdiffered in 4. Over a corpus of 55 calls the 20 in which no rank changes areidentical()to before; 14 of the 17 with repeated observations change, by up to 38%, and 21 of the 38 with continuous responses, by 7.9e-7 to 3.2e-5 of the statistic. - A Gaussian
glmwith a zero prior weight. The tests that read the Pearson residuals throughrpearson_residuals()refuse anlmwith a zero weight, whose Pearson residual is an artificial zero, but not the sameglm:rpearson_residuals()readsrprior_weights(), which returns no weights for aglm. The Koenker, White, Glejser, Spearman, GQ and wild bootstrap tests return a value for it, andperformWhiteTestBootstrap()stops with "Model residuals must be finite". Suggested fix: haverpearson_residuals()readrleast_squares_weights()instead ofrprior_weights(). Two related gaps:plotDiagnostics()readsrprior_weights()too, so a weighted Gaussianglmis not labelled "(weighted residuals)", andperformRESETTest()does not refuse a zero weight, even on anlm. Found on 2026-10-15 and recorded, not fixed, in 0.15.0, as decided that day. A fix changes what such fits return. Done in 0.16.0 as suggested: the 32 tests that returned a value, and the scatter diagnostic, refuse such aglmwith the message of thelm, andplotBeforeAfter()labels a weighted Gaussianglm.performRESETTest()keeps accepting a zero weight, on anlmand aglm: its two fits are weighted least squares, which drop the observation, and its F statistic and degrees of freedom are those of the fit to the positive weights (test-model-eligibility.R). Decided on 2026-10-17: RESET keeps accepting a zero weight, the documented exception on the Supported models page. - Found by the model-eligibility work (the Supported models page),
recorded, not fixed, as they change what a test computes rather than which
models it accepts. Those done in 0.16.0 are below; the others are under
0.17.0 and 0.18.0:
-
test_factory$run_test("szroeter", ...)always stops with "argument \"order_by\" is missing": the factory registersperformSzroeterTest(), which needs an ordering, where the registry name"szroeter"orders by the fitted values. Register the same function. An error becomes a value. Not "always":run_test()passes its...on, sotest_factory$run_test("szroeter", m, d, order_by = "x")givesperformSzroeterTest(m, d, order_by = "x"), and only a call withoutorder_bystops. Registering the function of the registry would refuse that argument, so a call that works would stop. Done in 0.16.0 instead as the factory treats a test that needs more than the model and the data, as"spatial_hetero"asks for its neighbour list: withoutorder_bythe entry refuses the call with a message that asks for it and shows how to give it, and withorder_byit returns what it returned. It orders by nothing of its own. No value changes; ordering by the fitted values whenorder_byis not given, as the registry does, would make the refusal a value, and is the author's decision. - The three tests that refused every
glm,performWhiteTestStreaming(),performHarrisonMcCabeTest()andperformVarianceFormTest(), refused a Gaussianglmwith the identity link too, which is a least-squares fit the others accept. Done in 0.16.0. The author's decision was to accept it if the literature supports it, and it does: the Supported models page sets out the argument and the references. They accept it and give the value of thelm. The White streaming test's checks read the R-squared of the least-squares fit, and the other two read the prior weights. With it, the checks of every test refuse a near-perfect Gaussianglmas thelm, andfitWLS()refits a Gaussianglmas thelmit is and refuses any otherglm. Refusals became values. -
performWhiteTest()counted one degree of freedom too many when its auxiliary columns span the constant: it took the rank of the auxiliary design without the intercept, where the regression has one. With a factor undercontr.sum, as inlm(y ~ x1 + f, contrasts = list(f = "contr.sum"))with four levels, the squares of its columns sum to a constant, and the df was 9 wherelmtest::bptest()on the same terms gives 8, with the same statistic. The review of the model-eligibility work, which found it, reported 15 against 14 with nested Helmert contrasts and 24 against 23 on a design with an empty cell (the empty cell of the Supported models page, under treatment contrasts, gives the right 23). Done in 0.16.0, outside the model context (N2, decision 22 of its design): the degrees of freedom are the rank of the auxiliary design with its constant, minus one, and the columns the constant makes redundant are dropped, so that every route to White's test that gives a value gives that oflmtest::bptest()on the same terms; the streaming test stops on these designs, whose auxiliary columns are collinear with its intercept. Changes p-values; on a nearly collinear auxiliary design, such as that of a smooth term, the statistic too. - The review of N2 found three more things, decided by the author on
2026-10-21. The rank of
[1, Z]depended on where the regressors were located: withx = 1e4 + u, or a date written asYYYYMMDD, the square ofxwas taken for a combination of the intercept andxand dropped (27.41 on 4 degrees of freedom where the test of the same model at zero gives 27.43 on 5). An auxiliary regression with as many independent terms as observations fitted the squared residuals exactly, and the test returned the number of observations as its statistic. And a regressor named like a column the test generates,abesidea_sq, stopped the auxiliary regression, which was fitted from a formula on the names. Done in 0.16.0: the rank is decided on the auxiliary design of the centred regressors, the regression fitted on the columns as they are unless that fit would lose one of them, a saturated auxiliary regression refused (by the streaming test too; the runner falls back on White's test without cross products), and the regression fitted withlm.fit(). Changes p-values on designs with a regressor far from zero, and refuses calls that returned a value. - The review of the follow-up of N2 found that the refusal of a
saturated auxiliary regression missed an exact fit with fewer terms than
observations. On a paired design,
lm(y ~ treat + subject)with two observations of each of 30 subjects, the residuals of a subject sum to zero, so their squares are equal and a combination of the subject dummies: every route to White's test gave 60 on 30 degrees of freedom (p = 0.0009), and the runner rejected at the 5% level in 200 of 200 homoscedastic draws. With a factor of 10 levels of two observations besidex1, 20 observations, the refusal of the saturated regression with the products let the runner fall back on White's test without them, an exact fit too: 20 on 11 (p = 0.045). Done in 0.16.0: White's test refuses an auxiliary regression whose residual sum of squares is at mostsqrt(.Machine$double.eps)of the total, with the message of the saturated one, on every route; when the regressors alone fit the squared residuals exactly, the runner takes no fallback, and stops. Refuses calls that returned a value. - The refusal of White's exact fit covers that fit only: one step away
from it, and on any fit with fixed effects for small groups, White's,
Koenker's and the Breusch-Pagan tests over-reject on homoscedastic data.
In 2,000 draws of
lm(y ~ x + subject)they rejected at the 5% level in 51%, 57% and 40% with 30 subjects of three observations, and in 24%, 24% and 25% with 100 subjects of eight; at a fixed number of observations per subject the size goes to 1 as the subjects grow (inst/validation/fixed-effects-size.R). The excess comes from the subject dummies in the auxiliary regression (inst/validation/fixed-effects-dummies.R). Done in 0.16.0, decided by the author on 2026-10-23: the three tests warn when a term of the model that holds a factor has more levels than observations per level,L^2 > n, counting the cells of an interaction, with their values unchanged; on a balanced design the rule isG <= T, which misclassified 1 of 69 designs of the study for the three tests together. The routes that compute the same statistics warn too, and the White bootstrap, the wild bootstrap andbootstrapTestStatistic(), which held their level on those designs, do not (inst/validation/fixed-effects-variants-size.R). A warning, not a correction: testing against the regressors without the dummies, and a test built for fixed effects, are for after 1.0.0. - Three more tests ignore an offset, besides the studentized statistic
above, so that on
lm(y ~ x1 + x2, offset = o), or with+ offset(o), they do not give the value of the same model fitted toy - o:performRESETTest()refits the augmented model without it (R/performRESETTest.R:50), with either form of offset;performGQTest()refits each segment of an unweighted fit from the formula, which drops an offset given as an argument (R/performGQTest.R:132), and those of a weighted fit withlm.wfit()and no offset at all; andperformQuantileRegressionTest()fitsquantreg::rq(), which ignores any. Witho = 0.8 u,ua regressor outside the model, RESET gave a negative F where the model ofy - ogives 4.49 (p = 0.013), GQ 1.63 against 2.38 and the quantile test 0.90 against 3.26 (found in the review of the model-eligibility work). Decided on 2026-10-17: until they are corrected, these three and the studentized statistic refuse exactly the offsets they leave out (RESET, the quantile test and the studentized statistic any offset; GQ an offset given as an argument, and any offset on a weighted fit), with classheteroTests_offset_error, and the runners do not fall back on another test for it (rrefuse_offset()). The offset fix of 0.16.0 makes them account for the offset, asrweighted_refit()and the bootstraps do, which turns these refusals back into values; announce it in bold. Done in 0.16.0, by step 2 of the model context (below): GQ and the quantile test give the value of the model ofy - o(2.38 and 3.26), and RESET the nested F test of the model with its offset (decision 19), F = 0.112 (p = 0.894) on that fit, where RESET of the model ofy - o, another test, gives 4.49;rrefuse_offset()and the classheteroTests_offset_errorare gone.
-
- Found by the review of the Gaussian
glmwork (#136), recorded, not fixed (the other is under 0.17.0):-
performHarrisonMcCabeTest()is not consistent with itself on a fit whose rank was decided with a tolerance below1e-7: a Gaussianglm, asglm.fit()decides it withmin(1e-7, epsilon / 1000),1e-11by default, orlm(..., tol = 1e-11). On a nearly collinear design, such asx3 = x1 + 1e-9 * rnorm(n), its residuals are those of the fit of rank 4, and its null distribution is computed fromqr()of the design at the default1e-7(R/performHarrisonMcCabeTest.R:239), of rank 3. Take the rank, and the columns, from the fit. Changes a p-value. Done in 0.16.0 by the third step of the model context (decision 21): the test takes the rank of the fit, at its tolerance, and theglmabove gives p 0.002273 where it gave 0.002288.
-
- Defects found during the 0.12.0 and 0.13.0 work, left for this
release so that 0.14.0 changes no value. Each fix that changes a value is
announced in bold in
NEWS.md. Those done are below; the others are under 0.17.0 and 0.18.0.-
fitWLS()keeps the original weights in the model frame of its result, soperformStudentizedBPTest()on afitWLS()fit reads stale weights: 14.81, against 1.73 for the equivalentlm(weights = estimated)fit. Done in 0.15.0 (decided 2026-10-16, in #129):.studentized_bp_test()reads the weights of the fit, throughrleast_squares_weights(), rather than those of its model frame, which is unchanged. It changes the values ofperformStudentizedBPTest(),performBPTestRobust()and the registered"student_bp"on afitWLS()fit only, and so the Breusch-Pagan entry ofrunAdvancedDiagnostics()on one. -
.studentized_bp_test(), behindperformStudentizedBPTest(),performBPTestRobust(studentize = TRUE)and the registered"student_bp", drops every offset, given as an argument or written in the formula: it refits the model withlm.wfit()and no offset. Onlm(y ~ x + z, offset = o)in the design oftests/testthat/test-fitwls-reestimation.Rit gives 19.91 whereperformKoenkerTest()gives 18.79. Found in the review of #129. Changes a value. Since 2026-10-17 (decided that day) it refuses a model with an offset until it is fixed, with the three tests of the item on offsets above; the fix turns the refusal back into a value. Done in 0.16.0, by step 2 of the model context: its refit carries the offset, and it gives Koenker's 18.79. -
performWildBootstrapTest()andperformWhiteTestBootstrap()accepted a non-Gaussianglmand returned a meaningless result. Done in 0.15.0 with the input validation: they refuse it, and read the prior weights of a Gaussianglm. - The caches of
runHeteroTests(use_cache = TRUE)(about 33 KB per new model) andcachedTest()(about 25 KB per call) grow without bound, as the validation cache did before #97. No value changes. Done in 0.16.0, with the contract decided on 2026-10-21: each cache holds at mostgetOption("heteroTests.cache_size", 100)results, the validation cachegetOption("heteroTests.validation_cache_size", 1000), each dropping the entry used least recently, and0turns a cache off; an entry keeps no copy of the model. The key holds everything that determines a result, so two fits with the same coefficients and residuals no longer share one (that changed values, announced in bold), and a test that draws random numbers is never cached. - About 0.8 KB per test call on a new model still persists after the
validation cache is cleared, as seen in long simulations. No value changes.
Done in 0.16.0: they were the keys of the caches of
cachedTest()andrunHeteroTests(), the names of variables of an environment, which R never frees (about 0.2 to 0.3 KB each, three per call ofrunHeteroTests()). The keys are a character vector now, as those of the validation cache since #97, and a cleared cache leaves nothing. - On a rank-deficient fit (
z2 = 2 * z, the design intests/testthat/test-koenker-studentized.R) bothperformKoenkerTest()andperformStudentizedBPTest()refuse with "Model coefficients must be finite…", wherelmtest::bptest()returns a statistic (20.87, df 2). Decide whether to compute on the estimable subspace or keep the refusal with a clearer message. Done in 0.16.0 on the estimable subspace, for every test, as the brief of the model-eligibility work asked: every test that accepts the model without the aliased columns accepts the rank-deficient one, and gives its statistic and p-value (to the last digit on the four fits oftest-model-eligibility.R); Koenker's is 20.87 on 2 df. No test needs every coefficient, so none refuses; the quantile regression test fitsquantreg::rq()to the columns that were not aliased.validateTestInputs(), functions defined outside the package that a user registers withtest_factory, andgenerateHeteroRecommendations()still refuse it, with a message that names the aliased coefficients. The power form ofperformVarianceFormTest()takes the logarithms of every column, the aliased ones included, so its value is not that of the reduced model, and does not depend on the order of the terms (kept so, decided in the review). Decided on 2026-10-17: computed on the estimable part of the design.
-
- The model context. One internal, validated object per call, built
from the stored fit, from which the tests read the response, the design, the
residuals, the weights, the offset and the rank, and which matches the rows of
datato the observations of the fit with the guard of 0.15.0 before anything is read from them: named data in any order, numbered data in fit order. The design, the contract, the test matrix and the decisions of 2026-10-16 and 2026-10-18 are on the site,docs/design/model-context.md. Each step is one pull request, checked byscripts/model-context-identity.Ragainstmain(every callidentical()but the cells of the defects the step closes) and pinned bytests/testthat/test-model-context.R, whosecontext_migratedlists the functions moved so far. Steps 1 to 7 are done in 0.16.0; steps 8 to 11 are under 0.17.0.- 1. The context (
R/model_context.R) and the guard split in two (rdata_fit_rows(),rcheck_rows_at());performWhiteTest(),performBPTest()andperformBreuschPaganTest(),performKoenkerTest(),performRankPermutationTest()andperformHighDimensionalTest()built on it, and through them the base statistic ofperformWhiteTestRobust()andperformBPTestRobust(studentize = FALSE). Closes B5, B12 (direct calls and the runner), B14, B15, N7, R4/N9 and R7 for these, and L1 and L2. The refusals of the context are final in the runners (decision 6).performWhiteTestBootstrap()keeps the computation of 0.15.0 for its observed statistic and its replicates (rwhite_test_rebuilt_design());bootstrapTestStatistic()andperformWhiteTestRobust(bootstrap = TRUE)refuse a fit whose frame cannot be rebuilt fromdata. Done in 0.16.0. - 2. The offsets: the studentized Breusch-Pagan statistic, RESET (the
nested F test of the offset model, decision 19, with one evidence cell of
5000 replications in
offset-refits-size.R: it rejects 0.0492, inside the release gate), Goldfeld-Quandt on the stored design (decision 20) and the quantile test ony - offset;rrefuse_offset()goes. Closes B7, N1, N6 for RESET, R5, and for these four the classes step 1 closed for its five (B5, B12, B14, B15, L1, L2, N7, R4). RESET reads nothing fromdataand builds its context without it, keeping its check ofdatauntil step 6; the four refuse anmgcv::gam()fit with smooth terms until step 11. Done in 0.16.0. - 3. Glejser, Park, Szroeter with the registered
"szroeter", which reads its fitted values from the context, Harrison-McCabe (its rank at the fit's tolerance, decision 21), Hartley and the spatial test. Closes B14, L1, L2 and M1 for these, N4, and N17 for the five that read a column ofdata; B15 gave values already. The six test agamwith smooth terms as before. Done in 0.16.0.- Found in step 3 (N17): the residuals of an
mgcv::gam()fit carry no names, and the tests that read a column ofdatabeyond the model matchdatato them by position, so named data in another order than the fit give a wrong value without an error. Step 3 closes it for its five, and step 4 for the five group tests (Levene's, Brown-Forsythe's, Fligner-Killeen's, Bartlett's and O'Brien's); the panel tests have it until step 5, the three streaming tests until step 7 and the replicates ofperformWhiteTestBootstrap()until step 8.
- Found in step 3 (N17): the residuals of an
- 4. The five group tests; their copies of the alignment go, and they
leave out the observations whose group is missing, with the warning of
before. Closes the character-named fit with its tibble and the complete
rows renumbered (B14), R7, L1, L2 and M1 for
group, and N17 for these; B12 and B15 gave values already. A fit withmodel = FALSEwhose data are gone given a shuffled tibble, and an edited fit (M8), from which they computed a value, are refused, as by the first fifteen. The five test agamwith smooth terms as before. Done in 0.16.0. - 5. The panel tests, the scatter diagnostic and
runPanelTests(), withidandtimeread at the confirmed rows; repeated(id, time)pairs refused (decision 13). Closes B3, B4, B16, R2, and N17 for these; a fit withmodel = FALSEwhose data are gone given a shuffled tibble, and an edited fit (M8), from which they computed a value, are refused, as by the first twenty. On a fit that dropped some rows of some units Pesaran's test refused the unbalanced panel (N12, the item above), and since the pull request of N12 computes it from the periods each pair of units shares. A fit withna.action = na.excludeis read as before, by position, until step 9. Done in 0.16.0. Step 9 reads it as the same fit withna.omit. - 6. The tests computed from the model alone, Cook-Weisberg with
var_formula,performVarianceFormTest()andfitWLS(data, var_formula), and the check ofdataRESET keeps until then (rcheck_data_argument()). Closes B9, R3, N6 for these. The nine tests computed from the model alone andfitWLS()build the context, which refuses a stalemodel = FALSEframe (N6), a multivariatelm, an edited fit (M8; notfitWLS()), and a fit whose data are gone unlessdatagives its response and fitted values. They read nothing fromdata, and match it through the guard as every test does, RESET andfitWLS()withoutvar_formulatoo (decision 31, decided by the author on 2026-10-26: one rule for every test, which ends decision 24): they refuse numbered data in another order than the fit, rows in front of those of the fit and repeated row names, from which they computed the value of the fit, and take the complete rows of a fit that dropped rows numbered again, which the check of 0.15.0 refused. A second formula (var_formula,against) is evaluated on the rows the alignment confirmed (B9), and its variables come fromdataor the model frame, never from the workspace (R3, decision 9). Done in 0.16.0. - 7. The three streaming tests, chunk by chunk from the stored design.
Closes B6, and B5, B14, L1, L2, N7, N17 and R4 for these; B12 and B15
gave values already. An edited fit (M8) and a fit with
model = FALSEwhose data are gone given a shuffled tibble, from which they computed a value, are refused, as by the tests built on the context. Agamwith smooth terms is tested as before, on the design of the variables of its formula (the item of the review of N2, under 0.17.0). Done in 0.16.0.
- 1. The context (
-
print()andsummary()for the results ofrunHeteroTests(). An addition, outside "no API change": exported S3 methodsprint()andsummary()forhetero_test_suiteandhetero_grouped_suite, andprint()andcoef()for their summaries, approved by the author on 2026-10-21, with the significance codes ofsummary.lm(). A suite printed the list, the fitted model among its attributes, and had the summary of a list. Done in 0.16.0 (?summary.hetero_test_suite): a table with one row per test, notes for the tests that fell back, were refused or failed, andcoef(summary(s))ascoef(summary(fit))is for anlm. With it a test the runner runs that reported"model"as itsdata.name, White's test and four others, reports the formula of the model, and a suite whose model the runner fitted itself says so inattr(, "model_origin"); no statistic changes. - The quantile regression test on a model with one regressor. On
lm(y ~ x1)with 60 Gaussian observations,runHeteroTests(m, d, tests = "quantile_regression")stops with "subscript out of bounds" and returns the rank permutation test, its fallback, under the name asked for; with two regressors or more, factors, an interaction or apoly()term it gives a value. Found by the review of the print and summary methods on 2026-10-22 and reproduced on main, so not caused by them. Explain it, and fix it or refuse such a model with a clear message. Explained and fixed in 0.16.0. The quantile regressions are fitted with the design of the fit as one matrix column,.design, and the formula names a coefficient after that column followed by the name of its column,.designx1, except when the matrix has one column, whose coefficient is called.designalone. The test removed.designfrom the names, so the slope of a model with one regressor had an empty name, and reading it by name forestimatestopped. The statistic was already computed, and right: it isanova()ofquantreg::rq(y ~ x1), whichtest-quantile-joint-reference.Rnow checks. The coefficients are named after the columns of the design, so such a model gives its value: onlm(stations ~ mag, data = quakes), F = 57.57 on 1 and 1999 degrees of freedom (p = 5.0e-14), where the test stopped andrunHeteroTests()returned the rank permutation test. So does any design with one column beside the intercept once aliased columns are dropped, such as a factor of two levels; a design with two columns or more beside it gives the value it gave,identical()(announced in bold). An exploratory size check, not to evidence grade (2,000 replications per cell, the design offull-sweep-size-power.Rwith one regressor), puts it below its level, as the two-regressor design is undert5errors: 0.029 and 0.040 at 60 and 150 Gaussian observations, 0.021 and 0.032 undert5errors. A model without an intercept is still refused byquantreg, which requires one, with its own messages ("Intercept required in common tau testing" with two regressors, "argument is of length zero" with one), as before; a refusal of its own would say why.
0.17.0: the model context, part 2 (increments 8–11) (done)¶
Moved from 0.16.0 on 2026-10-26: the last four steps of the model context,
and the defects found with it that those steps, or a pull request of their
own, settle. Among those are N3, White's streaming test on collinear terms
(decision 22 of the design), and the threshold of the streaming tests on
squared residuals that do not vary. Done in #164 (N3 and the threshold),
in #165 and #167 (step 8), #168 (step 9), #169 (step 10) and #171 (step 11).
The release also fixes CRAN's additional check of 0.16.0 with Intel MKL
(#172, under 0.11.x). No export, argument or default changes; hardhat
joins the suggested packages. What the steps found and did not settle moved
to 0.18.0 at the release (Left from steps 8 to 10 of the model context).
- The model context, part 2. Steps 8 to 11 of the plan on the site,
docs/design/model-context.md, each one pull request frommain, checked byscripts/model-context-identity.Rand pinned bytests/testthat/test-model-context.R, as steps 1 to 7 were (the model context, under 0.16.0). Done in 0.17.0: every step of the plan is done.- 8. The resampling tests and
bootstrapTestStatistic(), replicates from the stored design: the observed statistic and the replicates ofperformWhiteTestBootstrap()move together, andrwhite_test_rebuilt_design()goes;bootstrapTestStatistic()andperformWhiteTestRobust(bootstrap = TRUE)no longer need to refuse a fit whose frame cannot be rebuilt fromdata. Closes B12 for replicates, N8, and N17 for the replicates of the White bootstrap. Done in 0.17.0 (#165, and its review in #167): the White and wild bootstraps,bootstrapTestStatistic()and the robust White and Breusch-Pagan tests build the context, and each replicate is refitted by least squares on the stored design, drawing the random numbers it drew before, in the same order. Closes too B5, B14, B15, L1, L2 and N7 for these (cut(x, quantile(x)), refused or stopped, gives the value of the factor computed beforehand), and pairs resampling holds the design of the fit. A Gaussianglmis bootstrapped as thelmit is; agamwith smooth terms is refused by the White bootstrap, whose refit stopped on it, and bybootstrapTestStatistic(), which refuses everygam, as before. An edited fit (M8) and amodel = FALSEfit whose data are gone given a shuffled tibble ("frame"), from which the White and wild bootstraps computed a value, are refused. Its review: pairs resampling keeps the contrasts of the design and codes a factor or a character regressor a replicate leaves a value out of as a refit of the formula does; afitWLS()fit of agamwith smooth terms, whose terms do not rebuild its design, is refused (M8); andbootstrapTestStatistic()refuses a multivariatelmwith M7. - 9.
na.exclude, the second step of 0.17.0 (decision 16). Closes B13. Done in 0.17.0 (#168): a fit withna.action = na.excludedrops the rowsna.omitdrops, and padsresiduals(),fitted(),weights()andhatvalues()withNAat them. The checks of the model read one residual per observation of the fit, and every test built on the context reads the vectors of the fit without the padding, so such a fit gives what the same fit withna.omitgives on the same data: the same statistic, p-value, refusal and warnings, and for the resampling tests the same draws. The tests and the runners refused it ("Model residuals must be finite"), RESET stopped on it ("NA/NaN/Inf in 'x'"), and the panel tests and the scatter diagnostic read its padded residuals against the rows ofdataby position, which gaveNA, or a value computed with theNA, and refused data in another order and an unbalanced panel of the rows ofdata; that reading goes. An output with one value per row is padded asnaresid()pads it: the estimated variancesfitWLS()attaches to its fit.rvalidate_against_reference()gives lmtest and car the same fit read asna.omit, on whose padded weights lmtest stopped. From the review of the step: anmgcv::gam()fit withna.exclude, whose designmodel.matrix.gam()pads withNArows, which the tests refused for parts of different lengths, is read through the design of its twin; and the functions outside the context that the checks now let such a fit reach read it without the padding too (below). The twin rule is decision 32 of the design, decided by the author on 2026-10-28. - 10. The runners and the clean-up: a grouped run of a
fitWLS()fit refused (decision 10), workflows withdata;prepare_model_data_for_test(),rdata_or_model_frame(),rcheck_data_rows()andrcheck_rows_after_cleaning()go, which no test calls since step 8. The assertions oftests/testthat/test-data-alignment-guard.Rthat callprepare_model_data_for_test()(the tibble renumbered by the rows it dropped) andrcheck_data_rows()(no column that can be compared) exercise these dead internals, and go with them. Closes B11, N5. With the workflows withdata(N5):recommendRemediationStrategies()andgenerateHeteroRecommendations(), andgenerics::augment()of the results of the runner, should match the rows ofdatato the observations of the fit, as the tests do, where since step 9 they refuse data of another number of rows than the observations (or, foraugment(), than the fitted values) with a message that names both. Done in 0.17.0 (#169):runHeteroTests()refuses a grouped run of afitWLS()fit, whose refit within each group left out the estimated weights, with the class of the model context, before any group is refitted; a grouped run of a weightedlmis unchanged. The runners map the data a workflow was fitted to onto the variables of its engine by the workflow's own preprocessing (hardhat::forge(), a suggested package), and the outcome of a parsnip fit offit_xy()onto..y, confirm the rows by the alignment of the context, and test the model frame of the engine's fit, the value withoutdata; data in another order than the workflow was fitted in, and data that cannot be mapped, are refused with a message that says why. The recommendations andaugment()match the rows ofdatathrough the alignment of the context: named data in any order, numbered data in the order of the fit, and, for the recommendations, the full data of a fit that dropped rows, give the value of the rows of the fit in its order; what the alignment refuses is refused. The four internals go; the assertions on them that a public function still makes are made on that function. The two choices the first version left open were decided by the author on its review (decisions 34 and 35 of the design):augment()binds the rows ofdataas broom does, in their order, the rows the fit used on a fit withna.omitand every row withNAat the dropped ones on a fit withna.exclude; and a preprocessing that transforms the outcome maps it to..y. From the review:augment()maps the data of a workflow or of afit_xy()fit as the runners do; the refusals of data that lack the variables of the model name the function and say what to pass; a recipe step that drops rows at the fit only (step_naomit()) is matched when its rows can be confirmed, and refused with a message that names such a step otherwise (step_filter()); the case weights of a workflow are compared, as the weights of a fit are for the tests; the report profiles the data as given, and keeps the groups of grouped data; and the refusals of a model the context refuses by kind, and of data that lack the variables of the model, are announced in bold. From its second review:augment()traces the rows a workflow's preprocessing maps to the rows ofdata, and refuses the data of a preprocessing that reorders them (step_arrange(), whose rows it bound the values of other rows); a step skipped on new data that changed the outcome (step_log(y, skip = TRUE)) is named in the refusal; the case weights are compared on the rows a recipe keeps of new data; the results keep whataugment()needs of the workflow, not the workflow (2 KB a result, where the workflow added 1,272 KB to three results on 5,000 rows); and the identity harness compares the attributes of the results of the runners and checks each value ofaugment()on the tidy block row by row. From its third review: the data of afit_xy()fit with case weights, whose column the fit does not record, are refused; a refusal names only the skipped steps without which the rows are not confirmed; and the refusal byaugment()of a step that makes a row from other rows, whose values 0.16.0 bound right, is announced. Decided by the author on its fourth review (decision 36): the blueprint is kept as the workflow keeps it, a recipe step with the environment it was written in, so that a saved result carries what that environment holds, as saving the workflow does (51,245 KB for a recipe written in a function whose frame held 50 MB, wheremaingave 1,894 KB); the third review's rewriting of the expressions, which changed what some of them mean, is gone. - 11.
MASS::rlm()refused (decision 4). Closes R6, N10. Settle with it whether a penalisedmgcv::gam()with smooth terms, a Gaussianglmby class, is an input the tests justify: its residuals are not orthogonal to its design, and the null distributions assume least squares. The five tests of step 1, the six of step 3, the five of step 4 and the three streaming tests of step 7 accept it as before; the four of step 2 refuse it until this step settles it for every test together. In two parts, both done in 0.17.0 (#171):-
MASS::rlm()refused. Closes R6, N10. Done in 0.17.0 (#171): the model context refuses such a fit, an M-estimate whose residuals are not least-squares residuals, with the class of its refusals, beforedatais matched (rrefuse_rlm()), so every test, its registered name,test_factoryentry and runner refuses it, the runners keeping the refusal final, and the message names thelm()fit of the formula to test instead. 37 tests tested it as anlm, and the studentized Breusch-Pagan statistic tested the least-squares refit of its design (9.04 where Koenker's test gave 8.74);fitWLS(), which fits its variance function to the residuals of the fit, refuses it too, and the help page offitRobust(), which returns such a fit, says so. From its review: a grouped run refuses it before any group is refitted, where with MASS attached it refitted each group as anrlmand returned its values;autoCompareRemediations()refuses it; and thelm()call the message suggests keeps thedata,weights,subset,na.action,offsetandcontrastsof the fit. From its second review, the call of afitRobust()result names the formula and the data the caller gave, where it named the variables offitRobust(), so that the suggested call works andupdate()refits it. - The penalised
mgcv::gam()with smooth terms (decision 23): which tests take it. Decided by the author on 2026-10-29 on the survey of the first part (decisions 37 and 38 of the design of the model context). Such a fit is tested on its stored design, the basis of its smooths, by every test that reads only its residuals, fitted values, columns ofdataor the columns of its design as auxiliary regressors, and the three streaming tests read that basis too, where they read the variables of the formula (the Breusch-Pagan test 23.995 on 10, where it gave 11.185 on 2, Koenker's 18.313 on 10 for 8.537 on 2, White's 46.05 on 60 for 10.61 on 5). Every test that fits it again by least squares, or whose null distribution is derived for least-squares residuals of its design, refuses it with the class of the model context, final in the runners: RESET, Goldfeld-Quandt, the quantile regression test and the studentized Breusch-Pagan statistic, as before, and now the White and wild bootstraps,bootstrapTestStatistic()and the robust bootstraps, the variance form test andfitWLS(), which refitted it without its penalty, and the Harrison-McCabe and Szroeter tests; the runner no longer puts White's test under the namewild_bootstrap. Agamthat is the least-squares fit of its design, without smooth terms or with fixed ones, is taken by every test as thelm()of that design, the refitting and resampling tests included, where a fixed smooth was refused as a penalised one andbootstrapTestStatistic()and the wild bootstrap refused or stopped on everygam. From the second review: the check of a least-squaresgamis drawn on the residuals of the refit of its design, plus the rounding of the response and the offset, not on the spread of the response, which an offset or a trend inflates, so that how the model is written no longer decides it; agamwithout a penalised term that fails it is not called penalised; and a refusal of the context in a group stops a grouped run (decision 6). Done in 0.17.0 (#171).
-
- 8. The resampling tests and
- Found by the model-eligibility work (the Supported models page),
recorded, not fixed, as they change what a test computes rather than which
models it accepts. Those below are settled by the steps above or with them;
the others are under 0.16.0 and 0.18.0. Done in 0.17.0, but for the two
found by the reviews of step 10 on
augment()of a workflow, which are not settled and moved to 0.18.0 at the release (Left from steps 8 to 10 of the model context):-
performWhiteTestStreaming()stops with "Auxiliary regression became rank deficient" when terms of its auxiliary regression are collinear, as the squares of dummy variables are, on any model with a factor, whereperformWhiteTest()drops them. Drop the collinear terms from the accumulated cross-products, as the exact test does. Changes a value (an error becomes one). Done in the pull request of N3 (#164): the three streaming tests reduce each chunk of the auxiliary design of the regressors centred at their means by a QR decomposition stacked on the factor of the chunks before it, rather than accumulating its cross-products, and decide its rank asperformWhiteTest()does,qr()of the intercept and the centred auxiliary design at its tolerance, in the order of the columns, on that factor, which has the norms and inner products of those columns. White's streaming test drops the collinear terms and gives the degrees of freedom and the value ofperformWhiteTest(): 17.83 on 8 onlm(y ~ x1 + group), where it stopped. The pivoted Cholesky decomposition of the centred cross-products (#151), used on the accepted path as well, counted one term too many (31 for 30) on 30 subjects of two observations and one of three, from the rounding of the centring of cross-products whose columns are far from their means, and 54 for 60 on the basis of a smoothgam, where the factor givesperformWhiteTest()'s 60 and its value. The cross-products of a regressor 100 standard deviations from zero no longer look collinear (White's test), nor those of one 1,000 standard deviations from zero beside the intercept (the Breusch-Pagan and Koenker tests); the values of the three that change, change in their last bits, or to the value of the exact test where the normal equations lost more digits. Found with it: in more than one chunk, on a regressorabesidea_sq, whose square White's test namesa_sqtoo, the columns of each chunk were matched by name and White's streaming test gave 27.76 (chunks of 7) whereperformWhiteTest()gives 27.64; every chunk has the columns of the stored design, taken in their order. The exact White, Breusch-Pagan and Koenker tests, andrunHeteroTests()with its defaultchunk_threshold_mb = 100, switched to the streaming ones above 100 MB, so that such a model got a fallback under the namewhiteon large data. Done in 0.16.0, decided by the author on 2026-10-22: the streaming tests run only when asked for, by calling them or by givingrunHeteroTests()a finitechunk_threshold_mb, whose default isInf; the three stay exported, not deprecated. The rest is N3 of the design of the model context, a pull request of its own (decision 22). -
performWhiteTestStreaming()on anmgcv::gam()fit with smooth terms tests the design ofterms(gam), the parametric terms with the variables of the smooths as linear terms, not the design of the fit: ongam(y ~ s(x1) + x2)it gives 33.31 on 5 degrees of freedom whereperformWhiteTest()gives 83.29 on 59. Found by the review of N2. The step of the model context that builds the streaming tests on the stored design (step 7), or the one that settles whether the tests take a penalisedgam(step 11), settles it. Changes a value, or refuses one. Step 7 keeps the design of the variables of the formula, built from the model frame, so that the values are those of before, and leaves the question to step 11, as decision 23 provides for every test outside step 2 (the review of step 7 agreed): on the stored design the Breusch-Pagan and Koenker streaming tests would give the values of the exact tests, and White's would refuse the collinear squares of the basis of the smooths (N3). Since the pull request of N3 White's would give the value ofperformWhiteTest()there too: ongam(y ~ s(x1) + x2)in the design ofcontract_data(), 46.05 on 60 degrees of freedom on the basis, asperformWhiteTest()gives, where on the variables of the formula, which it keeps, it gives 10.61 on 5, as before (to the last bits). Done in step 11 (#171, decision 37): the three streaming tests read the basis, chunk by chunk, and give the values of the exact tests, 46.05 on 60 here. - The three streaming tests refuse squared residuals that do not vary,
"requires variation in squared residuals", by an absolute threshold:
chunked_regression_r2()compares their total sum of squares with.Machine$double.eps, whatever their scale. Residuals of about1e-5are refused: withset.seed(11); n <- 120; d <- data.frame(x1 = runif(n, 0.5, 2), x2 = rnorm(n)); d$y <- 1e-5 * rnorm(n, sd = d$x1); m <- lm(y ~ x1 + x2, data = d),performWhiteTestStreaming(m, d),performBPTestStreaming(m, d)andperformKoenkerTestStreaming(m, d)stop, whereperformWhiteTest()gives 12.787 on 5 degrees of freedom,performBPTest()12.997 andperformKoenkerTest()10.289; the same data times1e5stream to 12.787. Found in step 7 of the model context, which leaves it as it was. Compare the total with the scale of the squared residuals. Errors become values. Done with N3 (#164): the exact tests decide constancy without a scale,performKoenkerTest()refusing squared residuals that are all equal and the R-squared oflm()not depending on the scale of the response; the streaming tests take the squared residuals for constant when their total sum of squares about their mean is at most.Machine$double.epsof their sum of squares, when they agree to about eight digits, and give 12.787, 12.997 and 10.289 on those data. Squared residuals equal but for rounding (residuals of +1 and -1 orthogonal to the regressors) are refused at any scale and in chunks of any size, where in chunks of 40 or more they gave a value from the rounding at a scale of 1 (Koenker's 40, the number of observations) and refused it at others. -
runHeteroTests(<workflow>, data)refuses the data the workflow was fitted to, because the engine's fit names its response..y; withoutdatait uses the model frame of that fit (0.16.0), and the refusal says to passdata = NULL. Map the columns instead. An error becomes a value. Done in step 10 (#169, above): the data are mapped by the workflow's preprocessing, and give the value withoutdata. - A workflow whose preprocessing transforms the outcome, as a recipe
with
step_log()of the outcome, maps the outcome ofdatato its transformed values, which are the response..yof the engine's fit; the first version of step 10 refused such data, saying that the preprocessing transforms the outcome. Decide whether the transformed outcome of the preprocessing is taken as the response, which gives the value withoutdata, or the outcome column ofdatais required to be..y, which refuses them. Decided by the author on the review of step 10 (decision 35): the transformed outcome is taken, for a recipe and for a formula preprocessorlog(y) ~ x + zalike, and the value is that withoutdata. Done in step 10 (#169). -
MASS::rlm(), decision 4 of the model-context design: 37 tests accept it as anlmand test its M-estimation residuals as least-squares ones; the studentized Breusch-Pagan statistic ofperformStudentizedBPTest()andperformBPTestRobust()refits its design by least squares and tests the least-squares fit instead (9.04 against Koenker's 8.74 on the residuals of the M-estimate, in the design of the Supported models page); andperformQuantileRegressionTest()refuses it as a weighted fit, because its case weights are all 1. Keep accepting it, or refuse it. Deferred on 2026-10-17 to the work on the model context; accepted as before until then. Done in step 11 (#171, above): refused by every test, with the class of the model context, the runners keeping the refusal final.
-
- Found by the review of the Gaussian
glmwork (#136), recorded, not fixed (the other is under 0.16.0), and settled by step 8, which closes N8:bootstrapTestStatistic()refuses everyglm, because it refits each replicate by least squares, which would drop a family and a link. A Gaussianglmwith the identity link is a least-squares fit (the Supported models page), so the same justification lets it accept one and bootstrap thelm. A refusal becomes a value. Done in step 8 (#165): its replicates are those of the weightedlm, to1e-8, with the same p-value; anotherglm, asurvey::svyglm()fit and anmgcv::gam()fit are refused, as before.
- Found by step 8 of the model context, recorded, not fixed, as each
changes what a call returns. Done in 0.17.0 for the wild bootstrap and for
the
fitWLS()fit of a penalisedgam; thefitWLS()fit of agamwhose smooths are all fixed, and the tolerance of the replicates ofbootstrapTestStatistic(), are not settled and moved to 0.18.0 at the release (Left from steps 8 to 10 of the model context):-
performWildBootstrapTest()stops on everymgcv::gam()fit, with or without smooth terms ("lm object does not have a proper 'qr' component"): it reads the leverages withhatvalues(), which agamdoes not give. Before and after step 8 alike. Read the leverages of the stored design, or refuse thegamwith a message of its own. An error becomes a value, or a message. Done in step 11 (#171): the wild bootstrap reads the leverages of agamfrom its design, so agamthat is the least-squares fit of its design gives the value of thelm()of it, and refuses a penalisedgamwith a message of its own, as a fit each replicate fits again by least squares. -
fitWLS()of anmgcv::gam()fit with smooth terms returns anlmwhose terms do not rebuild its design:model.matrix()gives a column for each variable of the formula, for the coefficients of the basis of the smooths. The tests built on the model context stopped on it with "(subscript) logical subscript too long"; before step 8 the White and wild bootstraps returned values on the design of the variables. Since the review of step 8 the context refuses it, as a fit whose design does not reproduce its residuals (M8). Still to settle with step 11: refuse such a fit infitWLS()itself, or keep its basis there, so that it can be tested. Done in step 11 (#171) for a penalisedgam, whichfitWLS()refuses itself, as it would refit it without its penalty. Agamwhose smooths are all fixed, the least-squares fit of its design, is refitted, and itslmis refused by the tests as since step 8 (moved to 0.18.0).
-
- Found by step 9 of the model context and its review: functions
that are not built on the context and read the padded vectors of a fit with
na.action = na.excludeonce the checks of the model let it through, or keep one value per observation where the rule of step 9 pads them (decision 32 of the design). Done in 0.17.0, but for the positions of the influential observations, which are not settled and moved to 0.18.0 at the release (Left from steps 8 to 10 of the model context).-
analyzeMLResiduals()returnedrmse_reduction = NAon such a fit, andautoCompareRemediations()anNARMSE for the OLS and WLS fits (1.32 and 1.33 on the same fit withna.omit): each took the root mean square ofresid(), padded withNA. Done in step 9 (#168): the residuals of the observations of the fit, as the tests read them. -
generateHeteroRecommendations()stopped with "incompatible dimensions" (identifyVariancePatterns()correlated the padded absolute residuals with the columns of the model frame), andplotBubbleVariance(), and soplotDiagnosticSuite(), its deprecated name andgenerateDiagnosticReport(), with "replacement has 120 rows, data has 117";plotResidualsFitted()with and withoutenhanced,plotSpreadLevel(),plotResidualDensity(),plotResidualQQ(),plotBeforeAfter()andplotResidualsFittedEnhanced()drew theNA, with a warning of ggplot2, "Removed 3 rows containing non-finite values", that the same fit withna.omitdoes not give. Done in step 9 (#168): they read the residuals and fitted values of the observations, and draw what they draw on the same fit withna.omit. -
generics::augment()of the results of the runner stopped with "replacement has 120 rows, data has 117" withoutdataor given the rows of the fit. Done in step 9 (#168): it pads those rows with a row ofNAat each row the fit dropped, the result of the same fit withna.omitpadded; given the data of the fit, it binds the padded columns to its rows, as before; and data of another number of rows are refused with a message that says what it binds, for a fit withna.omittoo, where they stopped with the same error, or, when their number of rows was a multiple of the number of fitted values, had them recycled over the rows, bound to other observations (a new refusal on any fit, announced in bold). Open: the same fit withna.omitgiven the full data of the fit is refused (its columns have one value per observation), where the fit withna.excludebinds them; binding them by the row names of the observations would give that fit the rows of the other, a value where it is refused. Decided by the author on the review of step 10 (decision 34), and done there (#169): the rows ofdataare matched to the fit as the tests match them and bound as broom binds them, in the order ofdata: on a fit withna.omitthe rows the fit used, 117 of the 120 of its full data; on a fit withna.excludeevery row, withNAat the rows it dropped, the twin padded; and the full data shuffled under their own row names, which a fit withna.excludebound by position, get each row its own fitted value. -
recommendRemediationStrategies()correlated the residuals with the columns ofdatarow by row: given the full data of a fit that dropped rows, it stopped with "incompatible dimensions" on a fit withna.omit, and read the padded residuals of a fit withna.excludeby position. Done in step 9 (#168): it refuses data of another number of rows than the observations of the fit, naming both counts. Matching the rows ofdatato the observations, as the tests do, is left to step 10 (above), done there (#169): the full data give the value of the rows of the fit. - On an
mgcv::gam()fit withna.exclude, whosemodel.matrix()pads the design withNArows,performVIFDiagnostic()gaveNA, andcooks.distance()of thegamrecycled the padded residuals against the leverages, with a warning, soperformInfluenceDiagnostics()andrunDiagnostics()$influencegave other distances. Done in step 9 (#168): they read the fit as the same fit withna.omit, the distances padded asnaresid()pads them (on a weightedlm, whosecooks.distance()left the names of the padded positionsNA, they are named as the dropped rows, as on an unweighted one). - The record of the variance function that
fitWLS()attaches to its fit stays unpadded on a fit withna.exclude, one row per observation of the fit (decision 33, decided by the author on 2026-10-28); only the estimated variances,"variance_model", are padded. Done in step 9 (#168), as decided.
-
- Defects found during the 0.12.0 and 0.13.0 work, left for 0.16.0 so
that 0.14.0 changes no value. Each fix that changes a value is announced in
bold in
NEWS.md. Those below are settled by steps 8 and 10; the others are under 0.16.0 and 0.18.0. Done in 0.17.0.- The single tests and
performWhiteTestBootstrap()match a reordered tibble ordata.tableto the fit by position, throughprepare_model_data_for_test(). Since 0.15.0 they refuse such data instead of returning wrong values:rcheck_data_rows(), called byrdata_or_model_frame(), compares every column of the model frame, on the rows the row names pick, with the model frame: the columnsdataholds under their own names value by value, and the others (log(y),log(x),poly(),ns(),offset(o),(weights),(offset)) computed fromdatathrough thepredvarsof the terms and theweightsandoffsetexpressions of the call. A term that depends on the rows it is computed from (I(x - mean(x)),I(x > median(x)),rank(x)), found by computing it again on half the rows, is not compared when it differs, and neither is a column that names a variabledatalacks or whose computation fails; the compared columns must then leave no two rows of the fit that tie on them and differ in such a column computed from variables ofdata, and at least one column must be compared. The tests stop when a compared column differs, when such ties remain, when row names repeat, or when a fit withmodel = FALSErebuilds a frame whose response or fitted values are not the fit's;runHeteroTests()andrunDiagnostics()do not fall back on another test for it. The model context replaces this guard, so that named data in any order, and numbered data in fit order, give the value on the data of the fit; numbered data in another order stay refused (decision 3 of the design). Changes a value for such data. Since step 10 of the model context the guard is only its alignment step (rdata_fit_rows(),rcheck_rows_at()):prepare_model_data_for_test(),rdata_or_model_frame()andrcheck_data_rows()are gone. Its cost on a valid call grows with the columns it computes again: at 100,000 rows it takes about 5 ms onlm(y ~ x1 + x2 + g)and about 43 ms on a weighted fit withlog()andpoly()terms, against 50 to 70 ms for the Glejser and Bartlett tests and about 200 ms for Koenker's on main. The review of 0.15.0 measured it at about a third of the Glejser (100 ms) and Bartlett (120 ms) calls at that size, before factors were compared by their codes; the model context, which reads the design from the fit, does not need it. Done in 0.17.0: the replicates of the White bootstrap in step 8 (#165), and the guard reduced to its alignment step in step 10 (#169). Not covered by the guard:fitWLS(data, var_formula)andperformVarianceFormTest()withvar_formulaoragainstread the variables of the second formula throughrvariance_frame(), which matches by name and, when no name matches and the counts agree, by position, unchecked (a reordered tibble changes their value). Since the sixth step of the model context they read them at the rows the alignment confirmed, andrvariance_frame()is gone.- The tests that read nothing from
data(performSpearmanTest(),performHarveyTest(),performArchLMTest(),performMcLeodLiTest(),performCookWeisbergTest()withoutvar_formula,performNCVTest(),performSpreadLevelTest(),performDavidianCarrollTest(),performVarianceFormTest()without a second formula,performRESETTest()) accept reordered data and return the value of the fit. Since the sixth step of the model context they matchdataas every other test does (decision 31 of the design), and refuse such data. - The columns a test reads besides the model (
group,order_by,variable,id,time) are not compared. When every column of the model frame is compared, two rows that tie on all of them (response, regressors, weights, offset) can pass for each other, as with a binary or integer response; they then have the same design row, weight, offset and residual, so exchanging them moves only those other columns between identical observations. The review of the guard found that swapping such rows of an unweighted fit with no offset left the Bartlett, Glejser, Szroeter and GQ statistics unchanged. - A column computed from a vector of the workspace (
weights = wvec,offset = ovec,log(x + k)) is not compared; no test reads it fromdata, so it needs nothing more. - The guard refuses data it cannot confirm, even the data of the fit:
(i) when no column of the model frame can be compared, as with
lm(d$y ~ d$x); (ii) when the compared columns tie on rows of the fit that differ in a term that depends on the rows or in a term of variables ofdatathat cannot be computed, as with a discrete response and a row-dependent regressor on the rows anna.omitfit kept (lm(score ~ g + I(x > median(x)) + z2)), ory01 ~ myf(x)afterrm(myf)andns()afterdetach("package:splines"). The message names the columns; pass the full data the model was fitted on, with the rows it dropped, or the variables as columns ofdata. - A term that depends on the rows is recognised by computing it again on
the odd and even rows and on each half, so every row is left out of one
of them; one that gives the same values on all four by chance is
compared, and refused on the rows a
subset =orna.omitfit kept. - Data that reproduce every column of the model frame exactly but are
another data set, with another extra column the test reads (
gin T1b of the review), give a wrong value without an error, as in 0.14.0. performScatterDiagnostic()refuses a tibble in the order of the fit of a model fitted to data with character row names, which it cannot match by name. Since the fifth step of the model context it takes such data in the order of the fit.cut(x, quantile(x))in the formula makes Koenker's test stop with "factor has new levels", on main and with the guard alike. Since the first step of the model context the five tests built on it read the design of the fit and return a value; the others follow their steps.- The second review of the guard measured it at about 20 to 40% of a cheap test (Bartlett, Glejser) at 100,000 rows.
- A
model = FALSEfit whose data changed after the fit is refused by the tests that readdata, with or without it, but the tests that read only the model rebuild its frame too:performRESETTest()takes its response from that frame and stays wrong, as in 0.14.0. Since the second step of the model context RESET refuses it, and since the sixth the other tests computed from the model alone andfitWLS()do. bootstrapTestStatistic()andrbootstrap_test_statistic()keep their own check (0.13.0), and the panel tests andperformScatterDiagnostic()refuse data with row names of their own in another order instead of matching it by name. Since the fifth step of the model context the panel tests and the scatter diagnostic match it by name, and since the eighthbootstrapTestStatistic()matches it as the tests do, its refit of the formula ondatareplaced by the stored design.- The
(weights)column is compared with theweightsexpression of the call computed ondata. If thefitWLS()defect above is fixed by writing the estimated weights into(weights)(decision 11 of the model-context design) while the call still saysweights = w, every test givendataon afitWLS()fit of a weighted model would be refused; the model context, which takes the weights from the fit, avoids it, and this guard must then skip that column for such fits.
- A multivariate
lm,cbind(y, y2) ~ ..., fails with a coercion error instead of a clear refusal. No value changes. Since the first step of the model context the five tests built on it refuse it with "needs a model with one response"; the others follow their steps. Done in step 8 (#165, and its review in #167), whose resampling tests andbootstrapTestStatistic()were the last to meet the coercion error; since step 11 (#171) a grouped run stops with the refusal too.
- The single tests and
0.18.0: internals, dependencies and the defect backlog¶
Moved from 0.16.0 on 2026-10-26, with no API change: the internals and
dependencies planned for it, and the defects recorded before and during it
that the model context does not settle. Each fix that changes a value is
announced in bold in NEWS.md.
- The reverse-dependency job of
R-CMD-check.ymlhas never passed. On every weekly run since at least 2026-10-05, and on the manual runs on the 0.16.0 release branch, it stopped insetup-r-dependencieswith "Could not solve package dependencies" (pkgdepends), beforerevdepcheck::revdep_check()ran. It is not part of the release gate (release.yml). Done in #166: the job asked forany::revdepcheck, and revdepcheck is not on CRAN; it is installed from GitHub,r-lib/revdepcheck. The package has had no reverse dependencies on CRAN so far, so the job checks none, but it runs: on the manual run of the 0.17.0 release branch it passed, with no reverse dependency to check. - The DOI of Szroeter (1978) on the help page of
performSzroeterTest(). The page gave doi:10.2307/1913833, which Crossref resolves to Dagenais (1978), "The Computation of FIML Estimates as Iterative Generalized Least Squares Estimates…", in the same issue of Econometrica. Szroeter's paper is doi:10.2307/1913831 (Econometrica 46(6), 1311–1327). Fixed in the roxygen (R/additional_tests.R), the help page and its page on the site. In the same way,inst/CITATIONnamed Fligner's co-author "Thomas J. Killeen"; Crossref, and the article in JASA 71, give Timothy J. Killeen. The help pages give only the initials. Found by the R Journal article's additions (heteroTests-paper #6) and their review. -
man/is not what roxygen writes fromR/. Onmainafter 0.17.0 (a3709bc),devtools::document()with roxygen2 7.3.3, the version ofRoxygenNote, changes 36 files ofman/:- It deletes
ht_logging.Rdandrbootstrap_test_statistic.Rd, and writes 15 pages thatman/lacks. Among them arebootstrapTestStatistic.Rd, for the@rdname bootstrapTestStatisticofR/bootstrap_methods.R, andheteroTests_ggplot.Rd, a topic with no page today. - It rewrites 19 more. Some of the changes are in the text:
ht_log.Rdstill has the wording of September 2025, whichR/utils.Rhas since replaced. Others are in the form: the usage is wrapped one argument per line,\link{}replaces\code{\link{}}, and\code{}replaces the\file{}of paths.
So some pages were edited by hand, or written by another roxygen setup, and a help page can lag its source. That includes the pages of the CRAN tarball, which
R CMD buildtakes fromman/. Find which, makeman/the output ofdevtools::document(), review the help pages that change, and add a CI step that fails whendocument()changesman/. Found while fixing the DOI above, which was fixed in the roxygen and in the help page alone. Done in #180: the pages had been edited by hand, or written by hand under a roxygen header, asht_logging.Rdwas in #7, anddocument()had not been run on them since; neither the roxygen2 version nor the collation locale makes a difference. The edits the roxygen lacked were carried intoR/(the.recoverargument ofsafe_lm(), the arguments ofprint.htest_enhanced(), themodelofperformQuantileRegressionTest()), with the faults of the roxygen markdown they hid:`r ~ z`evaluated as inline R code,\%written as\\%, which drops the rest of the line, and a link toparallel::mclapply()that roxygen resolves tomcdummieson Windows. One sentence was dropped as no longer true ("Neither strategy accepts a glm", on the page ofbootstrapTestStatistic()), and the eight helpers documented ongenerateHeteroRecommendations.Rdname it as their page. The block that opensR/ggplot_extensions.Rdocuments no object and is@noRd, soheteroTests_ggplotgets no page. The 86 pages roxygen writes are now whatdocument()writes, and a step ofrun-checks.ymlfails whenroxygen2::roxygenise(), with the roxygen2 ofRoxygenNote, changesman/orNAMESPACE; the 52 it does not write are the next item. - [ ] 52 help pages andNAMESPACEare written by hand. roxygen2 does not overwrite a file without its "Generated by roxygen2" line, and 52 pages ofman/have none, among them those ofrunHeteroTests(),runDiagnostics(),performWhiteTest(),performBPTest(),performKoenkerTest()and most of the tests, andgenerateHeteroRecommendations.Rd. 45 of them are pages of a roxygen block inR/, which users do not read and the CI step above cannot see; the other 7 (test_results,diagnostic_data,hetero_test_suite,simulate_heteroand the methods ofHeteroDiagnostic) have no block. The page and the block have been kept up side by side. Given the roxygen line, the 45 pages are rewritten by +3,332 / -1,451 lines (git diff --numstat). Split into sentences, their rendered text holds 550 to 715 sentences of the page that the block lacks (the Supported models sections, the DOIs, aliases such asgenerate_benchmark_reportand theglance()andaugment()methods) and 570 to 670 of the block that the page lacks (references, see-also lists, examples), by the two ways #180 and its review matched them. Merge each page into its block, delete the page and let roxygen write it; write the 7 as blocks. The blocks ofperformBPTest(),performKoenkerTest()andperformWhiteTest()have 28 lines with\%, which markdown writes as\\%; two blocks have no@name(R/error_handling.R,R/intelligent_recommendations.R).generateHeteroRecommendations.Rddocuments nine functions, eight of whose blocks name it with@rdname: written by roxygen, it would take its\nameand title from the first of them,analyseDatasetCharacteristics(), and onedataargument, that ofgenerateHeteroRecommendations(), while the page'sdata, updated for 0.17.0, is in no block.NAMESPACEhas no roxygen line either: written by roxygen it would lose 59 exports whose blocks have no@export(runHeteroTests(),performBPTest(), thesigma_*()functions among them), theimportFrom()of stats, MASS, R6 and utils andimportFrom(ggplot2, scale_fill_manual), and gaina,Registerandheteroscedasticityand the registration ofprint.htest_enhanced(), whichNAMESPACElacks today (a method for a class no function of the package returns). A first run with noNAMESPACEalso exports the twoautoplot()methods as functions, since the package then loads withoutimportFrom(ggplot2, autoplot); a second run registers them again. Found by #180. - [ ] The site's reference pages do not render\tabular.tools/build-docs.Rhas no case for it, sorender()renders its children: the column format becomes text,\tabis dropped and\crbecomes a line break. The tables of four pages come out as lines of cells separated by two spaces, after the format as a stray token:docs/reference/performBPTest.md,performKoenkerTest.mdandperformWhiteTest.md(rrrrrl, lines 42, 41 and 67), the rejection rates with fixed effects for small groups, fromman/performBPTest.Rd,performKoenkerTest.RdandperformWhiteTest.Rd, written by hand; andperformWildBootstrapTest.md(line 112, "0.058.llrrrr", the format glued to the sentence before the table), the size of the wild bootstrap by errors and multiplier, from the markdown table of the roxygen ofperformWildBootstrapTest()(R/modern_diagnostics.R). R's help renders them.build-docs.Rwould need a"\\tabular"case that writes a pipe table: the alignment of each column from the format (l,randcas:---,---:and:---:), the rows split at\crand the cells at\tab, each cell rendered inline on one line, with|escaped, the first row as the header, which each of the four tables has, and a blank line before and after, since the table ofperformWildBootstrapTest.Rdstarts on the line of the sentence before it. Found by #182; recorded, not fixed. No value changes. - [ ] Dependencies. - MovecurltoSuggests: onlydownloadTeachingData()uses it. -R6goes ifTestFactoryleaves the API, since it is the only user. -Suggestsshrinks with the companion split. - Settle whetherdigestbelongs inImportsorSuggests, depending on whether caching stays exported. - [ ] PassR CMD checkwith_R_CHECK_DEPENDS_ONLY_=true. With 39 packages inSuggests, every test, example and vignette that uses one must degrade gracefully. Audit theskip_if_not_installed()coverage; 20 of the 69 test files currently use it. - [ ] The R floor.DESCRIPTIONclaims R >= 4.1, but CI tests only devel, release and oldrel-1. Either add R 4.1 to the matrix or raise the floor to what is tested. - [ ] Replace non-ASCII characters inR/with escapes (89 lines across 27 files). - [ ] The memory of a cache key.rcache_hash()(R/cache.R) serializes the model frame, the design, the vectors of the fit and the data into one raw vector on every call ofcachedTest()andrunHeteroTests(), about twice their size, which a wide design makes large, and frees it at once. Hash them part by part, or stream the serialization into the hash, so that the temporary allocation stays small (found by the review of #144). No value changes. - [ ] A runner call changes how the workflow it is given serializes. Thenamesof theresidualsandfitted.valuesof a workflow's engine fit are ALTREP deferred string conversions of the row numbers, which expand when they are read, and the runners read them: afterrunHeteroTests()of a workflow fitted to 120 observations they take 2,236 bytes where they took 1,209. No value of the workflow changes, but a cache that hashes objects, such as that oftargets, sees a workflow changed by the call. Found by the review of #169, which saw it onmainas well; recorded, not fixed. Read the names without expanding them, or work on a copy of the vectors. - [ ] Replicates lost without a warning. A replicate ofbootstrapTestStatistic()whose refit fails, or, under pairs resampling, whose design cannot be built on the rows it drew (rpairs_design(), since the review of step 8 of the model context), is left out silently: onlyeffective_samplesshows it, as for a failed refit before. The replicates of afitWLS()fit whose variance function cannot be fitted are left out with a warning that gives their number. Consider warning whenever replicates are lost (found by the review of #167). No value changes. - [ ] SplitR/validation.R(1,441 lines) into cache, result-type, assumption-checking and requirement-dispatch units, and factor out the repeated boilerplate: scalar validators, model and data preparation, and intercept stripping. - [ ] RetireTODO.md,SUGGESTIONS.mdandcran.md. The open items in the first two all concernsetup.shand bootstrappingrenv, andcran.mdis a generic guide. With CRAN now the install route, decide whethersetup.sh,renv.lockand theDockerfileremain supported. If they do, completerenv.lockagainstImports/Suggestsso the Docker build uses it instead ofinstall_local(). If they do not, remove them. - [ ] Keep the CRAN test run short. Long simulations run underskip_on_cran(), and the full versions run on CI. - [x]runHeteroTestsParallel()names its results"model". It returns what the registered functions return, so White's test and the four others of the registry reportdata.name = "model"there, whererunHeteroTests()reports the formula of the model since 0.16.0 (NEWS.md). Give it the rule of the runner, or say on its help page that it does not. Done in #182: it has the rule, and each of its results is the onerunHeteroTests()gives for the test on the same call,extras$streaming = FALSEincluded, its numbers computed the same way:identical()to it where the BLAS gives the same last bits on every call, as R's reference BLAS does, and equal to the last bits of the BLAS where it does not, as under Intel MKL or the noisy BLAS ofcran-blas.yml, which the test allows for (from the review of182). On socket workers the model a result carries is the copy that¶
travelled to the worker. Four names report
"model":"white","white_bootstrap","student_bp"and"szroeter". A resampling test still draws from its own stream, and the list is still not ahetero_test_suite.cachedTest(), which returned"model"too, has the rule as well; it gives noextras$streaming, which says how the runner ran a test, and its help page says so. - [x] A grouped run of a multivariatelm.runHeteroTests()refuseslm(cbind(y, x2) ~ x1)with the class of the model context ("needs a model with one response"), but on grouped data it refits the model in each group and records each refusal as a failure of the group: it returns a grouped suite whose every test isNA, with status"error"and issue"model_context", where the refusal of the context should be final (decision 6). A groupedMASS::rlm()fit behaved the same in the first version of step 11 of the model context, and with MASS attachedmainreturned the values of anrlmin each group; step 11 refuses it before any group is refitted, beside afitWLS()fit. Found by the review of step 11; the same onmainat the merge of step 10. Done in step 11 (#171), from its second review, for every refusal of the context: one in a group stops the grouped run with its class and message, a multivariatelmand a penalisedgamgiven a test that needs least squares among them; other failures are recorded in their group, as before. A suite ofNAbecomes a refusal. - [ ]performInfluenceDiagnostics()andperformVIFDiagnostic()on a multivariatelm. Every test, the runners,fitWLS()andbootstrapTestStatistic()refuselm(cbind(y, x2) ~ x1)with "needs a model with one response"; these two, which are not built on the model context, return a value. Onmain, withdfromcontract_data()(tests/testthat/helper-test-recipes.R),performInfluenceDiagnostics(lm(cbind(y, x2) ~ x1, data = d))returns the Cook's distances as a 120 by 2 matrix, a column for each response;influentialas 12 positions among its 240 cells (31, 32, 46, 59, 91, 122, 152, 155, 164, 166, 185 and 201, the seven above 120 rows of the column ofx2); and a cutoff4 / (n - p)that counts the 4 coefficients of both responses, 0.0345, wherelm(y ~ x1)gives 4 / 118.performVIFDiagnostic()of the same fit returns the VIF of the design the responses share, 1 forx1, the value oflm(y ~ x1)(and with a second regressor,d$x3 <- rnorm(120)drawn straight aftercontract_data(), which sets the seed 20261015, 1.005058 for each, as forlm(y ~ x1 + x3)). The decision is whether they refuse it as the tests do. Found in the 0.17.0 release audit; recorded, not fixed. A value becomes a refusal, or none. - [ ]recommendRemediationStrategies()withoutdata.recommendRemediationStrategies(results, model, NULL)stops with "argument is of length zero" for every model, anlmincluded:identifyVariancePatterns()indexes theNULLit is given. Found by increment 11 of the model context, recorded, not fixed; the same onmainat the merge of increment 10. Read the model frame withoutdata, or say thatdatais needed. A refusal becomes a value, or a clearer refusal. - [x] The deprecation messages of 0.14.0. The names deprecated in 0.14.0 that have a replacement (performBreuschPaganTest(),performNCVTest(),performStudentizedBPTest(), the three*Enhancedfunctions,rbootstrap_test_statistic(),test(),simulate_type_I_errors(),simulate_power_analysis(), and"student_bp"and"ncv"inrunHeteroTests()) warn with the default text of.Deprecated(), which does not say that they go in 1.0.0, as the internal helpers, the 0.15.0 forms andrrunAdvancedDiagnostics()do. Found in the 0.15.0 release audit; no value changes. Done in #182: the ten functions give the message ofrrunAdvancedDiagnostics(), now written by one helper for the eleven, as "'performBreuschPaganTest' is deprecated and leaves the exports in heteroTests 1.0.0. Use 'performBPTest' instead.", and the two test names that of thestudentizedargument ofperformBPTestRobust(), "Test name "ncv" is deprecated and stops working in heteroTests 1.0.0. Use "cook_weisberg" instead.". The tests of each pin the 1.0.0, andtest-public-api.Rchecks that every deprecated function gives a message of its own. - [x]ht_analyse_failure()labels a failure by its message, ignoring case. It calls a failure a rank deficiency when the message matches"rank|singular"and a data-quality issue when it matches"NA|NaN|missing|Inf", both withignore.case = TRUE(R/error_handling.R:269-275), andht_data_cleaning_suggestions()picks its advice by the same patterns. The refusals of aglmthat is not a least-squares fit name the test, and two names match: onm <- glm(am ~ wt + hp, data = mtcars, family = binomial),runHeteroTests(m, mtcars, tests = "rank_permutation")stops with "Rank deficiency detected for Rank Permutation:performRankPermutationTest()needs a model fitted by stats::lm(): ..." and the advice to remove collinear predictors, andtests = "high_dimensional"with "Data quality issue detected", from the "na" ofperformHighDimensionalTest(), and the advice to remove missing values. The other tests refuse it as "Diagnostic execution failed". Match whole words, andNAonly in capitals, or give the refusal of such aglman issue of its own. Recorded, not fixed, in 0.15.0; only the message and the recordedissuechange. Done in #182, both ways. The refusal has the classheteroTests_glm_family_error, which the failure analysis reads as the issue"glm_family", final as those of the model context and of the rows ofdata: on theglmabove,runHeteroTests(m, mtcars, tests = "rank_permutation")stops at once with "Model is not a least-squares fit for Rank Permutation:performRankPermutationTest()needs a model fitted by stats::lm(): ...", that class and no advice, and so do the other tests, where White's test tried its four fallbacks first; a grouped run records the issue in each group. The other messages are read word by word,NA,NaNandInfin capitals only, and the names of the rank tests ("Rank permutation", "rank-based") are not a rank deficiency, so that too few observations for the rank permutation test are no longer one. The notes ofsummary()give the new issue, and drop the prefix of White's exact fit, "Test cannot be computed", which they kept. From the review of #182: the refusal is no longer asimpleError, as those of the model context are not; catch it by its class or as anerror. A diagnostic a user registers under the name of a fallback, asregisterDiagnostic("breusch_pagan", f)for White's test, is no longer tried after this refusal, nor after one of data that lack rows of the fit (the item below), as it was not after the other final refusals. The plurals "NAs" and "NaNs" are read asNAandNaN, "singularity" and "singularities" as "singular", and "Spearman rank" is a name too. - [ ]performBoxMTest()with a missing value ingroupordatareturnsNA: a row whose group isNAbecomes a row ofNAs in every group (data[group == gr, ]), and a missing value indatamakes its group's covariance matrixNA. Found by the review of #158 (2026-10-26), where the test now refuses the groups on which it is not defined. Either refuse such data with a message that names the rows, or leave the rows out with a warning, as the group tests do ("Removed N observations due to missing values"). Either changes what such calls return. - [ ]performBoxMTest()on a group with a constant variable, on macOS. The check of #158 decides that the covariance matrix of a group is singular by the rank of its data centred atcolMeans()(rboxm_refuse_singular(),R/performBoxMTest.R:196). WherecolMeans()sums in double precision, as on macOS on arm64, whoselong doubleis adouble, the mean of 50 values of 0.2 is 0.19999999999999993, the centred column is a constant of -8.3e-17 rather than zero, andqr()gives the four columns rank 4; the log-determinant is-Inf, and the test stops with the message for a determinant that under- or overflows ("its variables have full rank, and their scale makes the determinant under- or overflow. Rescale the variables.") instead of the one that names the constant variable.test-boxm.R:157fails so on the macOS (R release) job of every run onmainsince #158 (runs 40326050314, 40361728314, 40382244911 and 40392607338, the last on 67bf41a), and passes on Linux and Windows. The call stops either way; only the message and its advice differ. Look for a variable that is constant in the group before the rank, or decide the rank on the scale of the uncentred data. Found in the 0.16.0 release audit; recorded, not fixed. - [ ]performFlignerKilleenTest()and ties of decimal arithmetic between different observations. Two different observations can have deviations that are equal in exact decimal arithmetic, as when two pairs of observations of a regressor recorded with few digits differ by the same amounts; the binary values of the data break such a tie by a few units in the last place of the response, which can exceed the tolerance of8 * eps * s, scaled by the residuals. Oniriswith every row twice,lm(Sepal.Length ~ Petal.Length)bySpecies, one of the 106 sets of exact ties stays broken, by 10.4 timeseps * s, and the statistic is 8.14863 where the residuals computed exactly, scaled to integers, give 8.14287 (8.13778 before 0.16.0); with+ Species, 0.479152 against 0.479156. Two forms of one model can differ by several percent: on 70 observations that repeat 30 values ofxrecorded with one decimal,y ~ poly(x, 3) + ggives 1.21547, andy ~ x + I(x^2) + I(x^3) + gandy ~ poly(x, 3, raw = TRUE) + ggive 1.30280, 7% more: repeated values ofxin different groups are different observations, rightly not given one residual, andpoly()breaks their decimal ties in its last bits (the second review of #156). Onirisitself and on the one-way fits ofsleep,PlantGrowth,warpbreaks,InsectSpraysandiris[-c(50, 100, 150), ]the value is exact. Found in 0.16.0 with the two items on the test under 0.16.0; recorded, not fixed. A tolerance scaled by the response would cover it, at the cost of merging deviations of continuous data that differ (the review of #156). - [ ]performFlignerKilleenTest()depends on the last bits of the BLAS. Oniriswith every row twice,lm(Sepal.Length ~ Petal.Length)bySpeciesgives 8.14863 on R's reference BLAS; with its coefficients moved by one unit in the last place, as another BLAS may compute them, it gives 8.13333, 8.14287, 8.14863 or 8.14867, and 8.13333 is what CRAN's check of 0.16.0 with Intel MKL gave for the lm (8.149 for the glm). With+ Speciesthe value moves in its sixth digit (0.479151 to 0.479156); thepoly()form does not move. So the statistic and the p-value a user sees depend on their BLAS. The likely cause:rfligner_rounding_ties()ties deviations within 8 eps times the largest absolute residual of their group, while the rounding ofy - X bscales with|y| + |X b|, thesizethatrfligner_residuals()already computes; scale the tolerance by it. Found by the review of #172 (whose test of these fits runs off CRAN only); recorded, not fixed. Changes a value on such data. - [x] Two comparisons in the tests failed under the noisy BLAS after #172. The noise a comparison draws depends on every BLAS call made before it in the process, so a test, or a change to code, that runs earlier moves it. On CRAN the F of the variance form test is compared to 1e-8 (blas_tolerance()) on every cell of the model-context matrix, not only on those with missing rows (#184). And the Gaussianglmagainstlm(..., tol = 1e-11)on a design within 1e-9 of collinear (test-model-eligibility.R) is compared to 1e-4 on CRAN, 1e-6 off it. That design amplifies the last bits: one unit in the last place of the data moves the p-values of theglmand of thelmapart by up to about 5e-7, in the Harrison-McCabe test and in Koenker's. On #185, which changes the streaming code that earlier tests run, the noisy BLAS moved them past 1e-6: Noisy BLAS 2 once (Koenker's), Noisy BLAS 3 twice. The statistics of the defaultlm, which aliasesx3, are at least 4.6e-3 from those of theglm, so the test still tells the two ranks apart. Tests only; no value changes. Done in #186. - [ ] Squared residuals that are equal but for rounding. Onlm(y ~ x)withy = 1 + 0.5 x + 0.3 s, wheresis 1 or -1, the residuals are 0.3 and -0.3, and their squares differ only in their last bits. Withx = rep(1:20, each = 2)andsalternating, Koenker's test gives 8.13 on 1 (p = 0.004); withx = 1:40andsin the pattern(1, -1, -1, 1), 15.25 on 1 (p = 9e-5). White's test gives 8.2 and 27.6, from the rounding alone. The streaming forms refuse both, saying the test "requires variation in squared residuals", and the studentized statistic stops on non-finite values. The same on 0.17.0 and with #184, whose rule rightly does not fire, since the regressors do not fit the rounding exactly. Decide the variation of the squared residuals in memory as the streaming forms do, on the scale of the residuals. Found by the review of #184; recorded, not fixed. Makes values refusals on such data. - [ ]performVIFDiagnostic()gives a number for an infinite VIF. On a design with exactly collinear columns, such asx3 = x1 * 3besidex1(thewf_local_variableworkflow ofhelper-model-context.R), the VIF of both is infinite, and the function returns the diagonal ofsolve(cor(X)): of the order of 1 / eps wheresolve()succeeds, its digits set by the last bits of the BLAS, andInffor every column where it fails, that ofx2, 1.0, among them. Under the noisy BLAS ofcran-blas.ymlthe two computations that test compares gave 1.5e15, 1.8e15, 2.25e15 orInfforx1andx3, and 1.0 orInfforx2, as the seed and the call decided. Take the rank of the design first and giveInffor the columns it aliases only. Found by the noisy BLAS in #172, whose test leaves such VIFs out on CRAN; recorded, not fixed. Changes a value on such designs. - [ ] Left from step 1 of the model context (under 0.16.0), on the rows ofdatamatched by number: - Follow-up, from the review of step 1: try the rows in the order of the fit also when the rows picked by number fail the comparison anddatahas one row per observation. That accepts a fit tod[order(x), ]given the same data numbered again, and character row names that look like numbers. - Known limit, as in 0.15.0: the rule on ties covers data taken in the order of the fit only. If the fit used numbered data with ties, and the same data is given permuted within those ties, it is matched by number and accepted, both before and after step 1. A test that reads a column the model frame does not hold then sees the rows exchanged: the rank permutation test withorder_by = "z"gives -0.02142 instead of -0.02619 in the order of the fit. Extend the rule on ties to rows matched by number when the call reads such a column. - [x] Data that lack rows of the fit are refused without the class of the other refusals of rows.rstop_data_rows()(R/interface.R:673, called atR/model_context.R:678) refuses them with a plainstop(). Every other refusal of the rows ofdatahas the classheteroTests_data_rows_error(rstop_data_rows_unmatched()), which the runners treat as final (R/error_handling.R:306). On a fit to the 120 rows ofd,runHeteroTests(m, d[1:100, ])tries the four fallbacks of White's test, each of which refuses the same rows. It then stops with "Data quality issue detected for White" and the advice to "Remove or impute missing values", which the "missing" of "Missing rows" selects (see the item onht_analyse_failure()above). A tidymodels workflow and a parsnipfit_xy()fit already give this refusal the class (R/ecosystem_integration.R:636-645). Give it the class and keep its message, which the suite pins (tests/testthat/test-model-context.R:287), so that the runners raise it at once. Only the class, and the message of the runners, change. Found by the review of the R Journal article's subsection on the model context. Done in #182: the refusal keeps its message and has the class, sorunHeteroTests(m, d[1:100, ])andrunDiagnostics()stop at once with "Data do not match the model for White: White test requiresdatato contain the rows used to fit the model. Missing rows: 101, 102, 103", that class and no advice. A grouped run, whose groups are refitted on their own rows and so do not meet it, would record it in its group with the issue"data_rows"and no fallback, as it records the other refusals of rows. The workflows and parsnip fits, which told it by its message, now tell it by its class. The refusal is no longer asimpleError, as the other refusals of rows are not; catch it by its class or as anerror(from the review of #182). - [x] The streaming tests could overflow on terms that are combinations of others.chunked_regression_r2()(R/performance.R) stacked each chunk under the factor of the chunks before it byqr.R(qr(rbind(factor, rows), tol = 0)), the decomposition of LINPACK (dqrdc2) without pivoting, which makes a Householder reflection from every column, however little is left of it. What is left of a column of the auxiliary design that is a combination of others, as the squares and products of dummies in White's design are, is rounding error; the next such column leaves about the square of that, and so on down.dqrdc2divides a column by its norm, and when that norm is below the reciprocal of the largest double the result is not finite: the next chunk, or theqr()that reads the factor, stopped on "NA/NaN/Inf in foreign function call (arg 1)". The defect predates #184, which found it: it came with the stacking, in 0.17.0, and is onmainat 01164f6. The noisy BLAS jobs ofcran-blas.ymlon #184 stopped there, on all three seeds, in White's streaming test on a one-way layout of nine groups of ten observations and one of two, a design that pull request tests; R's reference BLAS does not reach it there, but a multiple of a regressor of size 1e-300 does it on any BLAS, in chunks of any size (test-streaming.R). Done in #185: the stack is decomposed byqr()at a tolerance of 1e-13 (rchunk_factor()), which moves to the end a column of which less than 1e-13 of its norm is left after the columns before it, and the factor keeps the rows up to the rank, its columns put back in their order. The rows left out hold only what is left of the columns of the design moved, so the cross-products of the other columns do not change; where no column falls below the tolerance, the factor is that of 0.17.0 to the last bit. What is left of the response is never left out: whendqrdc2moves it, the factor keeps it as one more row, the norm of its effects past the rank after the reflections of the columns kept (qr.qty(), with the reflectionsdqrdc2goes on to make from the columns it moved set to 0, as they can be not finite). The first version of #185 left it out too, and the review of #185 found that this lost the exact-fit decision:dqrdc2compares the response with its norm about zero over the rows so far, which squared residuals that vary little are almost all of, so what is left below the tolerance can be the whole of the residual sum of squares, and it adds up over the chunks. In 10,000 chunks of 7 of squared residuals1 + 2.5e-8 xand such a part, whose total sum of squares about their mean is 6e-16 of their sum of squares, the residual sum of squares was 0, an exact fit for White's streaming test, where one chunk and 0.17.0 give 3.16e-18, five timessqrt(.Machine$double.eps)of the total; it is now 3.16e-18 in chunks of either size (test-streaming.R). The factor can now have fewer rows than columns: the rank, the effects, the residual sum of squares andrfactor_regressors_exact()read it throughqr(), as before. On 21 fits (continuous regressors, factors, a one-way layout, near-collinear and distant regressors,poly(), weights, a Gaussianglmand threemgcv::gam()fits, the basis ofs(x1)among them) in chunks of 7 and 10,000, 106 of the 126 statistics of the three streaming tests areidentical()to those of 0.17.0 and the others, each on a stack where a column fell below the tolerance, within 8.4e-16 relative (their p-values within 4.4e-15), with the same degrees of freedom and refusals; the factor isidentical()on the 55 stacks where no column fell below it. On the 17 fits of the review (100 streaming results), one of 100,000 observations among them, the largest change is 2.0e-14 relative, less than the 2.2e-14 to 4.5e-14 by which White's streaming test differs fromperformWhiteTest()there in 0.17.0 as since. A decomposition of LAPACK with column pivoting, tried first on the branch of #184, moved White's streaming statistic on the basis of anmgcv::gam()fit by 1.5e-8, and was not used. - [ ] A column of very small norm can still overflow the stacking of the streaming tests.rchunk_factor()(R/performance.R) keeps a column of which at least 1e-13 of its norm is left after the columns before it, so its guarantee is relative: a column whose norm is below about 5.6e-296, 1e13 times the reciprocal of the largest double, can leave less than that reciprocal, anddqrdc2divides by it. Onlm(y ~ x1 + x2)withx1 = 1e-150 * uandx2 = 1e-150 * (u + 1e-6 * w), 60 observations, whose squares and product in White's design are of size 1e-300 and nearly collinear,performWhiteTestStreaming()stops on "NA/NaN/Inf in foreign function call (arg 1)" in chunks of 7 and of 10,000, in 0.17.0 and since185 alike, where
performWhiteTest()gives 25.476; at 1e-140 it gives¶25.476 too. Found by the review of #185. Scale the columns of the stack before decomposing it, or refuse such a design with a message that says why; changes a value only where the test stops today. - [ ] Left from steps 8 to 10 of the model context, recorded, not fixed. Moved from 0.17.0 at its release (2026-10-30), where the steps that found them are done; each changes what a call returns: - From step 8:
bootstrapTestStatistic()refits each replicate at the tolerance oflm(),1e-7, as its refits of the formula did, so a fit whose rank was decided at a smaller tolerance and differs, anlmfitted with a smallertolor a Gaussianglm(1e-11) with a nearly collinear column, is refused ("datadoes not reproduce the fit"), as such anlmwas before. Refitting at the tolerance of the fit would give them values. A refusal becomes a value. - From step 8, left by step 11:fitWLS()of anmgcv::gam()fit whose smooths are all fixed (s(x, fx = TRUE)), the least-squares fit of its design, refits that design but returns anlmwhose terms rebuild the variables of the formula, not the basis of the smooths, so the tests refuse it as a fit whose design does not reproduce its residuals (M8), as since step 8. Refuse such a fit infitWLS()itself, as a penalisedgamis since step 11, or keep its basis there, so that it can be tested. A refusal moves, or becomes a value. - From step 9:performInfluenceDiagnostics(), and sorunDiagnostics()$influence, returns the Cook's distances padded withNA, one per row, as step 9 pads an output; butinfluentialholds their positions among the rows, where the same fit withna.omitgives their positions among the observations (the names are the same). Decide which the positions count. A value changes, or none. - From the review of step 10:augment()of the results of a workflow whose preprocessing drops rows of new data, a recipe step applied to them (step_filter()orstep_naomit()withskip = FALSE), refuses its data, since the preprocessing does not say which rows ofdatait kept; the runners take those data, whose rows the alignment confirms. Find the rows the preprocessing kept, or keep the refusal. A refusal becomes a value, or none. Measured on the second review of step 10: forging each row on its own, which says which rows a step that maps each row on its own kept, costs 2.75 ms a row (0.33 s for 120 rows, about 14 s for 5,000, where forging the whole data takes 3 ms), and a step that makes a row from other rows gives other values row by row; not adopted, the refusal kept. - From the third review of step 10:augment()refuses the data of a workflow whose recipe makes a row from other rows (step_mutate(cs = cumsum(x)),dplyr::row_number(), a mean overdplyr::lag(),step_lag()) or sorts data already in its order (step_arrange()), where 0.16.0 bound each row of data in the order of the fit its own values; the runners take them. A way to accept them: data in the order of the fit whose columns the preprocessing leaves unchanged identify each row (no two rows alike in them) and match the mapped rows in that order, so that each mapped row is the row ofdataat its position. A refusal becomes a value, or none. - [ ] Found by the model-eligibility work (the Supported models page), recorded, not fixed, as they change what a test computes rather than which models it accepts. Those below are not settled by the model context; the others are under 0.16.0 and 0.17.0: -rvalidate_against_reference("white", ...)compares White's statistic withcar::ncvTest(), the Cook-Weisberg score test, so it reports a mismatch on every model. Compare withlmtest::bptest()on the squares and cross products, astest-model-eligibility.Rdoes. Deprecated; changes what it returns. -performInfluenceDiagnostics()counts the aliased coefficients of a rank-deficient fit inpfor its default cutoff,4 / (n - p), and sorunDiagnostics()does; the rank is the number of coefficients. Changes a value. -generateHeteroRecommendations()refuses a rank-deficient fit (it checks the model with the checks ofvalidateTestInputs()), which the tests andrunHeteroTests()accept since 0.16.0. Accepting it makes a refusal a value. - [x] Koenker's and the Breusch-Pagan tests fit the squared residuals exactly on the same designs, since their auxiliary regression is on the regressors: on the paired design of White's exact fit (under 0.16.0)performKoenkerTest()gives 60 on 30 degrees of freedom (p = 0.0009), the number of observations, aslmtest::bptest()does, andperformBPTest()63.25 on 30 (p = 0.0004), a function of the spread of the squared residuals alone, whatever their pattern. Found by the review of the follow-up of N2. Refuse such a fit in both, at the threshold of White's test, in a pull request of their own. Refuses calls that return a value. Since 0.16.0 both warn on that design, whose 30 levels of two observations are fixed effects for small groups (the item on them, under 0.16.0); the refusal is still to do. Since #183performKoenkerTest()no longer callssummary.lm(), so on such a fit it no longer gives that function's warning "essentially perfect fit: summary may be unreliable": onlm(y ~ x1 + f)with 10 levels of 2 observations 0.17.0 warned and #183 gives the same 20 on 10 without it. Done in #184: both tests, and their streaming forms with the same message, refuse an auxiliary regression whose residual sum of squares is at mostsqrt(.Machine$double.eps)of the total, the rule of White's test, now one function that White's test calls too (rauxiliary_exact(),R/performWhiteTest.R). On the paired design they refuse where they gave 60 and 63.25 on 30, and onlm(y ~ x1 + f)where they gave 20 and 28.03 on 10, before the warning on fixed effects for small groups, as White's test does. The message is White's, built by the same code (rrefuse_exact_fit()): "The auxiliary regression of Koenker's test fits the squared residuals exactly: the squared residuals are a combination of its 31 independent terms with its intercept, for 60 observations. Its R-squared is 1 and its statistic the number of observations, whatever the data. The design forces the exact fit, as a factor with two observations per level does, however many levels it has; use more observations per level."; the Breusch-Pagan test's says its statistic is "a function of the spread of the squared residuals alone, whatever their pattern". Its class is a general one,heteroTests_exact_fit_error, not White's, whose name and fieldregressors_exactare about White's auxiliary design; White's refusal carries it too, after its ownheteroTests_white_exact_fit, with its message unchanged, so that one class catches every exact fit.ht_final_exact_fit()reads the general class, and the new refusals say that the regressors fit the squared residuals exactly, so inrunHeteroTests()they are final:"breusch_pagan"and"koenker", streamed or not, stop with the message where they returned the statistic, without the Cook-Weisberg test, their last fallback, under their name; the runner drops the class, as it does on White's refusal (keeping it, as #182 does for the refusal of rows, is left to decide). A fallback that refuses an exact fit ends the search (a refusal of classheteroTests_exact_fit_errorwhoseregressors_exactis notFALSE; other refusals still go on to the next fallback, as White's refusal of a saturated design, withregressors_exact = FALSE, goes on to the Breusch-Pagan test, 3.63 on 6 on 22 observations). So"high_dimensional", which refuses 20 observations, no longer returns the Breusch-Pagan test's 28.03 on 10 under its name onlm(y ~ x1 + f), and"spatial_hetero"withoutlistwstops with its own refusal on 12 subjects of two, where it returned the Breusch-Pagan test's 12.37 on 12. The audit of the other tests of an auxiliary regression of the squared residuals, on the paired design: the studentized statistic (performStudentizedBPTest(),performBPTestRobust(studentize = TRUE)with or without the bootstrap,"student_bp"), 60 on 30,performBPTestRobust(studentize = FALSE), 63.25, andbootstrapTestStatistic()of either test (p = 1 for Koenker's) refuse it with the refusal of the test they compute; the wild bootstrap, which gave 60 with p = 1, every replicate an exact fit too, refuses it before the replicates; the Cook-Weisberg test withvar_formula = ~ treat + subject(63.25 on 30), Harvey's test of the regressors (63.07 on 30, and an infinite F studentized), the high-dimensional test withvariance_threshold = 1, which keeps every component (60 on 30), and the variance form test with a variance regressor constant in each pair andagainst = ~ subject(F = 4.06e29 on 28 and 30) refuse it by the same rule, withregressors_exactunknown (NA) for the Cook-Weisberg, Harvey and variance form tests, whose regression need not be on the regressors. Not exact fits there, and unchanged: the Cook-Weisberg test of the fitted values (0.608 on 1), Harvey's of the fitted values and their square, the high-dimensional test with its default 26 components (56.84 on 26), Glejser's and Park's tests of one variable, and Bartlett's test of groups of two; Levene's, Brown-Forsythe's and Fligner-Killeen's refuse groups of two already.autoTransform(), which chooses by the p-value of the Breusch-Pagan test, stops with its refusal on such a design. One step away, with a subject observed a third time or 20 subjects of three, every test gives the value it gave. Ininst/validation/fixed-effects-size.csvthe T = 2 cells of the two tests, 10 to 200 subjects, are such fits (Koenker's mean statistic is n, and the Breusch-Pagan test rejects in 25.8% to 100%): the README says so, and the rows are kept as measured; no T = 3 or other cell is one. One test of the warning moved from two observations a cell to three (test-fixed-effects-warning.R, the crosseda * b). Refuses calls that returned a value. - Found by the audit of #184: on the paired design Glejser's test oftreat, which takes the values 0 and 1 in each pair, gives 1.1e-15 on 58 degrees of freedom (p = 1), since the absolute residuals of a pair are equal and their regression ontreathas a slope of 0 whatever the data: a test that cannot reject. Any test of a variable that varies only within pairs, balanced astreatis, gives 0 there. Not an exact fit, and not changed. Refuse such a variable, or say so on the help pages; changes a value, or none. - [x] To fix before 0.18.0 is released: the rule refuses a fit that the data make exact, not the design, and its message blames the design. It compares the residual sum of squares of the auxiliary regression with the total sum of squares of the squared residuals, and one gross error in a level of two observations can dominate that total. Onlm(y ~ g)with 9 groups of 10 and one of 2 (ynormal with mean 50 and sd 5,set.seed(1)), one value of the pair multiplied by 100 leaves a residual of 2336 against an sd of 4.33; the dummy of the pair fits its two equal squared residuals, and the ratio falls belowsqrt(.Machine$double.eps). 0.17.0 gives Koenker's test 92 on 9 and the Breusch-Pagan test 2069.35 on 9, andautoTransform()choosesboxcox. #184 refuses both tests, their streaming forms and the runner, andautoTransform()stops, saying "The design forces the exact fit … use more observations per level", which is false here. White's test has refused this case since 0.16.0. Withy ~ x + fa shift of 200 sd is still computed and one of 500 is refused; ifxvaries within the small level, nothing is refused. Refuse only a fit the design forces: push a fixed pseudo-response through the fit, and refuse only if the same regressors also fit its squared residuals exactly, so that White's test gives its value there too. Found by the review of #184; to be fixed in its own pull request. Makes refusals values. Done in #187: the tests refuse an exact fit only when the design forces it. Each test pushes a fixed pseudo-response, standard normal draws from a fixed seed by R's default generators, after which the state and the kinds of the session's generator are put back, through the QR decomposition of the fit, with its weights, and refuses only when the same auxiliary regressors fit the same function of its residuals exactly too (rforced_exact_fit()): their squares, their logs for Harvey's test, their centred squares for the studentized statistic. When the squares of those residuals do not vary, whether the design forces the fit is not known, and the test refuses it without saying so. The first version of #187 used the fractional parts of i times the golden ratio, whose pattern its review found the designs could follow, by the order of their rows: on 72 subjects of two visits sorted by visit a fit the design forces gave Koenker's test 144 on 72, and on 4 units of 13 periods sorted by unit a fit the data make exact was refused as forced, where with the rows shuffled it gave 52 on 15. The streaming forms carry those squares as one more response in the pass they make, and decide as the exact forms do; the stacking of #185 keeps what is left of both responses (rchunk_factor()). On the layout above the dummies fit the squared residuals to within 1.1e-9 of their total, and Koenker's test gives 92 on 9 and the Breusch-Pagan test 2069.35 on 9, streamed or not, throughrunHeteroTests()too, andautoTransform()choosesboxcox, the values of 0.17.0. White's test gives its value there too, 92 on 9, and n R^2 = 80 on the pairs oftest-performWhiteTest.Rwhose residuals are +(1 + x) and -(1 + x): a refusal becomes a value. Whether its regressors alone fit the squared residuals, which decides the runner's fallback, is asked of the squared pseudo-residuals too. A fit without a QR decomposition, anmgcv::gam()tested on its design or anlmfitted withqr = FALSE, is refused when the regression fits exactly, with a message that says only that it does, not that the design forces it.autoTransform()leaves out a candidate the Breusch-Pagan test refuses, since a candidate drops the rows where its response is not finite and so can fit another design, and stops with the first refusal only when every candidate is refused. A design that forces the fit, the paired design or a factor of two observations a level, is refused as in #184. - [ ] Left from the review of #187, recorded, not fixed. Anmgcv::gam()fit keeps no QR decomposition, so on an exact fit it is refused without the reason and the advice, on a paired design too, which forces it; for a gam whose smooths are fixed the QR decomposition of its weighted design would tell. And on a fit the data make exact the F statistics come back as values, as in 0.17.0: Harvey's studentized test gives Inf on 1 and 78 on pairs whose log squared residuals are x exactly, and the F of the variance form test can be a ratio to rounding error. Say so on their help pages, or refuse them. - [x] The degrees of freedom ofperformBPTest()andperformKoenkerTest()are the number of columns of the design but its intercept, not the rank of their auxiliary regression less its intercept, as White's are since N2: on a model without an intercept whose columns span the constant, such aslm(y ~ 0 + group + x1)with a factor of four levels, the auxiliary regression adds an intercept that the dummies already span, and the tests count 5 where the rank says 4. In the design ofcontract_data()the Breusch-Pagan test gives 8.653 on 5 and Koenker's 10.52 on 5, wherelm(y ~ x1 + group), the same column space, gives the same statistics on 4; the streaming forms refuse that model ("auxiliary regression became rank deficient"), as before the pull request of N3. Found by that pull request. Take the rank, as White's test does; changes a p-value, and makes the streaming refusals values. Done in #183: both tests take the rank of the auxiliary regression less its intercept, decided by the code of White's test on the regressors centred one by one (rauxiliary_columns(),R/performWhiteTest.R), and their streaming forms the rank they decide inchunked_regression_r2()(R/performance.R) by the same rule, where they refused a rank deficiency. Onlm(y ~ 0 + group + x1)the Breusch-Pagan test gives 8.653 on 4 (p = 0.0704, was 0.1237 on 5) and Koenker's 10.52 on 4 (p = 0.0325, was 0.0617 on 5), the values oflm(y ~ x1 + group)and oflmtest::bptest(), which takes the rank of its auxiliary fit too, so the package now agrees with it there; the streaming forms give them where they refused, andperformBPTestRobust(studentize = FALSE), whose interval follows the degrees of freedom, and the registered"breusch_pagan"and"koenker"follow:runHeteroTests()asked to stream them (chunk_threshold_mb) gives the streamed values, where it fell back on the Cook-Weisberg test, 3.79 on 1, under their names. Changes a p-value. The auxiliary regression is fitted from the matrix of the columns kept (rauxiliary_fit()), on the centred columns where those as they are lose one, not bylm()from a formula on the columns as they are, and that changes the statistic on three kinds of design of full rank, from a wrong value to that of the streaming forms, which had it already, and oflmtest::bptest()but on theglmbelow, whose design it refits bylm.fit(), dropping the regressor too (from the review of #183): a model without an intercept with a regressor far from zero, which that fit dropped as a combination of its intercept while the degrees of freedom counted it,lm(y ~ 0 + I(x1 + 1e8) + x2)in the design ofcontract_data()giving 2.850 on 2 (p = 0.241) for 16.72 (p = 0.0002) and Koenker's 2.372 (p = 0.305) for 13.92 (p = 0.001), the values oflmtest::bptest()withvarformula = ~ x1 + x2; a Gaussianglm, which keeps at a tolerance of 1e-11 a regressor thatlm()would alias,glm(y ~ I(1e6 + x1 / 10) + x2)giving 2.005 for 12.45 and 1.457 for 9.044, the values oflm(y ~ x1 + x2); and a regressor namedscaled_sqore_squared, which the formula of the Breusch-Pagan or the Koenker test took for its response,lm(y ~ scaled_sq + x2)giving 102.0 for 12.45 andlm(y ~ e_squared + x2)0.014 for 9.044. Changes a value there; on the 16 other fits the review compared, one of them a rank-deficientlmtested on the estimable part of its design, every result wasidentical()to 0.17.0's. A model whose only regressor is constant,lm(y ~ 0 + one), has no regressor beyond the intercept and is refused by all four ("requires at least one predictor beyond the intercept"), where the exact tests gave 0 on 1 and the streaming ones refused it as rank deficient; the exact and streaming forms refuse it with the error of a model with an intercept alone, asimpleErrorwith the same message, as the "rank deficient" refusal was asimpleErrortoo, of therassumption_violationtemplate of.std_error(), whose "Results may be unreliable" does not fit a refusal. Koenker's streaming test named the Breusch-Pagan test in that refusal, and names its own.runHeteroTests()on that model, which returned those values, stops with the refusal, as onlm(y ~ 1), and advises "Fit a model with a regressor that varies", where it advised removingone, its only regressor, as it did onlm(y ~ 1)whendataheld a constant column: on a model whose design is constant the constant columns ofdataare no longer advised away (ht_constant_design(),R/error_handling.R). The audit of the other tests of an auxiliary regression of the residuals:performWildBootstrapTest()reported the number of regressors as its degrees of freedom and reports the rank, and computed its statistic, for the data and each replicate, by the formula oflm()too, and computes it on the auxiliary regression of Koenker's test, so that on the far-from-zero model it gives 13.92 where it gave 2.372, the statistic ofx2alone, and a p-value of that statistic; its values on other fits are those of 0.17.0 (four fits at a fixed seed, to the last bit), and it refuses the model whose regressors are all constant, and a model with an intercept alone,lm(y ~ 1), on which it gave 0 on 1, p = 1, a test that cannot reject. The studentized statistic (performStudentizedBPTest(),performBPTestRobust(studentize = TRUE),"student_bp"), the Cook-Weisberg test (var_formulatoo), Harvey's test and the variance form test took the rank already; Glejser's, Park's, the spread-level and the Davidian-Carroll tests regress on one variable; and the high-dimensional test counts the principal components it keeps. No study ofinst/validation/fits such a model. - The studentized statistic (performStudentizedBPTest(),performBPTestRobust(studentize = TRUE),"student_bp") regresses the centred squared residuals on the design of the fit without adding an intercept, aslmtest::bptest()does, whereperformKoenkerTest(), whose statistic its help page says it computes, adds one. On a model without an intercept whose columns do not span the constant,lm(y ~ 0 + x1 + x2)in the design ofcontract_data(), it gives 1.507 on 1 degree of freedom (p = 0.220), the value oflmtest::bptest(), and Koenker's test 5.636 on 2 (p = 0.060). With an intercept, or columns that span the constant, the two agree. Found by the audit of #183. Add the intercept, or say on its help page that it differs there; changes a value, or none. - White's auxiliary design is built from the regressors centred one by one, so its rank does not depend on where a regressor that enters the model linearly is located, but a product or a power of a variable located far from zero inside the model stays ill-conditioned, and can lose a column of the auxiliary design whilelm()keeps every coefficient. On 200 observations of two standard normal regressors (set.seed(1); x1 <- rnorm(200); x2 <- rnorm(200),y <- 1 + x1 + x2 + rnorm(200, sd = exp((x1 + x2) / 4))),lm(y ~ I(x1 + 2e3) * I(x2 + 2e2))gives 46.83 on 7 degrees of freedom wherelm(y ~ x1 * x2), the same model, gives 51.90 on 8, andlm(y ~ I(x1 + 2e3) + I((x1 + 2e3)^2))8.05 on 3 for 11.29 on 4. What counts is the shift relative to the spread of the regressor. On ten data sets of 200 observations (five seeds of standard normal and of U(0, 1) regressors, the response as above), withx1shifted by k standard deviations andx2by k or k / 10 of its own, and k from 100 to 40,000 in steps of a factor of 1.12: up to k = 700 the degrees of freedom held in all three designs and the statistic moved by at most 3e-5 relative (1.1e-7 up to k = 200); White's test lost one column from k = 790 to 2,000, the square first and the uniform regressors before the normal ones; andlm()aliased the term itself from k = 3,200 (the square, and the interaction with both shifted by k) to 11,000 (x2shifted by k / 10), before White's test sees it. WithI(x1 + 1e4) * I(x2 + 1e3)on the data above the fit is additive, and the test gives 35.02 on 5, the value oflm(y ~ x1 + x2). So shifts of 1e3 and 1e2 keep the degrees of freedom on standard normal regressors, 1,000 standard deviations, and lose a column on U(0, 1) ones, 3,300 to 3,700: 7 for 8 in three seeds, the statistic 0.5% to 6.8% lower. A year from 1990 to 2020, about 220 standard deviations from zero, and its square,year * zandincome * agegive the value of the centred model to seven digits or more. Found by the review of the follow-up of N2; the examples and thresholds corrected by the review of #149. An optional improvement: build Z from the orthonormal basis Q ofqr(X_c), whose squares and products span the same columns as those of the centred regressors when the cross products are in, and only then. Changes a value on such designs. - Onlm(y ~ poly(x1 + 1e6, 2) + x2, data = d)the model context refusesd, the data of the fit: the basis of the polynomial computed again fromdthrough thepredvarsof the terms differs from that of the model frame by more than the tolerance of the comparison, and the tests stop with "cannot match the rows ofdatato the observations of the model". Withoutdata, or withpoly(x1 + 1e3, 2), White's test gives the value ofpoly(x1, 2). Found by the review of the follow-up of N2, as on main before it. A fix makes such calls return a value. - [ ] Defects found during the 0.12.0 and 0.13.0 work, left for 0.16.0 so that 0.14.0 changes no value. Each fix that changes a value is announced in bold inNEWS.md. The one below is not settled by the model context; the others are under 0.16.0 and 0.17.0. - The unexportedperformWhiteTestEnhanced(bootstrap = TRUE), throughbootstrap_white_test(), takes the log of a log response twice, drops an offset given as an argument and takes rows by position. Remove it or fix it. No exported value changes. - It deletes
0.19.0: release-grade evidence and the open decisions¶
Moved from 0.16.0 on 2026-10-26: the evidence for 1.0.0 at the grade of the
release gate in inst/validation/README.md, measured on the code that will
be frozen, and the decisions still open.
- Re-run the full sweep at
N_MC = 5000, so that the release gate ininst/validation/README.md([0.042, 0.058]) applies to every exported test, not only Passes A and B. At 400 replications the Monte Carlo standard error is 1.1%, so the Gaussian sizes between 0.068 and 0.072 (Cook–Weisberg and NCV, Spearman, Levene) cannot be told apart from noise. Pass A, at 5000 replications, put Cook–Weisberg at 0.044. Moved from 0.16.0 (decided by the author on 2026-10-26): 0.16.0 does not need it, and run on the code before the freeze it certifies that code. - The bootstraps that estimate the weights again, at the release
gate. The 0.15.0 default of
reestimate_weightsrests on rates measured at 2000 replications withB = 499(inst/validation/weighted-fits-size.csv), where the 99% interval around 0.05 is[0.037, 0.063]and all four cells are inside it. Against the release gate's interval at 5000 replications,[0.042, 0.058], the wild bootstrap with 150 observations, 0.0605, is outside and the White bootstrap, 0.0415, just below it; against[0.041, 0.057], around 0.049, the rate of an exact test withB = 999, the wild bootstrap is outside. Measure the four cells of block 4 at 5000 replications withB = 999, the default, beside the true weights in the same design. From the time per sample measured in 0.15.0 that costs about 36 to 50 CPU-hours for the four cells, and about 7.6 for the two cells of the wild bootstrap alone. If the wild bootstrap stays outside the gate at 150 observations, say so on its help page. -
The validation rows the R Journal article reads, on the code to be frozen. The article and its supplement read rates at render time from ten studies in
inst/validation/:- seven since the article's update to 0.17.0:
weighted-fits-size.csv,glejser-skewed-errors.csv,harrison-mccabe-size-power.csv,modified-wald-size-power.csv,offset-refits-size.csv,variance-form-dnvk-size-power.csvandszroeter-design-null-size.csv; - three since its additions of 2026-11-02 (heteroTests-paper #6):
pass-a-size-power.csv,wild-multiplier-size.csvandfixed-effects-size.csv.
pass-a-size-power.csvwas last written in 0.7.1 (2026-08-31). Five of the first seven were last written in 0.13.0 or 0.15.0, and so waswild-multiplier-size.csv(2026-10-15).offset-refits-size.csvandfixed-effects-size.csvwere written in the work on 0.16.0 (2026-10-18 and 2026-10-23), andszroeter-design-null-size.csvthe day before 0.16.0 was released (2026-10-26). None was measured on 0.17.0. The full sweep atN_MC = 5000(above) may replace the Pass A rates the article reads.The README says which rows predate a later change to their procedure, not the version each block was measured on. One such row is the Szroeter row of the known-weights block of
weighted-fits-size.csv, measured before 0.16.0 took the test's null moments from the design, and not run again.Re-measure every row that predates a change to its procedure on the code to be frozen, and record in the README the version on which each block was measured, so that the article can say it. Found by the review of the article's update to 0.17.0; the last three studies were added after the article's additions.
The article's range for fixed effects for small groups, 22% to 98%, comes from
fixed-effects-size.csv, whose T = 2 cells of the Breusch-Pagan and Koenker tests are refusals since 0.18.0 (#184; the README says so, and the rows are kept as measured). The article reads only the cells of 30 subjects of 3 and of 4, 100 of 8 and 200 of 4, none of them an exact fit. - [ ] The performance study the R Journal article reads, on the code to be frozen. The article and its supplement read their timings and memory allocations from three extracts in the article's repository,paper/generated/performance_*.csv.paper/scripts/performance_study.Rwrote them under heteroTests 0.11.2, and they were not written again, so the article says they do not measure the current code. Since then the exact Breusch-Pagan and Koenker tests have come to read the fit through the model context, the cache key has changed, and the streaming code has been rebuilt twice: on the stored design in 0.16.0 and on a stacked QR in 0.17.0. No milestone ran the study again. The script callsrun_benchmark_suite()andgenerate_benchmark_report(), which move to the companion package (see the row of Decisions needed on its name, due in 0.19.0). The suite'sncventry also times the internal.ncv_test(model)(R/benchmarking_system.R:363-368), where in 0.11.2 it timed the exportedperformNCVTest(model). Rewrite the script so that it times the exported tests and calls no function that leaves the package. Run it on the code to be frozen, and record the run in the article'sPROVENANCE.md. Found by the review of the article's restructure to the journal's 20 pages. - [ ] Provenance of a study run in chunks. Commit the script before a chunked run is launched, and record its md5 in the settings of each part, so thatMERGErefuses parts written by another version of the script.wild-multiplier-size.Rpredates the convention: its parts carry no md5, and adding one now would makeMERGErefuse them. - [ ] The open decisions. This release settles the rows of Decisions needed (below) that are still open and due before 1.0.0, so that none is open at the freeze. - seven since the article's update to 0.17.0:
1.0.0: freeze¶
- Remove everything deprecated in 0.14.0 and 0.15.0, or turn it into
.Defunct()stubs that name the replacement. -
test-public-api.Rasserts the frozen export list exactly. - A failed test keeps its name in
runHeteroTests()(decided by the author on 2026-10-23). From 1.0.0, when a test that was asked for fails, the runner returns a failed result under the name asked for, with anNAp-value and the reason it failed. The substitute that runs in its place is returned under its own name (for examplewhite_simplified), andprint()andsummary()of the suite show both. Code that reads$white$p.valuecan then never get the value of another test. It changes what the runner returns, an API change, so it waits for 1.0.0 and is not part of 0.16.0. It settles the item on the fallback ofrunHeteroTests()under 0.16.0. Not implemented. -
distribution = "none"inperformWildBootstrapTest()(decided by the author on 2026-10-25). A third value ofdistribution, the plain residual bootstrap of the leverage-standardised, centred residuals, without a multiplier (v = 1), with"rademacher"kept as the default. Ininst/validation/wild-no-multiplier-size.Rit held the level with skewed errors from 50 observations, where both multipliers need about 200, at a cost of 1.4 points of power at 30 observations. It adds a value to an argument and changes no existing call; itsmethodstring should say that no multiplier is used. Not implemented. - Reference index of the documentation site (
tools/reference-index.R) grouped by the frozen API: tests by family (auxiliary regression, group-wise, rank-based, time series, panel and spatial, resampling), remediation, simulation, plotting and reporting. - Vignettes. There are eight, and
tutorial,using_heteroTestsandcomprehensive_guideoverlap. Consolidate them into a short set that does not:- getting started;
- choosing a test, turning the
t5and power tables into advice; - remediation;
- theory and references.
- Publish the stability policy from What 1.0.0 means in the README and on the documentation site.
-
NEWS.mdfor 1.0.0 with the complete old → new migration table. - Full
--as-cranon every CI flavour, plus win-builder, then submit. - Update the R Journal manuscript, kept in its own repository, to the 1.0 API and submit it. Submitting on the frozen API avoids publishing function names that are about to be deprecated.
After 1.0.0¶
- First-class panel and spatial heteroscedasticity workflows.
- Helpers that bridge detection to conditional-variance modelling (ARCH/GARCH) for the time-series path.
- An experiment/report artefact (model-card style) summarising a diagnostic run.
- Streaming for more tests where it is meaningful, under a single
chunk_threshold_mbpolicy. - Stronger feasible-WLS and auto-transform helpers: iterated feasible GLS, an estimator of the additive variance function that keeps its fitted variances positive, and a Box–Cox search.
- A form of
performVarianceFormTest()that does not need the standardized errors to have a constant fourth moment, following the robust statistic of Wooldridge (1991). - A variance-regressor argument for
performBPTest()andperformKoenkerTest(), thevarformulaoflmtest::bptest(), so that a fit with fixed effects for small groups, on which the tests warn since 0.16.0, can be tested against its regressors without the dummies. On the balanced designs ofinst/validation/fixed-effects-dummies.R, where the tests with the dummies reject up to 100% of the time at the 5% level, the squared residuals regressed onxalone kept the level: 4.6% to 5.5% for Koenker's statistic and 4.3% to 5.6% for the Breusch-Pagan statistic. Unbalanced designs are untested. A new argument. - The test of Juhl and Sosa-Escudero (2014, Journal of Econometrics 178(3), 484-494) of heteroskedasticity after fixed effects estimation, for panels of many units observed a fixed number of times, the setting where White's, Koenker's and the Breusch-Pagan tests warn. It needs the user to name the factor that is the unit, which no test of the package takes today. A new export; its reference implementation, and whether it requires a balanced panel, are to be checked first (only the abstract could be read in 0.16.0).
- A single parametrised accuracy-validation test, with reference coverage
broadened to
skedasticandsandwich. - The companion package for the dashboard and the benchmark suite (decided on 2026-10-14; not yet named).
Candidate tests¶
None of these is scheduled. Each would be a new export, so each has to clear
the release gate in inst/validation/README.md before it ships: agreement to
1e-8 with an established implementation or with a reconstruction of the
primary reference, simulated size inside the gate, and power against the
alternative it is built for.
The list is arranged by where the test can be found today, because that decides both what there is to validate against and what the package would add. Availability was checked on 2026-10-09 by searching CRAN, the R documentation sites and the web for each test by name. "No R implementation found" reports that search. It is not proof that none exists.
No R implementation found¶
A first implementation in R. Where another system has one, it is the reference; where none does, the guard is a reconstruction of the primary reference and simulated size.
-
Modified Wald test for groupwise heteroscedasticity in a fixed-effects panel (Greene 2000; Baum 2001). The panel tests cover an individual effect and cross-sectional dependence, not unequal variances across units. Reference implementation: Stata's
xttest3. The Python packagepanelbox(1.0.2) has a function of that name that computes a different statistic, a likelihood-ratio form. A second search on 2026-10-10 found no R implementation either. Measured for 0.13.0 and not adopted: it does not hold its level. In the designs ofinst/validation/modified-wald-size-power.R, at 5000 replications, the test asxttest3computes it rejects a true null hypothesis at the 5% level this often:Errors Units T = 5 T = 10 T = 30 T = 100 Gaussian 10 0.927 0.681 0.282 0.100 Gaussian 30 1.000 0.966 0.584 0.183 Gaussian 100 1.000 1.000 0.928 0.361 t5 10 0.966 0.876 0.621 0.367 t5 30 1.000 0.999 0.944 0.717 t5 100 1.000 1.000 1.000 0.982 Two unbalanced panels are at 0.809 and 1.000 under Gaussian errors. Each unit's term is divided by a variance estimated from the same few residuals, and the excess grows with the number of units. In long panels the error reverses, because the statistic tends to a chi-squared with one degree of freedom fewer than its reference: with 2 units and 1000 periods it rejects 0.014 of the time. Three of the study's 40 null cells fall inside the release gate, all at 300 or 1000 periods, where the two errors happen to cancel, and under
t5errors it still rejects 0.129 of the time with 10 units and 0.235 with 30 at 1000 periods. Before a test of this hypothesis can ship it needs a different variance estimate or a small-sample correction, which makes it a different test. Two leads, neither part of the study: Bartlett's test on the within residuals held its level under Gaussian errors in an exploratory run but, being a normal-theory test, not undert5errors; and Juhl and Sosa-Escudero (2014, Journal of Econometrics 178, 484-494) derive studentized tests for heteroscedasticity after fixed-effects estimation with the number of periods fixed, whose fit to this hypothesis has not been checked. Greene's page 598, which Baum follows, could not be obtained, so whether Greene's own form differs from Baum's was not checked. - A joint LM test of homoscedasticity in a one-way error-component model (Baltagi, Bresson and Pirotte 2006), against heteroscedasticity in the individual effect and in the remainder error together. - Pagan–Hall (1983), the test of constant variance after instrumental-variables estimation, where Breusch–Pagan and White are valid only under further conditions. Reference implementation: Stata'sivhettest. The package accepts no instrumental-variables fit today, so this one starts with the input layer. - Kelejian–Robinson (1998), a joint test for spatial autocorrelation and heteroscedasticity.sphet::kpjtest()is a different test by Kelejian and Piras. - Newey–Powell (1987), a test built on asymmetric least squares: under constant variance the expectile regressions have equal slopes.expectregfits expectiles and has no such test. - Tests in nonparametric regression, which have power against any smooth departure from constant variance and need no variance regressors to be named: Eubank and Thomas (1993), Dette and Munk (1998), Zheng (2009), and Chown and Müller (2018). The cost is a bandwidth or a difference sequence to choose. - A consistent test of a variance function: Dette, Neumeyer and Van Keilegom (2007), Wang and Zhou (2007), and Samarakoon and Song (2011).performVarianceFormTest()has power against the terms it is given; these have it against any departure, at the cost of smoothing. Stage 1 of the first measured for 0.13.0, and not exported: as defined, it does not hold its level. The 2007 article could not be obtained; a prototype in plain R follows the authors' submitted manuscript of 2005, whose Tables 4.1 and 4.5 it reproduces in level and to within a few points in power. In the designs ofinst/validation/variance-form-size-power.R, with the variance function in one variable and the mean a linear model, the Cramér-von Mises statistic is inside the release gate in 4 of its 12 Gaussian null cells at 5000 replications, all four with constant variance at 150 or 400 observations; it rejects a true exponential or power variance function 0.019 to 0.036 of the time at 50 to 400 observations. The bootstrap draws its data from the least-squares fit of the variance function, on which the statistic's null distribution depends: drawn from the true shape, on the same samples, three of the four such cells come to 0.047 to 0.051. Against a departure the added terms ofperformVarianceFormTest()do not span it has the power that test lacks, 0.750 against 0.089 at 400 observations; against the other alternatives it has less. Design note, prototype and evidence:inst/validation/variance-form-dnvk-design.mdand thevariance-form-dnvk-*files beside it. What to do next is under Decisions needed. Wang and Zhou (2007) assume a parametric mean, which is this package's setting; Samarakoon and Song (2011) could not be obtained. Stage 1 of Wang and Zhou (2007) is parked on the branchfeat/wang-zhou-stage1, to be resumed after 1.0.0: pass 1 of its size and power study is done, and the Gaussian null cells at 5000 replications that decide it have not been run. The branch'swip/wang-zhou-stage1/README.mdsays how to resume. - Bai, Pan and Yin (2018), a test of homoscedasticity when the number of regressors grows with the sample. Li–Yao, below, is a different test for the same setting. - A design-based test of constant variance for survey data. The tests assume an independent sample with equal weights, andrunSurveyHeteroTests()rejects a homoscedastic model 37% to 100% of the time when selection depends on the response or the sample is clustered. The natural candidates regress the squared residuals of asurvey::svyglm()fit on the variance regressors using the design, as a Wald test (survey::regTermTest()) or as a Rao score test. Both were measured ininst/validation/survey-designs-size.Rand neither clears the gate: 6% to 25% under the null at 300 and 1000 observations when the weights vary with the regressor. The variance estimate needs a small-sample correction first.
In another R package, not in this one¶
These add no test to R. They add it to the common interface, and each comes with a reference implementation to agree with.
- Sign and size bias tests (Engle and Ng 1993): whether the squared
residuals respond differently to negative and to positive lagged residuals.
The time-series tests detect ARCH effects and say nothing about asymmetry.
rugarch::signbias()computes the tests for a fitted GARCH model. - A break in the variance at an unknown date: the CUSUM of squares test
(Brown, Durbin and Evans 1975), the statistic of Inclán and Tiao (1994), or
the kappa tests of Sansó, Aragó and Carrion (2004), which stay valid under
heavy tails and conditional heteroscedasticity. Goldfeld–Quandt and Szroeter
need the ordering and the split to be chosen in advance. Reference
implementation for the last:
micss::kappa_test(). - Simonoff–Tsai (1994) and Verbyla (1993). Versions of the Cook–Weisberg
score test built on modified profile likelihood and on residual maximum
likelihood, for small samples and high-leverage designs. Reference
implementations:
skedastic::simonoff_tsai()andskedastic::verbyla(). - Bickel (1978) and Anscombe (1961). Tests of the variance against the
fitted values. Bickel's is the robust one. Reference implementations:
skedastic::bickel()andskedastic::anscombe(). - Li–Yao (2019) for regressions with many regressors.
performHighDimensionalTest()regresses the squared residuals on principal components and cites no heteroscedasticity test as its source. Reference implementation:skedastic::li_yao(). - A spatial Breusch–Pagan test (Anselin 1988) for fitted spatial models.
performSpatialHeteroTest()applies Moran's I to the squared residuals, which detects variance that clusters in space and does not test it against regressors. Reference implementation:spatialreg::bptest.Sarlm(). - The rest of
skedastic. Of its 25 tests, the package has counterparts for eight. Besides those named above,skedasticprovidesbamset(),carapeto_holt(),diblasi_bowman(),dufour_etal(),evans_king(),godfrey_orme(),honda(),horn(),rackauskas_zuokas(),wilcox_keselman(),yuce()andzhou_etal(). None is a priority by itself; each is a candidate when a user asks for it.
Order of work¶
The first group is where the package can contribute something R does not have, which is also what a reader of the article asks of it. Within that group:
- The modified Wald test. The panel functions exist, and it is the test
Stata users look for under the name
xttest3. Measured for 0.13.0 and not adopted, because it does not hold its level (above). - The Dette, Neumeyer and Van Keilegom test. It removes the main limitation
of
performVarianceFormTest(), and is the most work of the two. Stage 1 done for 0.13.0: as the manuscript defines it, it does not hold its level when the variance is not constant (above).
Done¶
- A Glejser test that is valid under skewed errors (Im 2000; Machado and
Santos Silva 2000), in 0.13.0, as
performGlejserTest(robust = TRUE). In the designs ofinst/validation/glejser-skewed-errors.Rthe uncorrected statistic rejects a true null hypothesis 9% to 18% of the time at the 5% level when the errors are skewed, at every sample size, andskedastic::glejser()uses the uncorrected absolute residuals as well. The corrected statistic is the default since 0.15.0 (the 0.15.0 milestone, above). - Harrison–McCabe (1979), in 0.13.0, as
performHarrisonMcCabeTest(), beside Goldfeld–Quandt and Szroeter. The 1979 article was not read; the definition rests on Harrison (1981), by its first author, whose published exact critical values the function reproduces, and onlmtest::hmctest(), with which the statistic agrees to1e-8. The p-value is exact under normal errors given the regressors. Neither reference computes that p-value:lmtest::hmctest()simulates it from a regression on a constant alone, andskedastic::harrison_mccabe()2.0.3 drops repeated eigenvalues and, withdeflator, leaves the design in the order of the rows. In the designs ofinst/validation/harrison-mccabe-size-power.Rit holds its level under Gaussian errors, rejects 12% to 16% of the time undert5errors as the other two ordered tests do, and has less power than either.
Decisions needed¶
| Question | Proposal | Needed by |
|---|---|---|
Ties in the ordering of performSzroeterTest() |
Average the ranks within ties and adjust the null variance for them. Today ties keep the order of the rows the model was fitted to, which holds the level but makes the statistic depend on that order when order_by is discrete, and for the registered test on a model whose design rows repeat, as with factors, binary or other discrete regressors. It changes the values the function returns. Since 0.16.0 the null mean and variance come from the design, and they hold for any weights: with averaged ranks a_i in place of i, h = sum a_i e_(i)^2 / sum e_(i)^2 has E h = tr(M A) / nu and Var h = 2 (nu tr(M A M A) - tr(M A)^2) / (nu^2 (nu + 2)) with A = diag(a_i), so the averaged ranks carry into the moments with no separate adjustment; rszroeter_moments(), which takes 1, ..., n today, would take the a_i |
0.19.0 |
| Supported R floor | Test the floor in CI, or raise it to oldrel-1 | 0.18.0 |
| The name of the companion package for the dashboard and the benchmark suite | None recorded. Once it has one, launchDiagnosticDashboard(), run_benchmark_suite() and generate_benchmark_report() warn here for a minor release, naming it, and the code moves |
0.19.0 |
| A consistent test of the variance function after stage 1 | Do not ship the Dette, Neumeyer and Van Keilegom test as defined: its prototype rejects a true non-constant variance function 0.019 to 0.036 of the time. Choose between a new stage 1 for a modified procedure (a better estimate of the variance function for the bootstrap data, or a studentized statistic) and one for Wang and Zhou (2007), and compare the published article with the manuscript first. If a test ships, a function of its own rather than performVarianceFormTest(against = "any") |
Before any stage 2 |
setup.sh / renv / Docker |
Keep Docker for reproducibility; drop setup.sh now CRAN is the install route |
0.18.0 |
Register the tests now callable as (model, data) |
None recorded. Since 0.15.0 performArchLMTest(), performMcLeodLiTest(), performSpearmanTest(), performDavidianCarrollTest() and performHarveyTest() can be called as (model, data) with no further argument, which is when a test goes in R/diagnosticsRegistry.R; registering one adds a name runHeteroTests() accepts and changes the article |
1.0.0 |