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%
8.2 KB · 218 lines swift
Raw Blame History
1//2//  FixedEffects.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Foundation1112/// Panel fixed-effects (within) estimator for `xtreg, fe`.13///14/// The within transformation subtracts group means and adds back grand15/// means (Stata's convention, so `_cons` is reported), then solves by the16/// usual QR path. Degrees of freedom absorb the G group effects:17/// df = N − K − G. Cluster-robust VCE clusters on the panel variable with18/// the G/(G−1) factor and t statistics on G−1 df.19public struct ZQFEResult: Equatable, Sendable {20    public var coefficients: [ZQCoefficient]21    public var observationCount: Int22    public var groupCount: Int23    public var degreesOfFreedomResidual: Int24    public var inferenceDF: Double25    /// Within R²: fit of the demeaned regression.26    public var rSquaredWithin: Double27    public var rootMSE: Double28    public var fStatistic: Double?29    public var fPValue: Double?30    public var clustered: Bool3132    public init(33        coefficients: [ZQCoefficient], observationCount: Int, groupCount: Int,34        degreesOfFreedomResidual: Int, inferenceDF: Double,35        rSquaredWithin: Double, rootMSE: Double,36        fStatistic: Double?, fPValue: Double?, clustered: Bool37    ) {38        self.coefficients = coefficients39        self.observationCount = observationCount40        self.groupCount = groupCount41        self.degreesOfFreedomResidual = degreesOfFreedomResidual42        self.inferenceDF = inferenceDF43        self.rSquaredWithin = rSquaredWithin44        self.rootMSE = rootMSE45        self.fStatistic = fStatistic46        self.fPValue = fPValue47        self.clustered = clustered48    }49}5051public enum ZQFixedEffects {5253    /// - Parameters:54    ///   - groups: dense panel codes (0..<G), one per observation.55    ///   - clustered: cluster the VCE on the panel variable.56    public static func fitWithin(57        y: [Double],58        predictors: [(name: String, values: [Double])],59        groups: [Int],60        clustered: Bool = false,61        confidenceLevel: Double = 0.9562    ) throws -> ZQFEResult {63        let n = y.count64        let slopes = predictors.count65        guard slopes > 0 else { throw ZQStatsError("xtreg: regressors required") }66        guard groups.count == n else {67            throw ZQStatsError("panel variable has wrong length")68        }69        let groupCount = Set(groups).count70        let k = slopes + 1   // slopes + reported constant71        let dfResidual = n - slopes - groupCount72        guard dfResidual > 0 else {73            throw ZQStatsError("insufficient observations: N=\(n), K=\(slopes), G=\(groupCount)")74        }7576        // Group means and grand means.77        func withinTransform(_ values: [Double]) -> (transformed: [Double], demeaned: [Double]) {78            var sums = [Double](repeating: 0, count: groupCount)79            var counts = [Double](repeating: 0, count: groupCount)80            for i in 0..<n {81                sums[groups[i]] += values[i]82                counts[groups[i]] += 183            }84            let grand = values.reduce(0, +) / Double(n)85            var transformed = [Double](repeating: 0, count: n)86            var demeaned = [Double](repeating: 0, count: n)87            for i in 0..<n {88                let groupMean = sums[groups[i]] / counts[groups[i]]89                demeaned[i] = values[i] - groupMean90                transformed[i] = demeaned[i] + grand91            }92            return (transformed, demeaned)93        }9495        let (yTransformed, yDemeaned) = withinTransform(y)96        var design = [Double]()97        design.reserveCapacity(n * k)98        var demeanedColumns: [[Double]] = []99        for column in predictors {100            guard column.values.count == n else {101                throw ZQStatsError("regressor '\(column.name)' has wrong length")102            }103            let (transformed, demeaned) = withinTransform(column.values)104            design.append(contentsOf: transformed)105            demeanedColumns.append(demeaned)106        }107        design.append(contentsOf: [Double](repeating: 1, count: n))108109        let qr: LinearAlgebra.QR110        let beta: [Double]111        let xtxInverse: [Double]112        do {113            qr = try LinearAlgebra.QR(matrix: design, rows: n, cols: k)114            beta = try qr.solve(rhs: yTransformed)115            xtxInverse = try qr.crossProductInverse()116        } catch {117            throw ZQStatsError("xtreg: design matrix is rank deficient after demeaning — \(error)")118        }119120        // Within residuals and fit.121        let fitted = LinearAlgebra.multiply(matrix: design, rows: n, cols: k, vector: beta)122        var residuals = [Double](repeating: 0, count: n)123        var rss = 0.0124        for i in 0..<n {125            residuals[i] = yTransformed[i] - fitted[i]126            rss += residuals[i] * residuals[i]127        }128        let tssWithin = yDemeaned.reduce(0) { $0 + $1 * $1 }129        let sigma2 = rss / Double(dfResidual)130        let rSquaredWithin = tssWithin > 0 ? 1 - rss / tssWithin : .nan131132        // Covariance.133        let vce: [Double]134        let inferenceDF: Double135        if clustered {136            var scores: [Int: [Double]] = [:]137            for i in 0..<n {138                var u = scores[groups[i]] ?? [Double](repeating: 0, count: k)139                for j in 0..<slopes {140                    u[j] += demeanedColumns[j][i] * residuals[i]141                }142                u[slopes] += residuals[i]   // constant column143                scores[groups[i]] = u144            }145            guard groupCount > 1 else {146                throw ZQStatsError("cluster-robust VCE needs at least 2 groups")147            }148            var meat = [Double](repeating: 0, count: k * k)149            for u in scores.values {150                for j in 0..<k {151                    for i in 0..<k {152                        meat[j * k + i] += u[i] * u[j]153                    }154                }155            }156            let scale = Double(groupCount) / Double(groupCount - 1)157            for index in meat.indices { meat[index] *= scale }158            vce = LinearAlgebra.sandwich(bread: xtxInverse, meat: meat, k: k)159            inferenceDF = Double(groupCount - 1)160        } else {161            vce = xtxInverse.map { $0 * sigma2 }162            inferenceDF = Double(dfResidual)163        }164165        let tCritical = ZQDistributions.studentTQuantile(166            0.5 + confidenceLevel / 2, df: inferenceDF167        )168        var names = predictors.map(\.name)169        names.append("_cons")170        var coefficients: [ZQCoefficient] = []171        for j in 0..<k {172            let se = vce[j * k + j].squareRoot()173            let t = beta[j] / se174            coefficients.append(ZQCoefficient(175                name: names[j],176                estimate: beta[j],177                standardError: se,178                tStatistic: t,179                pValue: ZQDistributions.tTestPValue(t, df: inferenceDF),180                confidenceLower: beta[j] - tCritical * se,181                confidenceUpper: beta[j] + tCritical * se182            ))183        }184185        // Wald F on the slopes.186        var fStatistic: Double?187        var fPValue: Double?188        var subVce = [Double](repeating: 0, count: slopes * slopes)189        var subBeta = [Double](repeating: 0, count: slopes)190        for j in 0..<slopes {191            subBeta[j] = beta[j]192            for i in 0..<slopes {193                subVce[j * slopes + i] = vce[j * k + i]194            }195        }196        if let solved = try? LinearAlgebra.solveSymmetric(subVce, k: slopes, rhs: subBeta) {197            let wald = zip(subBeta, solved).reduce(0) { $0 + $1.0 * $1.1 }198            fStatistic = wald / Double(slopes)199            fPValue = ZQDistributions.fTestPValue(200                wald / Double(slopes), df1: Double(slopes), df2: inferenceDF201            )202        }203204        return ZQFEResult(205            coefficients: coefficients,206            observationCount: n,207            groupCount: groupCount,208            degreesOfFreedomResidual: dfResidual,209            inferenceDF: inferenceDF,210            rSquaredWithin: rSquaredWithin,211            rootMSE: sigma2.squareRoot(),212            fStatistic: fStatistic,213            fPValue: fPValue,214            clustered: clustered215        )216    }217}218