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.15.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: the next submission is 1.0.0 (decided 2026-10-11).
- 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. - 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. What stands between 0.15.0 and 1.0.0 is the evidence and internals of 0.16.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.
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.16.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: statistics evidence, dependencies and internals (no API change)¶
- 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. - 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. -
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. -
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. - 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. - 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 that
MERGErefuses 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. -
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. -
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. -
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. - 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. - 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. - 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. - 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. - 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. -
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. -
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. - 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. - 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.-
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.-
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 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 such data give the value on the data of the fit. Changes a value for such data. 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. 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).- 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. - 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.cut(x, quantile(x))in the formula makes Koenker's test stop with "factor has new levels", on main and with the guard alike.- 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. 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.- 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.
- 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. - A multivariate
lm,cbind(y, y2) ~ ..., fails with a coercion error instead of a clear refusal. No value changes. - The unexported
performWhiteTestEnhanced(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. - 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.
- 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.
-
- Dependencies.
- Move
curltoSuggests: onlydownloadTeachingData()uses it. R6goes ifTestFactoryleaves the API, since it is the only user.Suggestsshrinks with the companion split.- Settle whether
digestbelongs inImportsorSuggests, depending on whether caching stays exported.
- Move
- Pass
R 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 in
R/with escapes (89 lines across 27 files). - Split
R/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. - Retire
TODO.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 under
skip_on_cran(), and the full versions run on CI.
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. - 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 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 |
0.16.0 |
| Supported R floor | Test the floor in CI, or raise it to oldrel-1 | 0.16.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.16.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.16.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 |