// // BayesianRegression.swift // Metrika // // Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Copyright © 2026 Simon-Pierre Boucher. All rights reserved. // import Foundation import ZQGPU /// Bayesian linear regression by Gibbs sampling (CLAUDE.md §1 MCMC /// module). Semi-conjugate model with Stata `bayes` default priors: /// /// y | β, σ² ~ N(Xβ, σ²I) /// β_j ~ N(0, τ²), τ² = 10 000 by default /// σ² ~ InvGamma(a₀, b₀), a₀ = b₀ = 0.01 by default /// /// Full conditionals are exact (Gibbs, acceptance rate 1): /// β | σ², y ~ N(Vₙ X'y/σ², Vₙ), Vₙ = (I/τ² + X'X/σ²)⁻¹ /// σ² | β, y ~ InvGamma(a₀ + n/2, b₀ + ‖y − Xβ‖²/2) /// /// Draws come from the Philox stream, so a (seed, chain) pair fully /// determines the chain. public struct ZQBayesCoefficient: Equatable, Sendable { public var name: String public var posteriorMean: Double public var posteriorSD: Double public var credibleLower: Double // equal-tailed 2.5% public var credibleUpper: Double // 97.5% } public struct ZQBayesResult: Equatable, Sendable { public var coefficients: [ZQBayesCoefficient] /// Posterior summary of σ (the residual standard deviation). public var sigma: ZQBayesCoefficient public var observationCount: Int public var mcmcSize: Int public var burnIn: Int } public enum ZQBayesianRegression { public static func fitGibbs( y: [Double], predictors: [(name: String, values: [Double])], includeConstant: Bool = true, mcmcSize: Int = 10_000, burnIn: Int = 2_500, seed: UInt64, coefficientPriorVariance: Double = 10_000, sigmaPriorShape: Double = 0.01, sigmaPriorRate: Double = 0.01 ) throws -> ZQBayesResult { let n = y.count var names = predictors.map(\.name) if includeConstant { names.append("_cons") } let k = names.count guard n > k else { throw ZQStatsError("bayes: insufficient observations: n=\(n), k=\(k)") } guard mcmcSize > 10 else { throw ZQStatsError("bayes: mcmcsize too small") } // Design (column-major) and sufficient statistics. var x = [Double]() x.reserveCapacity(n * k) for column in predictors { guard column.values.count == n else { throw ZQStatsError("regressor '\(column.name)' has wrong length") } x.append(contentsOf: column.values) } if includeConstant { x.append(contentsOf: [Double](repeating: 1, count: n)) } var xtx = [Double](repeating: 0, count: k * k) var xty = [Double](repeating: 0, count: k) for j in 0..= burnIn { betaDraws[iteration - burnIn] = beta sigmaDraws[iteration - burnIn] = sigma2.squareRoot() } } // Posterior summaries. func summarize(_ name: String, _ draws: [Double]) -> ZQBayesCoefficient { let m = draws.reduce(0, +) / Double(draws.count) let variance = draws.reduce(0) { $0 + ($1 - m) * ($1 - m) } / Double(draws.count - 1) let sorted = draws.sorted() func quantile(_ p: Double) -> Double { let position = p * Double(sorted.count - 1) let lower = Int(position) let fraction = position - Double(lower) if lower + 1 < sorted.count { return sorted[lower] * (1 - fraction) + sorted[lower + 1] * fraction } return sorted[lower] } return ZQBayesCoefficient( name: name, posteriorMean: m, posteriorSD: variance.squareRoot(), credibleLower: quantile(0.025), credibleUpper: quantile(0.975) ) } var coefficients: [ZQBayesCoefficient] = [] for j in 0.. [Double] { var l = [Double](repeating: 0, count: k * k) for j in 0.. 0 else { throw ZQStatsError("bayes: posterior precision is not positive definite") } let root = diagonal.squareRoot() l[j * k + j] = root for i in (j + 1).. [Double] { var z = [Double](repeating: 0, count: k) for i in 0.. [Double] { var solution = [Double](repeating: 0, count: k) for i in stride(from: k - 1, through: 0, by: -1) { var value = rhs[i] for p in (i + 1)..