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%
9.0 KB · 236 lines swift
Raw Blame History
1//2//  IV.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Accelerate11import Foundation1213/// Two-stage least squares for `ivregress 2sls`.14///15/// The projection uses the thin Q of the instrument matrix (X̂ = Q·QᵀX),16/// never forming Z'Z. Inference follows Stata's `small` convention:17/// σ² = u'u/(N−K) with t statistics; residuals come from the ORIGINAL18/// regressors (u = y − Xβ), not the projected ones.19public enum ZQIV {2021    public static func fit2SLS(22        y: [Double],23        endogenous: [(name: String, values: [Double])],24        exogenous: [(name: String, values: [Double])],25        instruments: [(name: String, values: [Double])],26        includeConstant: Bool = true,27        variance: ZQVarianceEstimator = .classical,28        confidenceLevel: Double = 0.9529    ) throws -> ZQOLSResult {30        let n = y.count31        guard !endogenous.isEmpty else {32            throw ZQStatsError("ivregress: at least one endogenous regressor required")33        }34        let k = endogenous.count + exogenous.count + (includeConstant ? 1 : 0)35        let m = instruments.count + exogenous.count + (includeConstant ? 1 : 0)36        guard m >= k else {37            throw ZQStatsError(38                "ivregress: order condition fails — \(instruments.count) instruments for \(endogenous.count) endogenous regressors"39            )40        }41        guard n > k else {42            throw ZQStatsError("insufficient observations: n=\(n), k=\(k)")43        }4445        func columnMajor(_ columns: [[Double]]) -> [Double] {46            var flat = [Double]()47            flat.reserveCapacity(n * columns.count)48            for column in columns { flat.append(contentsOf: column) }49            return flat50        }51        let ones = [Double](repeating: 1, count: n)5253        // X = [endog, exog, 1], Z = [instruments, exog, 1].54        var xColumns = endogenous.map(\.values) + exogenous.map(\.values)55        var zColumns = instruments.map(\.values) + exogenous.map(\.values)56        if includeConstant {57            xColumns.append(ones)58            zColumns.append(ones)59        }60        for column in xColumns + zColumns where column.count != n {61            throw ZQStatsError("ivregress: variable has wrong length")62        }63        let x = columnMajor(xColumns)64        let z = columnMajor(zColumns)6566        // First stage: X̂ = Q·(QᵀX) with Q the thin Q of Z.67        let projected: [Double]68        do {69            let zQR = try LinearAlgebra.QR(matrix: z, rows: n, cols: m)70            let q = try zQR.thinQ()71            var qtx = [Double](repeating: 0, count: m * k)72            cblas_dgemm(73                CblasColMajor, CblasTrans, CblasNoTrans,74                Int32(m), Int32(k), Int32(n),75                1.0, q, Int32(n), x, Int32(n), 0.0, &qtx, Int32(m)76            )77            var xhat = [Double](repeating: 0, count: n * k)78            cblas_dgemm(79                CblasColMajor, CblasNoTrans, CblasNoTrans,80                Int32(n), Int32(k), Int32(m),81                1.0, q, Int32(n), qtx, Int32(m), 0.0, &xhat, Int32(n)82            )83            projected = xhat84        } catch let error as LinearAlgebra.Failure {85            throw ZQStatsError("ivregress: instrument matrix is rank deficient — \(error)")86        }8788        // Second stage.89        let secondStage: LinearAlgebra.QR90        let beta: [Double]91        let bread: [Double]92        do {93            secondStage = try LinearAlgebra.QR(matrix: projected, rows: n, cols: k)94            beta = try secondStage.solve(rhs: y)95            bread = try secondStage.crossProductInverse()96        } catch {97            throw ZQStatsError(98                "ivregress: projected design is rank deficient (weak or collinear instruments?)"99            )100        }101102        // Residuals from the ORIGINAL regressors.103        let fitted = LinearAlgebra.multiply(matrix: x, rows: n, cols: k, vector: beta)104        var residuals = [Double](repeating: 0, count: n)105        var rss = 0.0106        for i in 0..<n {107            residuals[i] = y[i] - fitted[i]108            rss += residuals[i] * residuals[i]109        }110        let dfResidual = n - k111        let sigma2 = rss / Double(dfResidual)112        let meanY = y.reduce(0, +) / Double(n)113        let tss = y.reduce(0) { $0 + ($1 - meanY) * ($1 - meanY) }114        let rSquared = tss > 0 ? 1 - rss / tss : .nan   // can be negative for IV115        let dfModel = k - (includeConstant ? 1 : 0)116117        // Covariance: sandwich pieces built on the PROJECTED regressors.118        var clusterCount: Int?119        let vce: [Double]120        switch variance {121        case .classical:122            vce = bread.map { $0 * sigma2 }123        case .hc0, .hc1, .hc2, .hc3:124            var meat = [Double](repeating: 0, count: k * k)125            for row in 0..<n {126                let weight = residuals[row] * residuals[row]127                for j in 0..<k {128                    for i in j..<k {129                        let value = weight * projected[i * n + row] * projected[j * n + row]130                        meat[j * k + i] += value131                    }132                }133            }134            for j in 0..<k {135                for i in 0..<j { meat[j * k + i] = meat[i * k + j] }136            }137            if variance == .hc1 {138                let scale = Double(n) / Double(dfResidual)139                for index in meat.indices { meat[index] *= scale }140            }141            vce = LinearAlgebra.sandwich(bread: bread, meat: meat, k: k)142        case .cluster(let clusters):143            guard clusters.count == n else {144                throw ZQStatsError("cluster variable has wrong length")145            }146            var scores: [Int: [Double]] = [:]147            for i in 0..<n {148                var u = scores[clusters[i]] ?? [Double](repeating: 0, count: k)149                for j in 0..<k {150                    u[j] += projected[j * n + i] * residuals[i]151                }152                scores[clusters[i]] = u153            }154            let g = scores.count155            guard g > 1 else {156                throw ZQStatsError("cluster variable must define at least 2 groups")157            }158            clusterCount = g159            var meat = [Double](repeating: 0, count: k * k)160            for u in scores.values {161                for j in 0..<k {162                    for i in 0..<k {163                        meat[j * k + i] += u[i] * u[j]164                    }165                }166            }167            let scale = Double(g) / Double(g - 1) * Double(n - 1) / Double(dfResidual)168            for index in meat.indices { meat[index] *= scale }169            vce = LinearAlgebra.sandwich(bread: bread, meat: meat, k: k)170        }171172        let inferenceDF = clusterCount.map { Double($0 - 1) } ?? Double(dfResidual)173        let tCritical = ZQDistributions.studentTQuantile(174            0.5 + confidenceLevel / 2, df: inferenceDF175        )176177        var names = endogenous.map(\.name) + exogenous.map(\.name)178        if includeConstant { names.append("_cons") }179        var coefficients: [ZQCoefficient] = []180        for j in 0..<k {181            let se = vce[j * k + j].squareRoot()182            let t = beta[j] / se183            coefficients.append(ZQCoefficient(184                name: names[j],185                estimate: beta[j],186                standardError: se,187                tStatistic: t,188                pValue: ZQDistributions.tTestPValue(t, df: inferenceDF),189                confidenceLower: beta[j] - tCritical * se,190                confidenceUpper: beta[j] + tCritical * se191            ))192        }193194        // Wald F on the slopes.195        var fStatistic: Double?196        var fPValue: Double?197        var fDF: (Double, Double)?198        if dfModel > 0 {199            var subVce = [Double](repeating: 0, count: dfModel * dfModel)200            var subBeta = [Double](repeating: 0, count: dfModel)201            for j in 0..<dfModel {202                subBeta[j] = beta[j]203                for i in 0..<dfModel {204                    subVce[j * dfModel + i] = vce[j * k + i]205                }206            }207            if let solved = try? LinearAlgebra.solveSymmetric(208                subVce, k: dfModel, rhs: subBeta209            ) {210                let wald = zip(subBeta, solved).reduce(0) { $0 + $1.0 * $1.1 }211                fStatistic = wald / Double(dfModel)212                fDF = (Double(dfModel), inferenceDF)213                fPValue = ZQDistributions.fTestPValue(214                    wald / Double(dfModel), df1: Double(dfModel), df2: inferenceDF215                )216            }217        }218219        return ZQOLSResult(220            coefficients: coefficients,221            observationCount: n,222            degreesOfFreedomResidual: dfResidual,223            inferenceDF: inferenceDF,224            rSquared: rSquared,225            adjustedRSquared: 1 - (1 - rSquared) * Double(n - 1) / Double(dfResidual),226            rootMSE: sigma2.squareRoot(),227            fStatistic: fStatistic,228            fPValue: fPValue,229            fDF: fDF,230            clusterCount: clusterCount,231            residuals: residuals,232            vce: vce233        )234    }235}236