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%
6.0 KB · 164 lines swift
Raw Blame History
1//2//  GLMTests.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Foundation11import Testing12import ZQData13import ZQStats1415/// GLM estimators vs R glm() fixtures (epsilon 1e-12) at 1e-10 relative16/// tolerance.17@Suite("ZQGLM vs R fixtures")18struct GLMTests {19    let fixtures: Fixtures20    let purchase: [Double]21    let orders: [Double]22    let price: [Double]23    let firmID: [Int]2425    init() async throws {26        self.fixtures = try Fixtures()27        let store = try ZQDataStore()28        let frame = try await store.load(contentsOf: fixtures.datasetURL)29        let (purchaseAll, _) = try frame.requireNumeric("purchase")30        let (ordersAll, _) = try frame.requireNumeric("orders")31        let (priceAll, priceMissing) = try frame.requireNumeric("price")32        let (firmAll, _) = try frame.requireNumeric("firm_id")3334        var purchase: [Double] = [], orders: [Double] = []35        var price: [Double] = [], firm: [Int] = []36        for i in 0..<frame.rowCount where !priceMissing[i] {37            purchase.append(purchaseAll[i])38            orders.append(ordersAll[i])39            price.append(priceAll[i])40            firm.append(Int(firmAll[i]))41        }42        self.purchase = purchase43        self.orders = orders44        self.price = price45        self.firmID = firm46    }4748    @Test("logit matches R glm(binomial)")49    func logit() throws {50        let result = try ZQGLM.fit(51            y: purchase, predictors: [("price", price)], family: .logit52        )53        expectClose(result.coefficients[0].estimate, fixtures["logit_b_price"], "b[price]")54        expectClose(result.coefficients[1].estimate, fixtures["logit_b_cons"], "b[_cons]")55        expectClose(56            result.coefficients[0].standardError, fixtures["logit_se_price"], "se[price]"57        )58        expectClose(59            result.coefficients[1].standardError, fixtures["logit_se_cons"], "se[_cons]"60        )61        expectClose(result.logLikelihood, fixtures["logit_ll"], "log likelihood")62        expectClose(result.nullLogLikelihood, fixtures["logit_ll0"], "null log likelihood")63        expectClose(try #require(result.chiSquared), fixtures["logit_chi2"], "LR chi2")64        expectClose(65            try #require(result.chiSquaredPValue),66            fixtures["logit_chi2p"], rtol: 1e-9, "Prob > chi2"67        )68    }6970    @Test("logit robust and cluster standard errors")71    func logitRobust() throws {72        let robust = try ZQGLM.fit(73            y: purchase, predictors: [("price", price)], family: .logit, variance: .hc074        )75        expectClose(76            robust.coefficients[0].standardError,77            fixtures["logit_se_hc0_price"], "robust se[price]"78        )79        let clustered = try ZQGLM.fit(80            y: purchase, predictors: [("price", price)], family: .logit,81            variance: .cluster(firmID)82        )83        expectClose(84            clustered.coefficients[0].standardError,85            fixtures["logit_se_cluster_price"], "cluster se[price]"86        )87    }8889    @Test("probit matches R glm(binomial(probit))")90    func probit() throws {91        let result = try ZQGLM.fit(92            y: purchase, predictors: [("price", price)], family: .probit93        )94        expectClose(result.coefficients[0].estimate, fixtures["probit_b_price"], "b[price]")95        expectClose(result.coefficients[1].estimate, fixtures["probit_b_cons"], "b[_cons]")96        expectClose(97            result.coefficients[0].standardError, fixtures["probit_se_price"], "se[price]"98        )99        expectClose(result.logLikelihood, fixtures["probit_ll"], "log likelihood")100    }101102    @Test("poisson matches R glm(poisson)")103    func poisson() throws {104        let result = try ZQGLM.fit(105            y: orders, predictors: [("price", price)], family: .poisson106        )107        expectClose(result.coefficients[0].estimate, fixtures["pois_b_price"], "b[price]")108        expectClose(result.coefficients[1].estimate, fixtures["pois_b_cons"], "b[_cons]")109        expectClose(110            result.coefficients[0].standardError, fixtures["pois_se_price"], "se[price]"111        )112        expectClose(result.logLikelihood, fixtures["pois_ll"], "log likelihood")113114        let robust = try ZQGLM.fit(115            y: orders, predictors: [("price", price)], family: .poisson, variance: .hc0116        )117        expectClose(118            robust.coefficients[0].standardError,119            fixtures["pois_se_hc0_price"], "robust se[price]"120        )121    }122123    @Test("logit rejects non-binary outcomes")124    func binaryValidation() {125        #expect(throws: ZQStatsError.self) {126            _ = try ZQGLM.fit(127                y: orders, predictors: [("price", price)], family: .logit128            )129        }130    }131132    @Test("correlate matches R cor()")133    func correlate() async throws {134        let store = try ZQDataStore()135        let frame = try await store.load(contentsOf: fixtures.datasetURL)136        let (revenueAll, _) = try frame.requireNumeric("revenue")137        let (priceAll, priceMissing) = try frame.requireNumeric("price")138        var revenue: [Double] = [], priceComplete: [Double] = []139        for i in 0..<frame.rowCount where !priceMissing[i] {140            revenue.append(revenueAll[i])141            priceComplete.append(priceAll[i])142        }143        let matrix = ZQCorrelate.matrix(columns: [revenue, priceComplete])144        expectClose(matrix[0][1], fixtures["corr_rev_price"], "cor(revenue, price)")145        #expect(matrix[0][0] == 1 && matrix[1][1] == 1)146    }147148    @Test("chi-square CDF and normal quantile vs R")149    func chiSquare() {150        expectClose(151            ZQDistributions.chiSquareCDF(3.8, df: 1),152            fixtures["dist_pchisq_3p8_1"], rtol: 1e-12, "pchisq(3.8, 1)"153        )154        expectClose(155            ZQDistributions.chiSquareCDF(25, df: 4),156            fixtures["dist_pchisq_25_4"], rtol: 1e-12, "pchisq(25, 4)"157        )158        expectClose(159            ZQDistributions.normalQuantile(0.975),160            fixtures["dist_qnorm_0p975"], rtol: 1e-12, "qnorm(0.975)"161        )162    }163}164