fix(stats): hold the Student-t tails past t-squared overflow
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -28,6 +28,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- `Viterbi` refuses a sequence of probability zero under the model,
|
||||
the same refusal `Forward` makes, instead of returning a meaningless
|
||||
path beside a log probability of -Inf.
|
||||
- The Student-t tails stay accurate past the point t squared
|
||||
overflows float64: `StudentTCDF`, the regression coefficient
|
||||
p-values and `NoncentralTCDF` at extreme t answer the tail the
|
||||
format still holds instead of a silent zero or an error naming a
|
||||
NaN.
|
||||
|
||||
## [1.0.0] - 2026-09-03
|
||||
|
||||
|
||||
+11
-5
@@ -269,15 +269,18 @@ func StudentTCDF(t float64, df int) (float64, error) {
|
||||
if df < 1 {
|
||||
return 0, base.Errf("StudentTCDF: df must be ≥ 1, got %d", df)
|
||||
}
|
||||
z := float64(df) / (float64(df) + t*t)
|
||||
upper, err := BetaIncomplete(z, float64(df)/2, 0.5)
|
||||
// One house tail: the upper-tail helper carries the asymptotic forms
|
||||
// the heavy df ≤ 2 laws keep past t²'s overflow, where the closed
|
||||
// form's z = df/(df+t²) collapses to 0 and the lower tail answered a
|
||||
// silent 0 for a tail the format still holds.
|
||||
upper, err := studentTUpperTail(math.Abs(t), df)
|
||||
if err != nil {
|
||||
return 0, base.Errf("StudentTCDF: %w", err)
|
||||
}
|
||||
if t >= 0 {
|
||||
return 1 - upper/2, nil
|
||||
return 1 - upper, nil
|
||||
}
|
||||
return upper / 2, nil
|
||||
return upper, nil
|
||||
}
|
||||
|
||||
// PoissonCDF returns P(N ≤ k) for N ~ Poisson(lambda), through the
|
||||
@@ -701,7 +704,10 @@ func studentTUpperTail(t float64, df int) (float64, error) {
|
||||
// huge t keeps accurate.
|
||||
return 1 / (math.Pi * t), nil
|
||||
case df == 2:
|
||||
return 1 / (2 * t * t), nil
|
||||
// The tail 1/(2t²) divided one t at a time: the literal
|
||||
// denominator overflows past √MaxFloat64, and the quotient
|
||||
// would flush the still-representable tail to zero.
|
||||
return 0.5 / t / t, nil
|
||||
}
|
||||
}
|
||||
z := float64(df) / (float64(df) + t*t)
|
||||
|
||||
@@ -276,3 +276,56 @@ func TestGammaLowerLargeShape(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStudentTCDFSquareOverflowTail pins the Student laws past the square
|
||||
// overflow: a t beyond √MaxFloat64 drives z = df/(df+t²) through Inf/Inf
|
||||
// to the NaN and 0 the closed form answers, while the heavy df = 1 and
|
||||
// df = 2 tails are still representable there. The lower tail and the
|
||||
// two-sided tail must answer their asymptotic forms, exactly as the
|
||||
// one-sided upper tail already does.
|
||||
func TestStudentTCDFSquareOverflowTail(t *testing.T) {
|
||||
const huge = 1e155
|
||||
// df = 1 (Cauchy): the one-sided tail is 1/(π·t), two-sided 2/(π·t).
|
||||
one, err := StudentTCDF(-huge, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("StudentTCDF(-1e155, 1): %v", err)
|
||||
}
|
||||
if want := 1 / (math.Pi * huge); math.Abs(one-want) > 1e-12*want {
|
||||
t.Fatalf("StudentTCDF(-1e155, 1) = %.17g, want the Cauchy tail %.17g", one, want)
|
||||
}
|
||||
two, err := twoSidedT(huge, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("twoSidedT(1e155, 1): %v", err)
|
||||
}
|
||||
if want := 2 / (math.Pi * huge); math.Abs(two-want) > 1e-12*want {
|
||||
t.Fatalf("twoSidedT(1e155, 1) = %.17g, want the Cauchy tail %.17g", two, want)
|
||||
}
|
||||
// df = 2: the two-sided tail is 1 − t/√(t²+2), the one-sided half of
|
||||
// it, both ≈ 1/t² here and still inside the subnormal range. The
|
||||
// reference is assembled as u/(√(1+u)+1) with u = 2/t², the form
|
||||
// that never squares t.
|
||||
const u = 2e-310 // 2/t² at t = 1e155
|
||||
two2, err := twoSidedT(huge, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("twoSidedT(1e155, 2): %v", err)
|
||||
}
|
||||
if want := u / (math.Sqrt(1+u) + 1); math.Abs(two2-want) > 1e-6*want {
|
||||
t.Fatalf("twoSidedT(1e155, 2) = %.17g, want the df 2 tail %.17g", two2, want)
|
||||
}
|
||||
one2, err := StudentTCDF(-huge, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("StudentTCDF(-1e155, 2): %v", err)
|
||||
}
|
||||
if want := 0.5 * u / (math.Sqrt(1+u) + 1); math.Abs(one2-want) > 1e-6*want {
|
||||
t.Fatalf("StudentTCDF(-1e155, 2) = %.17g, want the df 2 tail %.17g", one2, want)
|
||||
}
|
||||
// The upper half of the axis keeps answering 1, and a df whose tail
|
||||
// genuinely underflows keeps answering 0: both are the honest
|
||||
// roundings there.
|
||||
if v, err := StudentTCDF(huge, 1); err != nil || v != 1 {
|
||||
t.Fatalf("StudentTCDF(1e155, 1) = %v (%v), want 1", v, err)
|
||||
}
|
||||
if v, err := StudentTCDF(-huge, 4); err != nil || v != 0 {
|
||||
t.Fatalf("StudentTCDF(-1e155, 4) = %v (%v), want the underflowed 0", v, err)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-1
@@ -304,7 +304,14 @@ func NoncentralTCDF(t float64, df int, delta float64) (float64, error) {
|
||||
magnitude = -t
|
||||
shift = -delta
|
||||
}
|
||||
x := magnitude * magnitude / (magnitude*magnitude + float64(df))
|
||||
magnitude2 := magnitude * magnitude
|
||||
x := magnitude2 / (magnitude2 + float64(df))
|
||||
if math.IsInf(magnitude2, 1) {
|
||||
// A finite t whose square overflows drove the quotient through
|
||||
// Inf/Inf into a NaN the incomplete beta refused under its own
|
||||
// name. The beta argument's limit there is exactly 1.
|
||||
x = 1
|
||||
}
|
||||
if x == 0 {
|
||||
// t = 0: the value collapses to Φ(−δ) exactly.
|
||||
return NormalCDF(-delta), nil
|
||||
|
||||
@@ -219,6 +219,36 @@ func TestNoncentralUnderflowSurvival(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestNoncentralTCDFHugeFiniteT pins the far corner of the signed axis: a
|
||||
// finite t whose square overflows drives the beta argument to Inf/Inf, a
|
||||
// NaN the incomplete beta refused under its own name. The CDF there is 1
|
||||
// below rounding for t on the δ side and 0 above it, the same limits the
|
||||
// central law answers.
|
||||
func TestNoncentralTCDFHugeFiniteT(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
tv float64
|
||||
df int
|
||||
delta float64
|
||||
want float64
|
||||
}{
|
||||
{1e200, 3, 2, 1},
|
||||
{1e155, 1, 0.5, 1},
|
||||
{-1e200, 5, 1, 0},
|
||||
{-1e155, 2, -3, 0},
|
||||
} {
|
||||
got, err := NoncentralTCDF(c.tv, c.df, c.delta)
|
||||
if err != nil {
|
||||
t.Fatalf("NoncentralTCDF(%g, %d, %g): %v", c.tv, c.df, c.delta, err)
|
||||
}
|
||||
if math.IsNaN(got) || got < 0 || got > 1 {
|
||||
t.Fatalf("NoncentralTCDF(%g, %d, %g) = %g, want a probability", c.tv, c.df, c.delta, got)
|
||||
}
|
||||
if math.Abs(got-c.want) > 1e-15 {
|
||||
t.Fatalf("NoncentralTCDF(%g, %d, %g) = %.17g, want %g", c.tv, c.df, c.delta, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNoncentralTIdentityReductions pins the exact corners: δ = 0 is
|
||||
// the central Student t, t = 0 is Φ(−δ), and the two reflection
|
||||
// identities of the law hold to rounding.
|
||||
|
||||
+6
-4
@@ -335,14 +335,16 @@ func LinearRegression(x, y *core.Array) (*LinearRegressionResult, error) {
|
||||
// freedom, by the closed-form tail I_z(df/2, 1/2) with z = df/(df+t²).
|
||||
// The identity is used rather than 2·(1 − T_cdf(t)): near t = 0 the
|
||||
// subtraction cancels catastrophically, while the incomplete beta
|
||||
// stays accurate into the far tail where p values matter most.
|
||||
// stays accurate into the far tail where p values matter most. The
|
||||
// upper-tail helper carries it, so the asymptotic forms that hold the
|
||||
// df ≤ 2 tails past t²'s overflow serve here too: the bare closed form
|
||||
// answers a silent 0 there while the true tail is still representable.
|
||||
func twoSidedT(t float64, df int) (float64, error) {
|
||||
z := float64(df) / (float64(df) + t*t)
|
||||
p, err := BetaIncomplete(z, float64(df)/2, 0.5)
|
||||
upper, err := studentTUpperTail(math.Abs(t), df)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return p, nil
|
||||
return 2 * upper, nil
|
||||
}
|
||||
|
||||
// hasConstantColumn reports whether an (n, p) design holds a column of
|
||||
|
||||
Reference in New Issue
Block a user