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%
4.1 KB · 122 lines swift
Raw Blame History
1//2//  Summarize.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Foundation1112/// Descriptive statistics for one variable, Stata `summarize` semantics.13public struct ZQSummary: Equatable, Sendable {14    public var name: String15    public var observationCount: Int16    public var mean: Double17    /// Sample variance (n−1 denominator).18    public var variance: Double19    public var standardDeviation: Double20    public var minimum: Double21    public var maximum: Double22    /// Detail block (percentiles, skewness, kurtosis) — only populated for23    /// `summarize, detail`.24    public var detail: Detail?2526    public struct Detail: Equatable, Sendable {27        /// Percentiles keyed by percent (1, 5, 10, 25, 50, 75, 90, 95, 99),28        /// computed with Stata's definition: for np/100 integral, the mean29        /// of adjacent order statistics; otherwise the next order statistic.30        public var percentiles: [Int: Double]31        /// Skewness m₃/m₂^{3/2} and kurtosis m₄/m₂² on central moments32        /// with n denominators (Stata definitions).33        public var skewness: Double34        public var kurtosis: Double35        public var smallest: [Double]36        public var largest: [Double]37    }38}3940public enum ZQSummarize {4142    /// Summarizes non-missing values. Returns nil when all values are43    /// missing (Stata prints a zero-observation row in that case; the44    /// caller renders it).45    public static func summary(46        name: String, values: [Double], missing: [Bool], detail: Bool = false47    ) -> ZQSummary? {48        var data: [Double] = []49        data.reserveCapacity(values.count)50        for i in 0..<values.count where !missing[i] {51            data.append(values[i])52        }53        let n = data.count54        guard n > 0 else { return nil }5556        var sum = 0.057        for v in data { sum += v }58        let mean = sum / Double(n)5960        var m2 = 0.0, m3 = 0.0, m4 = 0.061        var minimum = data[0], maximum = data[0]62        for v in data {63            let d = v - mean64            let d2 = d * d65            m2 += d266            m3 += d2 * d67            m4 += d2 * d268            if v < minimum { minimum = v }69            if v > maximum { maximum = v }70        }71        let variance = n > 1 ? m2 / Double(n - 1) : 07273        var summary = ZQSummary(74            name: name,75            observationCount: n,76            mean: mean,77            variance: variance,78            standardDeviation: variance.squareRoot(),79            minimum: minimum,80            maximum: maximum,81            detail: nil82        )8384        if detail {85            let sorted = data.sorted()86            var percentiles: [Int: Double] = [:]87            for p in [1, 5, 10, 25, 50, 75, 90, 95, 99] {88                percentiles[p] = stataPercentile(sorted: sorted, percent: Double(p))89            }90            let mc2 = m2 / Double(n)91            let mc3 = m3 / Double(n)92            let mc4 = m4 / Double(n)93            summary.detail = ZQSummary.Detail(94                percentiles: percentiles,95                skewness: mc2 > 0 ? mc3 / pow(mc2, 1.5) : .nan,96                kurtosis: mc2 > 0 ? mc4 / (mc2 * mc2) : .nan,97                smallest: Array(sorted.prefix(4)),98                largest: Array(sorted.suffix(4))99            )100        }101        return summary102    }103104    /// Stata percentile definition: with np = n·p/100, if np is integral105    /// the percentile is (x₍np₎ + x₍np+1₎)/2, otherwise x₍⌈np⌉₎ (1-based106    /// order statistics).107    public static func stataPercentile(sorted: [Double], percent: Double) -> Double {108        let n = sorted.count109        precondition(n > 0)110        let np = Double(n) * percent / 100111        let isIntegral = np == np.rounded()112        if isIntegral {113            let index = Int(np)114            if index >= n { return sorted[n - 1] }115            if index < 1 { return sorted[0] }116            return 0.5 * (sorted[index - 1] + sorted[index])117        }118        let index = min(n, max(1, Int(np.rounded(.up))))119        return sorted[index - 1]120    }121}122