// // FixedEffects.swift // Metrika // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // import Foundation /// Panel fixed-effects (within) estimator for `xtreg, fe`. /// /// The within transformation subtracts group means and adds back grand /// means (Stata's convention, so `_cons` is reported), then solves by the /// usual QR path. Degrees of freedom absorb the G group effects: /// df = N − K − G. Cluster-robust VCE clusters on the panel variable with /// the G/(G−1) factor and t statistics on G−1 df. public struct ZQFEResult: Equatable, Sendable { public var coefficients: [ZQCoefficient] public var observationCount: Int public var groupCount: Int public var degreesOfFreedomResidual: Int public var inferenceDF: Double /// Within R²: fit of the demeaned regression. public var rSquaredWithin: Double public var rootMSE: Double public var fStatistic: Double? public var fPValue: Double? public var clustered: Bool public init( coefficients: [ZQCoefficient], observationCount: Int, groupCount: Int, degreesOfFreedomResidual: Int, inferenceDF: Double, rSquaredWithin: Double, rootMSE: Double, fStatistic: Double?, fPValue: Double?, clustered: Bool ) { self.coefficients = coefficients self.observationCount = observationCount self.groupCount = groupCount self.degreesOfFreedomResidual = degreesOfFreedomResidual self.inferenceDF = inferenceDF self.rSquaredWithin = rSquaredWithin self.rootMSE = rootMSE self.fStatistic = fStatistic self.fPValue = fPValue self.clustered = clustered } } public enum ZQFixedEffects { /// - Parameters: /// - groups: dense panel codes (0.. ZQFEResult { let n = y.count let slopes = predictors.count guard slopes > 0 else { throw ZQStatsError("xtreg: regressors required") } guard groups.count == n else { throw ZQStatsError("panel variable has wrong length") } let groupCount = Set(groups).count let k = slopes + 1 // slopes + reported constant let dfResidual = n - slopes - groupCount guard dfResidual > 0 else { throw ZQStatsError("insufficient observations: N=\(n), K=\(slopes), G=\(groupCount)") } // Group means and grand means. func withinTransform(_ values: [Double]) -> (transformed: [Double], demeaned: [Double]) { var sums = [Double](repeating: 0, count: groupCount) var counts = [Double](repeating: 0, count: groupCount) for i in 0.. 0 ? 1 - rss / tssWithin : .nan // Covariance. let vce: [Double] let inferenceDF: Double if clustered { var scores: [Int: [Double]] = [:] for i in 0.. 1 else { throw ZQStatsError("cluster-robust VCE needs at least 2 groups") } var meat = [Double](repeating: 0, count: k * k) for u in scores.values { for j in 0..