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%
ZIP
NameLast commitUpdated
Assets fix(assets): move DMG background art to Assets/DMG (was under... 5 days ago
docs docs: README with badges, screenshots, and the v1.0 story 5 days ago
Metrika docs: README with badges, screenshots, and the v1.0 story 5 days ago
Metrika.xcodeproj chore: commit pending workspace changes yesterday
MetrikaKit perf(data): 880x faster loads via DuckDB C-API bulk extraction 5 days ago
scripts feat: v1.0 features — sample datasets, docs site, Metal 2M-point... 5 days ago
Tests feat: v1.0 features — sample datasets, docs site, Metal 2M-point... 5 days ago
.gitignore feat: bootstrap Metrika v0.1 skeleton with working ZQL vertical slice 5 days ago
CLAUDE.md perf(data): 880x faster loads via DuckDB C-API bulk extraction 5 days ago
project.yml perf(data): 880x faster loads via DuckDB C-API bulk extraction 5 days ago
README.md perf(data): 880x faster loads via DuckDB C-API bulk extraction 5 days ago
README.md
Metrika icon

# Metrika

Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift. No Electron. No Python runtime. No compromises.

Release macOS Swift Apple Silicon GPU Tests Validated Notarized

Cluster-robust regression with factor variables and a 10,000-replicate GPU bootstrap

One session: a cluster-robust factor-variable regression, then a 10,000-replicate pairs bootstrap — batched on the Apple GPU, exactly reproducible from seed(42) on any backend.


# Why Metrika

  • 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.
  • 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.
  • 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.
  • 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.
  • Big data on a laptop. DuckDB columnar engine with bulk C-API extraction: 10 million rows load in 0.2 s, summarize in ~0.3 s, regress in ~0.2 s. A Metal point-sprite renderer takes over scatter plots past 100k points and shrugs at 2,000,000.

# Screenshots

Swift Charts with by() groups
2,000,000 points — Metal renderer
Virtualized data browser with expression filter
Built-in manual for all 38 commands

# Install

Download Metrika.dmg — signed, notarized, and stapled. Drag into Applications. macOS 14+ on Apple Silicon.

# A five-minute tour

. sysuse sales                        // bundled 200-firm × 5-year panel
. gen log_rev = ln(revenue)
. summarize revenue price, detail

. reg log_rev price i.region, robust  // HC1 SEs, factor expansion
. predict yhat
. margins, dydx(price)                // delta-method standard errors

. xtset firm_id
. xtreg log_rev price, fe cluster(firm_id)
. ivregress 2sls log_rev (price = z1 z2), robust

. logit purchase price
. margins, dydx(price)

. bootstrap, reps(100000) seed(42): reg log_rev price   // GPU batched
. permute,  reps(10000)  seed(42): reg log_rev price    // exact p-values
. bayes, mcmcsize(20000) seed(42): reg log_rev price    // Gibbs sampler

. lasso log_rev price z1 z2 orders, lambda(0.05)         // glmnet-exact
. boost log_rev z1 z2 orders, rounds(100) maxdepth(3)    // xgboost-exact

. scatter log_rev price, by(region)
. histogram revenue, bins(20)
. save results.dta, replace           // native Stata .dta 118

Everything above works identically in the console, in .zyq do-files (⌘R in the editor), and headlessly through metrika-cli.

# What's inside

Pillar Contents
Data parquet, csv, json, arrow, native Stata .dta (read 117–119, write 118) · DuckDB engine · explicit missing-value semantics with listwise-deletion reporting
Estimation OLS (QR, never X'X) · logit / probit / poisson · 2SLS · panel fixed effects · summarize / tabulate / correlate
Inference robust HC0–HC3 · cluster-robust with Stata small-sample factors · GPU pairs bootstrap · permutation tests · Bayesian regression (Gibbs)
Machine learning lasso & elastic net (coordinate descent, glmnet-exact) · gradient-boosted trees (xgboost-exact)
Post-estimation predict (xb, residuals, pr, n) · margins, dydx() with delta-method SEs
Graphics Swift Charts scatter/line/histogram/kdensity · Metal renderer for millions of points
Extensibility .zyq script commands with args macros · native Swift ZQCommandPlugins with syntax validation and mutation gating

# Numerical validation

Metrika's test suite doesn't check that code runs — it checks that the numbers are right:

  • 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.
  • Lasso/elastic-net coefficients match glmnet (including its subtle gaussian y-standardization convention); the selection pattern — which coefficients are exactly zero — matches exactly.
  • Boosted-tree predictions match xgboost observation-by-observation.
  • 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.
  • With diffuse priors, the Bayesian posterior reproduces the frequentist answer within Monte-Carlo error — asserted, not assumed.

# Architecture

┌─────────────────────────────────────────────────────────┐
│                     Metrika (SwiftUI)                    │
│   Console · Data browser · Do-file editor · Manual       │
└──────────────────────────┬──────────────────────────────┘

┌──────────────────────────▼──────────────────────────────┐
│                   MetrikaKit (Swift package)             │
│                                                          │
│   ZQParser     command grammar → typed AST               │
│   ZQPlanner    AST → CPU / GPU / hybrid dispatch         │
│   ZQEngine     sessions, execution, logging, help        │
│   ZQData       DataFrame façade over DuckDB + .dta       │
│   ZQStats      LAPACK estimators (Accelerate)            │
│   ZQGPU        MLX batched solves, Philox RNG            │
│   ZQGraphics   plot specs → Swift Charts / Metal         │
│   ZQPlugins    user commands & sandboxing                │
└──────────────────────────────────────────────────────────┘

MetrikaKit has zero UI dependencies and is fully testable with swift test. ZQGPU is the only module allowed to touch MLX/Metal; ZQStats the only one touching Accelerate — backends stay swappable.

# Building from source

git clone https://github.com/spboucher-ai/metrika && cd metrika
./scripts/install_hooks.sh

# Library + CLI + tests
cd MetrikaKit
swift build && swift test                # 116 tests (GPU suites auto-skip)
xcodebuild test -scheme MetrikaKit-Package \
  -destination 'platform=macOS' -skipPackagePluginValidation   # + GPU suites

# App
cd .. && xcodegen generate
xcodebuild -project Metrika.xcodeproj -scheme Metrika \
  -skipPackagePluginValidation build

# Signed, notarized DMG
./scripts/release.sh

SwiftPM's CLI cannot compile Metal shaders, so swift test skips the GPU suites and CLI builds fall back to CPU automatically; xcodebuild runs give you everything.

# Documentation

  • In the app: the Manual tab, or help <command> in the console.
  • Static site: docs/index.html — regenerated from the same registry by metrika-cli docs, so it can never drift from the app.

# Extending Metrika

Drop a .zyq script into ~/Library/Application Support/Metrika/Commands/:

// logreg.zyq — its filename becomes the command
args response predictor
gen __log = ln(`response')
reg __log `predictor', robust
drop __log

…or compile a Swift plugin into the app:

public struct ZScorePlugin: ZQCommandPlugin {
    public static let verb = "zscore"
    public static let syntax = ZQSyntaxSpec(mutates: true)
    public func execute(_ ctx: ZQContext) async throws -> ZQResult {  }
}

Plugins that shadow built-ins are rejected at startup; datasets can only be mutated through a declared, gated channel.


© 2026 Simon-Pierre Boucher. All rights reserved. contact@spboucher.ai