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%
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