fix(stats): keep regression inference alive when squared deviations underflow
This commit is contained in:
@@ -239,3 +239,112 @@ func mustMatrix(t *testing.T, vals []float64, r, c int) *core.Array {
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// TestLinearRegressionTinyScaleInference pins the inference of a response
|
||||
// on a scale whose squared residuals fall below the subnormal floor: the
|
||||
// plain residual and total sums of squares read zero there, and the fit
|
||||
// used to report the evidence backwards, R² of 1 with an infinite t and
|
||||
// p = 0 beside an F of zero with p = 1. The response y = [0, 0, e] over
|
||||
// x = 1, 2, 3 keeps every least-squares quantity exactly representable
|
||||
// while both sums of squares underflow: slope e/2, residual sum e²/6,
|
||||
// total 2e²/3, (XᵀX)⁻¹₁₁ = 1/2, so SE(slope) = e/(2√3), t = √3,
|
||||
// p = 1/3, F = 3 and R² = 3/4, all closed fractions.
|
||||
func TestLinearRegressionTinyScaleInference(t *testing.T) {
|
||||
const e = 1e-200
|
||||
x := mustMatrix(t, []float64{1, 1, 1, 2, 1, 3}, 3, 2)
|
||||
y := mustFloats(t, []float64{0, 0, e}, 3)
|
||||
res, err := LinearRegression(x, y)
|
||||
if err != nil {
|
||||
t.Fatalf("LinearRegression: %v", err)
|
||||
}
|
||||
if math.Abs(res.Coefficients[1]-e/2) > 1e-12*e/2 {
|
||||
t.Fatalf("slope = %.17g, want %.17g", res.Coefficients[1], e/2)
|
||||
}
|
||||
wantSE := e / (2 * math.Sqrt(3))
|
||||
se := res.StandardErrors[1]
|
||||
if !(se > 0) || math.IsInf(se, 0) {
|
||||
t.Fatalf("slope standard error = %g beside nonzero residuals, want %.17g", se, wantSE)
|
||||
}
|
||||
if math.Abs(se-wantSE) > 1e-12*wantSE {
|
||||
t.Fatalf("slope standard error = %.17g, want %.17g", se, wantSE)
|
||||
}
|
||||
if math.Abs(res.TStatistics[1]-math.Sqrt(3)) > 1e-12 {
|
||||
t.Fatalf("t = %.17g, want √3", res.TStatistics[1])
|
||||
}
|
||||
if math.Abs(res.PValues[1]-1.0/3) > 1e-12 {
|
||||
t.Fatalf("p = %.17g, want 1/3", res.PValues[1])
|
||||
}
|
||||
if math.Abs(res.RSquared-0.75) > 1e-12 {
|
||||
t.Fatalf("R² = %.17g, want 3/4", res.RSquared)
|
||||
}
|
||||
if math.Abs(res.AdjustedRSquared-0.5) > 1e-12 {
|
||||
t.Fatalf("adjusted R² = %.17g, want 1/2", res.AdjustedRSquared)
|
||||
}
|
||||
if math.Abs(res.FStatistic-3) > 1e-11 {
|
||||
t.Fatalf("F = %.17g, want 3", res.FStatistic)
|
||||
}
|
||||
if math.Abs(res.FPValue-1.0/3) > 1e-11 {
|
||||
t.Fatalf("F p-value = %.17g, want 1/3", res.FPValue)
|
||||
}
|
||||
|
||||
// Unit weights are the same fit, weighted statistics included.
|
||||
w := mustFloats(t, []float64{1, 1, 1}, 3)
|
||||
wres, err := WeightedLinearRegression(x, y, w)
|
||||
if err != nil {
|
||||
t.Fatalf("WeightedLinearRegression: %v", err)
|
||||
}
|
||||
if math.Abs(wres.TStatistics[1]-math.Sqrt(3)) > 1e-12 {
|
||||
t.Fatalf("weighted t = %.17g, want √3", wres.TStatistics[1])
|
||||
}
|
||||
if math.Abs(wres.RSquared-0.75) > 1e-12 {
|
||||
t.Fatalf("weighted R² = %.17g, want 3/4", wres.RSquared)
|
||||
}
|
||||
if math.Abs(wres.FStatistic-3) > 1e-11 {
|
||||
t.Fatalf("weighted F = %.17g, want 3", wres.FStatistic)
|
||||
}
|
||||
if math.Abs(wres.FPValue-1.0/3) > 1e-11 {
|
||||
t.Fatalf("weighted F p-value = %.17g, want 1/3", wres.FPValue)
|
||||
}
|
||||
|
||||
// A response with a live scale beside the tiny spread keeps the same
|
||||
// behaviour: the slope's own rounding leaves residuals near its last
|
||||
// ulp, and the standard error must stay representable and the t
|
||||
// finite rather than answer an exact fit the residuals contradict.
|
||||
const a = 1e-160
|
||||
step := math.Nextafter(3*a, math.Inf(1)) - 3*a
|
||||
y2 := mustFloats(t, []float64{a, 2 * a, 3*a + step}, 3)
|
||||
res2, err := LinearRegression(x, y2)
|
||||
if err != nil {
|
||||
t.Fatalf("LinearRegression: %v", err)
|
||||
}
|
||||
if se2 := res2.StandardErrors[1]; !(se2 > 0) || math.IsInf(se2, 0) {
|
||||
t.Fatalf("slope standard error = %g beside nonzero residuals, want a representable value", se2)
|
||||
}
|
||||
if math.IsInf(res2.TStatistics[1], 0) {
|
||||
t.Fatalf("t = %g beside nonzero residuals, want a finite statistic", res2.TStatistics[1])
|
||||
}
|
||||
if res2.FPValue > 1e-10 || res2.PValues[1] > 1e-10 {
|
||||
t.Fatalf("p = %g, F p = %g, want the far tail both", res2.PValues[1], res2.FPValue)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLinearRegressionTinyScaleExactLine pins the fully underflowed
|
||||
// corner: an exact line at a scale where both the residual and the total
|
||||
// sums of squares fall below the subnormal floor. The t statistics
|
||||
// already answer the exact fit with infinite evidence; the F test must
|
||||
// agree with them instead of reporting zero evidence.
|
||||
func TestLinearRegressionTinyScaleExactLine(t *testing.T) {
|
||||
const a = 1e-300
|
||||
x := mustMatrix(t, []float64{1, 1, 1, 2, 1, 3}, 3, 2)
|
||||
y := mustFloats(t, []float64{a, 2 * a, 3 * a}, 3)
|
||||
res, err := LinearRegression(x, y)
|
||||
if err != nil {
|
||||
t.Fatalf("LinearRegression: %v", err)
|
||||
}
|
||||
if res.FPValue > 1e-10 {
|
||||
t.Fatalf("F p-value = %g on an exact line at a tiny scale, want the far tail beside the infinite t", res.FPValue)
|
||||
}
|
||||
if res.PValues[1] > 1e-10 {
|
||||
t.Fatalf("slope p-value = %g, want the exact-fit report", res.PValues[1])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user