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%

docs: README with badges, screenshots, and the v1.0 story

- hero: console session showing cluster-robust factor regression and the
  GPU-batched 10k bootstrap; screenshot grid (Charts by(), Metal 2M,
  data browser, manual)
- shields badges: release, macOS 14+, Swift 6, arm64, Metal+MLX, 116
  tests, R-validated 1e-10, notarized DMG
- five-minute tour, feature matrix, numerical-validation section,
  architecture, build instructions, extensibility examples
- METRIKA_AUTOPANE debug hook (headless pane selection for screenshots)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
simon-pierre boucher committed 5 days ago (Aug 5, 2026) parent 18c6874

Showing 9 changed files with +207 and −3

modified Metrika.xcodeproj/project.pbxproj +2 −2
@@ -353,7 +353,7 @@
353 353 "$(inherited)",
354 354 "@executable_path/../Frameworks",
355 355 );
356 MARKETING_VERSION = 0.1.0;
356 + MARKETING_VERSION = 1.0.0;
357 357 ONLY_ACTIVE_ARCH = YES;
358 358 PRODUCT_BUNDLE_IDENTIFIER = ai.spboucher.metrika;
359 359 SDKROOT = macosx;
@@ -400,7 +400,7 @@
400 400 "$(inherited)",
401 401 "@executable_path/../Frameworks",
402 402 );
403 MARKETING_VERSION = 0.1.0;
403 + MARKETING_VERSION = 1.0.0;
404 404 ONLY_ACTIVE_ARCH = YES;
405 405 PRODUCT_BUNDLE_IDENTIFIER = ai.spboucher.metrika;
406 406 SDKROOT = macosx;
modified Metrika/Sources/MetrikaApp.swift +13 −1
@@ -32,7 +32,19 @@ struct ContentView: View {
32 32 }
33 33
34 34 @Environment(SessionModel.self) private var model
35 @State private var pane: Pane = .console
35 + @State private var pane: Pane = ContentView.initialPane()
36 +
37 + /// Debug hook for headless screenshots: METRIKA_AUTOPANE selects the
38 + /// pane shown at launch.
39 + private static func initialPane() -> Pane {
40 + #if DEBUG
41 + if let name = ProcessInfo.processInfo.environment["METRIKA_AUTOPANE"],
42 + let selected = Pane(rawValue: name) {
43 + return selected
44 + }
45 + #endif
46 + return .console
47 + }
36 48
37 49 var body: some View {
38 50 NavigationSplitView {
added README.md +192 −0
@@ -0,0 +1,192 @@
1 +<div align="center">
2 +
3 +<img src="docs/screenshots/icon.png" width="140" alt="Metrika icon">
4 +
5 +# Metrika
6 +
7 +**Stata-class statistics, GPU-accelerated by Apple Silicon.**
8 +Native Swift. No Electron. No Python runtime. No compromises.
9 +
10 +[![Release](https://img.shields.io/badge/release-v1.0.0-4f8cff)](../../releases/latest)
11 +[![macOS](https://img.shields.io/badge/macOS-14%2B-black?logo=apple)](#requirements)
12 +[![Swift](https://img.shields.io/badge/Swift-6-F05138?logo=swift&logoColor=white)](#building-from-source)
13 +[![Apple Silicon](https://img.shields.io/badge/Apple%20Silicon-arm64-333)](#requirements)
14 +[![GPU](https://img.shields.io/badge/GPU-Metal%20%2B%20MLX-9b6cff)](#the-gpu-invisibly)
15 +[![Tests](https://img.shields.io/badge/tests-116%20passing-34c759)](#numerical-validation)
16 +[![Validated](https://img.shields.io/badge/R--validated-1e--10-34c759)](#numerical-validation)
17 +[![Notarized](https://img.shields.io/badge/DMG-signed%20%26%20notarized-0a84ff)](../../releases/latest)
18 +
19 +<img src="docs/screenshots/console-regression.png" width="820" alt="Cluster-robust regression with factor variables and a 10,000-replicate GPU bootstrap">
20 +
21 +*One session: a cluster-robust factor-variable regression, then a
22 +10,000-replicate pairs bootstrap — **batched on the Apple GPU**, exactly
23 +reproducible from `seed(42)` on any backend.*
24 +
25 +</div>
26 +
27 +---
28 +
29 +## Why Metrika
30 +
31 +- **One line, publication-ready output.** `reg log_rev price i.region, cluster(firm_id)` — the Stata mental model, with factor variables, `if`/`in` qualifiers, robust and cluster-robust inference.
32 +- **The GPU is invisible.** A planner dispatches every command to CPU (LAPACK) or GPU (MLX) automatically. Large bootstrap runs execute as batched Metal solves; you never choose a backend.
33 +- **Reproducibility is a feature, not an accident.** All randomness flows through a counter-based Philox4x32 generator: `set seed 42` produces *bit-identical* resamples on CPU and GPU, in any chunk order, across any parallelism.
34 +- **Numbers you can defend.** Every CPU estimator is validated against R to **1e-10 relative tolerance** — coefficients, standard errors (classical, HC0–HC3, cluster), p-values, marginal effects. Penalized and boosted models cross-validate against glmnet and xgboost.
35 +- **Big data on a laptop.** DuckDB columnar engine underneath; a Metal point-sprite renderer takes over scatter plots past 100k points and shrugs at 2,000,000.
36 +
37 +## Screenshots
38 +
39 +| | |
40 +|:---:|:---:|
41 +| **Swift Charts with `by()` groups**<br><img src="docs/screenshots/scatter-by.png" width="420"> | **2,000,000 points — Metal renderer**<br><img src="docs/screenshots/metal-2m.png" width="420"> |
42 +| **Virtualized data browser with expression filter**<br><img src="docs/screenshots/data-browser.png" width="420"> | **Built-in manual for all 38 commands**<br><img src="docs/screenshots/manual.png" width="420"> |
43 +
44 +## Install
45 +
46 +Download **[Metrika.dmg](../../releases/latest)** — signed, notarized, and stapled. Drag into Applications. macOS 14+ on Apple Silicon.
47 +
48 +## A five-minute tour
49 +
50 +```stata
51 +. sysuse sales // bundled 200-firm × 5-year panel
52 +. gen log_rev = ln(revenue)
53 +. summarize revenue price, detail
54 +
55 +. reg log_rev price i.region, robust // HC1 SEs, factor expansion
56 +. predict yhat
57 +. margins, dydx(price) // delta-method standard errors
58 +
59 +. xtset firm_id
60 +. xtreg log_rev price, fe cluster(firm_id)
61 +. ivregress 2sls log_rev (price = z1 z2), robust
62 +
63 +. logit purchase price
64 +. margins, dydx(price)
65 +
66 +. bootstrap, reps(100000) seed(42): reg log_rev price // GPU batched
67 +. permute, reps(10000) seed(42): reg log_rev price // exact p-values
68 +. bayes, mcmcsize(20000) seed(42): reg log_rev price // Gibbs sampler
69 +
70 +. lasso log_rev price z1 z2 orders, lambda(0.05) // glmnet-exact
71 +. boost log_rev z1 z2 orders, rounds(100) maxdepth(3) // xgboost-exact
72 +
73 +. scatter log_rev price, by(region)
74 +. histogram revenue, bins(20)
75 +. save results.dta, replace // native Stata .dta 118
76 +```
77 +
78 +Everything above works identically in the console, in `.zyq` do-files
79 +(⌘R in the editor), and headlessly through `metrika-cli`.
80 +
81 +## What's inside
82 +
83 +| Pillar | Contents |
84 +|---|---|
85 +| **Data** | parquet, csv, json, arrow, native Stata **.dta** (read 117–119, write 118) · DuckDB engine · explicit missing-value semantics with listwise-deletion reporting |
86 +| **Estimation** | OLS (QR, never X'X) · logit / probit / poisson · 2SLS · panel fixed effects · summarize / tabulate / correlate |
87 +| **Inference** | robust HC0–HC3 · cluster-robust with Stata small-sample factors · **GPU pairs bootstrap** · permutation tests · Bayesian regression (Gibbs) |
88 +| **Machine learning** | lasso & elastic net (coordinate descent, glmnet-exact) · gradient-boosted trees (xgboost-exact) |
89 +| **Post-estimation** | `predict` (xb, residuals, pr, n) · `margins, dydx()` with delta-method SEs |
90 +| **Graphics** | Swift Charts scatter/line/histogram/kdensity · **Metal renderer** for millions of points |
91 +| **Extensibility** | `.zyq` script commands with `args` macros · native Swift `ZQCommandPlugin`s with syntax validation and mutation gating |
92 +
93 +## Numerical validation
94 +
95 +Metrika's test suite doesn't check that code runs — it checks that the
96 +*numbers are right*:
97 +
98 +- **116 tests** compare against golden values generated by R (`Tests/Fixtures/generate.R`): OLS coefficients, every SE variant, t/F/χ² p-values into the far tails (p = 4×10⁻²² matches R exactly), GLM likelihoods, marginal effects **and their delta-method SEs** — all at 1e-10 relative tolerance.
99 +- Lasso/elastic-net coefficients match **glmnet** (including its subtle gaussian y-standardization convention); the selection pattern — which coefficients are exactly zero — matches exactly.
100 +- Boosted-tree predictions match **xgboost** observation-by-observation.
101 +- The GPU bootstrap's resample indices are asserted **bit-identical** to the CPU Philox reference; Philox itself is pinned to the Random123 known-answer vectors.
102 +- With diffuse priors, the Bayesian posterior reproduces the frequentist answer within Monte-Carlo error — asserted, not assumed.
103 +
104 +## Architecture
105 +
106 +```
107 +┌─────────────────────────────────────────────────────────┐
108 +│ Metrika (SwiftUI) │
109 +│ Console · Data browser · Do-file editor · Manual │
110 +└──────────────────────────┬──────────────────────────────┘
111 +
112 +┌──────────────────────────▼──────────────────────────────┐
113 +│ MetrikaKit (Swift package) │
114 +│ │
115 +│ ZQParser command grammar → typed AST │
116 +│ ZQPlanner AST → CPU / GPU / hybrid dispatch │
117 +│ ZQEngine sessions, execution, logging, help │
118 +│ ZQData DataFrame façade over DuckDB + .dta │
119 +│ ZQStats LAPACK estimators (Accelerate) │
120 +│ ZQGPU MLX batched solves, Philox RNG │
121 +│ ZQGraphics plot specs → Swift Charts / Metal │
122 +│ ZQPlugins user commands & sandboxing │
123 +└──────────────────────────────────────────────────────────┘
124 +```
125 +
126 +`MetrikaKit` has zero UI dependencies and is fully testable with
127 +`swift test`. `ZQGPU` is the only module allowed to touch MLX/Metal;
128 +`ZQStats` the only one touching Accelerate — backends stay swappable.
129 +
130 +## Building from source
131 +
132 +```bash
133 +git clone https://github.com/spboucher-ai/metrika && cd metrika
134 +./scripts/install_hooks.sh
135 +
136 +# Library + CLI + tests
137 +cd MetrikaKit
138 +swift build && swift test # 116 tests (GPU suites auto-skip)
139 +xcodebuild test -scheme MetrikaKit-Package \
140 + -destination 'platform=macOS' -skipPackagePluginValidation # + GPU suites
141 +
142 +# App
143 +cd .. && xcodegen generate
144 +xcodebuild -project Metrika.xcodeproj -scheme Metrika \
145 + -skipPackagePluginValidation build
146 +
147 +# Signed, notarized DMG
148 +./scripts/release.sh
149 +```
150 +
151 +> SwiftPM's CLI cannot compile Metal shaders, so `swift test` skips the
152 +> GPU suites and CLI builds fall back to CPU automatically; `xcodebuild`
153 +> runs give you everything.
154 +
155 +## Documentation
156 +
157 +- **In the app**: the Manual tab, or `help <command>` in the console.
158 +- **Static site**: [`docs/index.html`](docs/index.html) — regenerated from the same registry by `metrika-cli docs`, so it can never drift from the app.
159 +
160 +## Extending Metrika
161 +
162 +Drop a `.zyq` script into `~/Library/Application Support/Metrika/Commands/`:
163 +
164 +```stata
165 +// logreg.zyq — its filename becomes the command
166 +args response predictor
167 +gen __log = ln(`response')
168 +reg __log `predictor', robust
169 +drop __log
170 +```
171 +
172 +…or compile a Swift plugin into the app:
173 +
174 +```swift
175 +public struct ZScorePlugin: ZQCommandPlugin {
176 + public static let verb = "zscore"
177 + public static let syntax = ZQSyntaxSpec(mutates: true)
178 + public func execute(_ ctx: ZQContext) async throws -> ZQResult { }
179 +}
180 +```
181 +
182 +Plugins that shadow built-ins are rejected at startup; datasets can only
183 +be mutated through a declared, gated channel.
184 +
185 +---
186 +
187 +<div align="center">
188 +
189 +**© 2026 Simon-Pierre Boucher. All rights reserved.**
190 +[contact@spboucher.ai](mailto:contact@spboucher.ai)
191 +
192 +</div>
added docs/screenshots/console-regression.png +0 −0

Binary file not shown.

added docs/screenshots/data-browser.png +0 −0

Binary file not shown.

added docs/screenshots/icon.png +0 −0

Binary file not shown.

added docs/screenshots/manual.png +0 −0

Binary file not shown.

added docs/screenshots/metal-2m.png +0 −0

Binary file not shown.

added docs/screenshots/scatter-by.png +0 −0

Binary file not shown.