SPB Git

spb/metrika Public

Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift — no Electron, no Python runtime, no compromises.

Swift 92.4% HTML 3.3% R 3% Shell 1.3%
7.7 KB · 216 lines swift
Raw Blame History
1//2//  Distributions.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Foundation1112/// Special functions and distribution CDFs used for inference output.13/// Implementations follow the classic Lanczos / continued-fraction14/// formulations and are accurate to well beyond the 1e-10 project15/// tolerance for the parameter ranges used in regression output.16public enum ZQDistributions {1718    /// Natural log of the gamma function (Lanczos approximation, g=7).19    public static func logGamma(_ x: Double) -> Double {20        precondition(x > 0, "logGamma requires x > 0")21        let coefficients: [Double] = [22            676.5203681218851, -1259.1392167224028, 771.32342877765313,23            -176.61502916214059, 12.507343278686905, -0.13857109526572012,24            9.9843695780195716e-6, 1.5056327351493116e-7,25        ]26        if x < 0.5 {27            // Reflection formula.28            return log(.pi / sin(.pi * x)) - logGamma(1 - x)29        }30        let z = x - 131        var sum = 0.9999999999998099332        for (i, c) in coefficients.enumerated() {33            sum += c / (z + Double(i) + 1)34        }35        let t = z + 7.536        return 0.5 * log(2 * .pi) + (z + 0.5) * log(t) - t + log(sum)37    }3839    /// Regularized incomplete beta function I_x(a, b) via the Lentz40    /// continued-fraction algorithm.41    public static func incompleteBeta(a: Double, b: Double, x: Double) -> Double {42        precondition(a > 0 && b > 0, "incompleteBeta requires a, b > 0")43        if x <= 0 { return 0 }44        if x >= 1 { return 1 }4546        let logBeta = logGamma(a + b) - logGamma(a) - logGamma(b)47        let front = exp(logBeta + a * log(x) + b * log(1 - x))4849        // Use the symmetry relation to keep the continued fraction in its50        // rapidly converging region.51        if x < (a + 1) / (a + b + 2) {52            return front * betaContinuedFraction(a: a, b: b, x: x) / a53        } else {54            return 1 - front * betaContinuedFraction(a: b, b: a, x: 1 - x) / b55        }56    }5758    private static func betaContinuedFraction(a: Double, b: Double, x: Double) -> Double {59        let tiny = 1e-30060        let epsilon = 1e-1661        let qab = a + b62        let qap = a + 163        let qam = a - 16465        var c = 1.066        var d = 1 - qab * x / qap67        if abs(d) < tiny { d = tiny }68        d = 1 / d69        var h = d7071        for m in 1...300 {72            let dm = Double(m)73            // Even step.74            var numerator = dm * (b - dm) * x / ((qam + 2 * dm) * (a + 2 * dm))75            d = 1 + numerator * d76            if abs(d) < tiny { d = tiny }77            c = 1 + numerator / c78            if abs(c) < tiny { c = tiny }79            d = 1 / d80            h *= d * c81            // Odd step.82            numerator = -(a + dm) * (qab + dm) * x / ((a + 2 * dm) * (qap + 2 * dm))83            d = 1 + numerator * d84            if abs(d) < tiny { d = tiny }85            c = 1 + numerator / c86            if abs(c) < tiny { c = tiny }87            d = 1 / d88            let delta = d * c89            h *= delta90            if abs(delta - 1) < epsilon { break }91        }92        return h93    }9495    /// Standard normal CDF.96    public static func normalCDF(_ z: Double) -> Double {97        0.5 * erfc(-z / 2.0.squareRoot())98    }99100    /// Student t CDF with `df` degrees of freedom.101    public static func studentTCDF(_ t: Double, df: Double) -> Double {102        precondition(df > 0, "studentTCDF requires df > 0")103        let x = df / (df + t * t)104        let tail = 0.5 * incompleteBeta(a: df / 2, b: 0.5, x: x)105        return t > 0 ? 1 - tail : tail106    }107108    /// Two-sided p-value for a t statistic, computed directly from the109    /// tail integral: 2·P(T > |t|) = I_{df/(df+t²)}(df/2, 1/2). Going110    /// through `2·(1 − CDF)` would lose all precision below ~1e-16.111    public static func tTestPValue(_ t: Double, df: Double) -> Double {112        incompleteBeta(a: df / 2, b: 0.5, x: df / (df + t * t))113    }114115    /// F distribution CDF with (df1, df2) degrees of freedom.116    public static func fCDF(_ f: Double, df1: Double, df2: Double) -> Double {117        precondition(df1 > 0 && df2 > 0, "fCDF requires positive df")118        if f <= 0 { return 0 }119        let x = df1 * f / (df1 * f + df2)120        return incompleteBeta(a: df1 / 2, b: df2 / 2, x: x)121    }122123    /// Upper-tail p-value for an F statistic, computed directly as124    /// I_{df2/(df2+df1·f)}(df2/2, df1/2) to preserve precision for large F.125    public static func fTestPValue(_ f: Double, df1: Double, df2: Double) -> Double {126        if f <= 0 { return 1 }127        return incompleteBeta(a: df2 / 2, b: df1 / 2, x: df2 / (df2 + df1 * f))128    }129130    /// Regularized lower incomplete gamma P(a, x), via the series for131    /// x < a+1 and the Lentz continued fraction otherwise.132    public static func incompleteGamma(a: Double, x: Double) -> Double {133        precondition(a > 0, "incompleteGamma requires a > 0")134        if x <= 0 { return 0 }135136        if x < a + 1 {137            // Series representation.138            var term = 1.0 / a139            var sum = term140            var ap = a141            for _ in 0..<500 {142                ap += 1143                term *= x / ap144                sum += term145                if abs(term) < abs(sum) * 1e-16 { break }146            }147            return sum * exp(-x + a * log(x) - logGamma(a))148        } else {149            // Continued fraction for Q(a, x); P = 1 − Q.150            let tiny = 1e-300151            var b = x + 1 - a152            var c = 1 / tiny153            var d = 1 / b154            var h = d155            for i in 1...500 {156                let an = -Double(i) * (Double(i) - a)157                b += 2158                d = an * d + b159                if abs(d) < tiny { d = tiny }160                c = b + an / c161                if abs(c) < tiny { c = tiny }162                d = 1 / d163                let delta = d * c164                h *= delta165                if abs(delta - 1) < 1e-16 { break }166            }167            let q = exp(-x + a * log(x) - logGamma(a)) * h168            return 1 - q169        }170    }171172    /// Chi-square CDF with `df` degrees of freedom.173    public static func chiSquareCDF(_ x: Double, df: Double) -> Double {174        x <= 0 ? 0 : incompleteGamma(a: df / 2, x: x / 2)175    }176177    /// Upper-tail p-value for a chi-square statistic.178    public static func chiSquarePValue(_ x: Double, df: Double) -> Double {179        1 - chiSquareCDF(x, df: df)180    }181182    /// Standard normal quantile via bisection on the CDF (erfc-based, so183    /// accurate to ~1e-15) — used for z confidence intervals.184    public static func normalQuantile(_ p: Double) -> Double {185        precondition(p > 0 && p < 1, "quantile requires 0 < p < 1")186        if abs(p - 0.5) < 1e-15 { return 0 }187        var low = -40.0, high = 40.0188        for _ in 0..<200 {189            let mid = 0.5 * (low + high)190            if normalCDF(mid) < p { low = mid } else { high = mid }191            if high - low < 1e-15 * max(1, abs(mid)) { break }192        }193        return 0.5 * (low + high)194    }195196    /// Student t quantile (inverse CDF) via bisection refined with Newton197    /// steps — used for confidence intervals.198    public static func studentTQuantile(_ p: Double, df: Double) -> Double {199        precondition(p > 0 && p < 1, "quantile requires 0 < p < 1")200        if abs(p - 0.5) < 1e-15 { return 0 }201202        var low = -1000.0, high = 1000.0203        var mid = 0.0204        for _ in 0..<200 {205            mid = 0.5 * (low + high)206            if studentTCDF(mid, df: df) < p {207                low = mid208            } else {209                high = mid210            }211            if high - low < 1e-13 * max(1, abs(mid)) { break }212        }213        return 0.5 * (low + high)214    }215}216