0096 ordinary least squares earns its own package - CyrilB1531/lodestar GitHub Wiki
0096 — Ordinary least squares earns its own package
Status: accepted · Date: 2026-09-10
Context
#566 asked for OLS with inference — not
the estimate, which is a solve, but the standard errors, t statistics, p-values, intervals,
adjusted R-squared, overall F and VIF a summary() table holds. It put two questions before any
code: whether that ships as a namespace inside Lodestar.Stats or as its own package, and whether
the gap it claims to fill is real.
The second question came with an instruction from
#442 and
decision 0074: read the
incumbent's exported surface through a MetadataLoadContext, not its README.
The reading
MathNet.Numerics 5.0.0 (MIT), lib/netstandard2.0: 336 exported types, 5 333 members.
Every regression entry point returns coefficients. MultipleRegression.QR, .Svd,
.NormalEquations, .DirectMethod return Vector<T>, Matrix<T> or T[];
SimpleRegression.Fit returns a (double, double); WeightedRegression.Weighted and .Local the
same shapes. GoodnessOfFit adds five whole-model scalars — RSquared, R,
CoefficientOfDetermination, StandardError(modelled, observed, dof) and
PopulationStandardError.
Searching the assembly rather than that namespace found the nuance a README would have hidden. A coefficient covariance matrix does exist in MathNet:
MathNet.Numerics.Optimization.NonlinearMinimizationResult
Matrix<T> Correlation
Matrix<T> Covariance
Vector<T> StandardErrors
It belongs to the non-linear least-squares minimisers and is unreachable from
LinearRegression. Past it the chain stops even for its own callers: across 5 333 exported members
there is no t statistic, no p-value, no interval on a coefficient, no adjusted R-squared, no
overall F and no VIF.
Accord.Statistics 3.8.0: 561 exported types, 4 796 members, none unresolvable.
Analysis.MultipleLinearRegressionAnalysis really does export the whole table — StandardErrors,
Confidences, FTest, ZTest, RSquareAdjusted, InformationMatrix, an ANOVA Table, and a
Coefficients collection whose row carries Value, StandardError, TTest, ConfidenceLower
and ConfidenceUpper. No VIF, in either assembly.
Only declarations were read; no method body of either assembly was opened, which is what decision 0003 constrains.
So #442's sentence — "nobody in .NET does inference" — is false as written, and this record
replaces it. Accord did it, completely, and stopped: accord-net/framework is archived, last
release 2017-10-19, last push 2020-11-18, LGPL-2.1. MathNet, the maintained option, stops at
the point estimate. The gap is a maintained, permissively licensed, framework-free OLS table.
Decision
Lodestar.Stats.Regression, a new core-tier package, rather than a namespace inside
Lodestar.Stats.
The audience is the argument. Lodestar.Stats is ten hypothesis-test families; a caller who wants
a regression table wants none of them, and a caller running a Kruskal-Wallis wants no QR. That is
the split #427 asks for — "a distinct
audience, or a distinct release cadence" — and it is the trigger
decision 0081 wrote its own escape hatch for,
spent in decision 0095.
Two edges, both to core packages, nothing external: Lodestar.Stats 0.2.0 for the Student and
Fisher tails, Lodestar.Decomposition 0.2.0 for the Householder QR. Core tier holds
(decision 0076) — and would have failed
immediately had this taken Accord instead, whatever its license said.
Solved through the QR, not the normal equations. Forming XᵀX squares the condition number,
and the near-collinear designs a VIF exists to report are exactly the ones that costs. The
covariance is read as σ²R⁻¹R⁻ᵀ, whose diagonal is the row norms of R⁻¹.
Two things parity turned out to mean
statsmodels 0.15.0 changed what a VIF is.
stats.outliers_influence.variance_inflation_factor now takes standardize=True by default: each
column is centred and scaled to unit spread before the auxiliary regression, with a std > 1e-10
mask that exempts the constant column, and the resulting R-squared is clipped to 1 - 1e-15. With
an intercept this changes nothing — an affine change of the regressors leaves R-squared alone once
a constant absorbs the shift — and the corpus's four intercept cases passed before this was
implemented. Without an intercept it is the whole answer: 87.43 on the naive reading against
the reference's 21.0. Reproduced, because the corpus is frozen from the reference and a VIF that
disagrees with statsmodels by a factor of four is not a VIF.
One divergence, deliberate. A single regressor fitted with no intercept leaves the auxiliary
regression with an empty design, and statsmodels raises a ValueError out of numpy.
OlsSummary.VarianceInflationFactors[0] is NaN there instead. The rest of the table is well
defined — the estimate, its error, its p-value and its interval are all sound — and one undefined
diagnostic should not sink a fit that is otherwise correct. docs/equivalence.md carries the row.
Consequences
- A thirteenth package, with the fixed cost #566 measured: five hard-coded pack lists across
ci.ymlandsonarcloud.yml, both release workflows' allow-lists,Version.props, theEXPECTEDentry incheck_nuspec_dependencies.py, twoFLOORSrows incheck_version_floor.py, the*.NetStandard.Testsmirror with all fourLodestar.*assemblies pinned byProjectReference(#529), awiki-map.jsonentry, reference pages, samples,equivalence.mdrows and a guide. statsmodels0.15.0 (BSD-3-Clause) joinstools/requirements.txtand the hashed lock, which grows bypandas,patsy,formulaicand their leaves. It fails decision 0078's test forrequirements-nodeps.txt, since pandas is not already on the lock's graph. The 126 existing corpora were regenerated against the new graph and are byte-identical.- A benchmark against a named incumbent, because the reading found one. #566 expected this
section to record that no .NET incumbent exists; the reading above says otherwise, so
bench/Lodestar.Stats.Benchmarksmeasures againstAccord.Statistics'MultipleLinearRegressionAnalysisinstead. The plumbing is already there — section 18 ofbench/README.mdbenchmarksLodestar.Statsagainst the same package, and a benchmark project is not shipped, so the LGPL-2.1 that bars Accord fromsrc/does not bar it frombench/. Being archived is a reason to prefer a maintained implementation, not a reason to skip the measurement. The result inverts with size and is reported that way: 2.7× faster at 100 rows, 1.33× slower at 10 000, because Accord's table has no VIF and this one does — four regressors mean five Householder factorisations here against Accord's single solve. A caller who wants the table without the diagnostic has no way to say so today; that is the next measurement, not a defect of this decision. - #338 proposes GLMs and econometric summaries under the same name. This package is OLS and its table; anything with a link function or a time index falls on that issue's side of the line.