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%

feat: v1.0 features — sample datasets, docs site, Metal 2M-point renderer

- sysuse command with bundled samples (sales: 200-firm x 5-year panel;
  mtcars) shipped as ZQEngine resources, generated by
  scripts/make_samples.R
- metrika-cli docs: static documentation site rendered from the shared
  command registry (docs/index.html, 38 commands, light/dark); CLI
  restructured with a default 'run' subcommand
- Metal point-sprite renderer (CLAUDE.md §7 pane 4): packed 12-byte
  vertices, unified-memory buffer written once, scroll-pan /
  pinch-zoom / double-click-reset touching only a 24-byte uniform;
  takes over scatter plots past 100k points (METRIKA_METAL_THRESHOLD
  override for tests); verified rendering 2,000,000 points in-app
- METRIKA_AUTORUN debug hook for headless app driving
- MARKETING_VERSION 1.0.0
- 116 kit tests + 4 UI tests green

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

Showing 18 changed files with +1,728 and −6

modified Metrika.xcodeproj/project.pbxproj +8 −0
@@ -7,11 +7,13 @@
7 7 objects = {
8 8
9 9 /* Begin PBXBuildFile section */
10 + 0B840EF0C831CE88C2C10C02 /* PointRenderer.metal in Sources */ = {isa = PBXBuildFile; fileRef = E080A0AFB04F825193F03420 /* PointRenderer.metal */; };
10 11 0BD0F927FC614CF19EF3D59D /* DoFileEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9BB0AC3BAA378743FD57FF7 /* DoFileEditorView.swift */; };
11 12 0F7000A49C9879593F09A0D3 /* MetrikaKit in Frameworks */ = {isa = PBXBuildFile; productRef = 498C4115F45BADDF16191A57 /* MetrikaKit */; };
12 13 3084AE2A328ED6746D01F46E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0404FDEAF8A57414EFB3C649 /* Assets.xcassets */; };
13 14 7E936A23CCC90823D6B08BCC /* SessionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0453FB0F256E853D2C2F481C /* SessionModel.swift */; };
14 15 8B0CDB213D64E3EE87593808 /* ConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 159695CF89D6BB25DF883F07 /* ConsoleView.swift */; };
16 + 936699328DFFABBE4AB89A9D /* MetalScatterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E05D9876A717AAC190A2750A /* MetalScatterView.swift */; };
15 17 9BE65709781719FAE0CA740A /* MetrikaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5393DC050C555E4FB69FC0 /* MetrikaApp.swift */; };
16 18 CB602D8A4EDDF5D0FBB0CFB7 /* ConsoleSmokeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB7ECE59F86249B3A25B17A4 /* ConsoleSmokeTests.swift */; };
17 19 D01BD59F090074D4C3FC392D /* ManualView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7C64C028218A21FE8E5D9D1 /* ManualView.swift */; };
@@ -43,6 +45,8 @@
43 45 A7C64C028218A21FE8E5D9D1 /* ManualView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManualView.swift; sourceTree = "<group>"; };
44 46 AB7ECE59F86249B3A25B17A4 /* ConsoleSmokeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleSmokeTests.swift; sourceTree = "<group>"; };
45 47 C9BB0AC3BAA378743FD57FF7 /* DoFileEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoFileEditorView.swift; sourceTree = "<group>"; };
48 + E05D9876A717AAC190A2750A /* MetalScatterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalScatterView.swift; sourceTree = "<group>"; };
49 + E080A0AFB04F825193F03420 /* PointRenderer.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = PointRenderer.metal; sourceTree = "<group>"; };
46 50 /* End PBXFileReference section */
47 51
48 52 /* Begin PBXFrameworksBuildPhase section */
@@ -81,8 +85,10 @@
81 85 2B8E64F5B2DE7930640AEE29 /* DataBrowserView.swift */,
82 86 C9BB0AC3BAA378743FD57FF7 /* DoFileEditorView.swift */,
83 87 A7C64C028218A21FE8E5D9D1 /* ManualView.swift */,
88 + E05D9876A717AAC190A2750A /* MetalScatterView.swift */,
84 89 9C5393DC050C555E4FB69FC0 /* MetrikaApp.swift */,
85 90 4AD551AA707CD1853C3D6D21 /* PlotView.swift */,
91 + E080A0AFB04F825193F03420 /* PointRenderer.metal */,
86 92 0453FB0F256E853D2C2F481C /* SessionModel.swift */,
87 93 );
88 94 path = Sources;
@@ -234,8 +240,10 @@
234 240 F0A49DC471FA794BE5948A37 /* DataBrowserView.swift in Sources */,
235 241 0BD0F927FC614CF19EF3D59D /* DoFileEditorView.swift in Sources */,
236 242 D01BD59F090074D4C3FC392D /* ManualView.swift in Sources */,
243 + 936699328DFFABBE4AB89A9D /* MetalScatterView.swift in Sources */,
237 244 9BE65709781719FAE0CA740A /* MetrikaApp.swift in Sources */,
238 245 DDCC2DC487AF9FAC7D6B8E3E /* PlotView.swift in Sources */,
246 + 0B840EF0C831CE88C2C10C02 /* PointRenderer.metal in Sources */,
239 247 7E936A23CCC90823D6B08BCC /* SessionModel.swift in Sources */,
240 248 );
241 249 runOnlyForDeploymentPostprocessing = 0;
added Metrika/Sources/MetalScatterView.swift +232 −0
@@ -0,0 +1,232 @@
1 +//
2 +// MetalScatterView.swift
3 +// Metrika
4 +//
5 +// Author: Simon-Pierre Boucher
6 +// Contact: contact@spboucher.ai
7 +// Copyright © 2026 Simon-Pierre Boucher. All rights reserved.
8 +//
9 +
10 +import MetalKit
11 +import SwiftUI
12 +import ZQGraphics
13 +
14 +/// Metal point-sprite renderer for scatter plots past the Swift Charts
15 +/// threshold (CLAUDE.md §7 pane 4). Unified memory: the packed vertex
16 +/// buffer is written once; pan (scroll) and zoom (pinch or scroll+⌥)
17 +/// only touch a 24-byte uniform.
18 +struct MetalScatterPane: View {
19 + let plot: ZQPlotSpec
20 +
21 + var body: some View {
22 + VStack(spacing: 0) {
23 + MetalScatterView(plot: plot)
24 + Divider()
25 + HStack {
26 + Text("\(plot.series.reduce(0) { $0 + $1.x.count }) points — Metal renderer")
27 + Spacer()
28 + Text("scroll to pan · pinch or ⌥-scroll to zoom · double-click to reset")
29 + }
30 + .font(.caption)
31 + .foregroundStyle(.secondary)
32 + .padding(6)
33 + }
34 + }
35 +}
36 +
37 +struct MetalScatterView: NSViewRepresentable {
38 + let plot: ZQPlotSpec
39 +
40 + func makeCoordinator() -> Renderer { Renderer() }
41 +
42 + func makeNSView(context: Context) -> ScatterMTKView {
43 + let view = ScatterMTKView()
44 + view.device = MTLCreateSystemDefaultDevice()
45 + view.delegate = context.coordinator
46 + view.renderer = context.coordinator
47 + view.enableSetNeedsDisplay = true
48 + view.isPaused = true
49 + view.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
50 + view.layer?.isOpaque = false
51 + context.coordinator.attach(view: view)
52 + context.coordinator.load(plot: plot)
53 + return view
54 + }
55 +
56 + func updateNSView(_ view: ScatterMTKView, context: Context) {
57 + context.coordinator.load(plot: plot)
58 + view.needsDisplay = true
59 + }
60 +
61 + /// MTKView subclass owning the pan/zoom gestures.
62 + final class ScatterMTKView: MTKView {
63 + weak var renderer: Renderer?
64 +
65 + override func scrollWheel(with event: NSEvent) {
66 + if event.modifierFlags.contains(.option) {
67 + renderer?.zoom(by: 1 + event.scrollingDeltaY * 0.01)
68 + } else {
69 + renderer?.pan(
70 + deltaX: event.scrollingDeltaX,
71 + deltaY: event.scrollingDeltaY,
72 + viewSize: bounds.size
73 + )
74 + }
75 + needsDisplay = true
76 + }
77 +
78 + override func magnify(with event: NSEvent) {
79 + renderer?.zoom(by: 1 + event.magnification)
80 + needsDisplay = true
81 + }
82 +
83 + override func mouseDown(with event: NSEvent) {
84 + if event.clickCount == 2 {
85 + renderer?.resetViewport()
86 + needsDisplay = true
87 + }
88 + }
89 + }
90 +
91 + /// Pipeline + vertex buffer + viewport state.
92 + @MainActor
93 + final class Renderer: NSObject, MTKViewDelegate {
94 + private struct Uniforms {
95 + var center: SIMD2<Float>
96 + var scale: SIMD2<Float>
97 + var pointSize: Float
98 + }
99 +
100 + private var pipeline: MTLRenderPipelineState?
101 + private var commandQueue: MTLCommandQueue?
102 + private var vertexBuffer: MTLBuffer?
103 + private var pointCount = 0
104 + private var loadedPlotSignature = 0
105 +
106 + // Data bounds and viewport state.
107 + private var dataCenter = SIMD2<Float>(0, 0)
108 + private var dataHalfSpan = SIMD2<Float>(1, 1)
109 + private var zoomLevel: Float = 1
110 + private var panOffset = SIMD2<Float>(0, 0)
111 + private var aspect: Float = 1
112 +
113 + func attach(view: MTKView) {
114 + guard let device = view.device else { return }
115 + commandQueue = device.makeCommandQueue()
116 + guard let library = device.makeDefaultLibrary(),
117 + let vertexFunction = library.makeFunction(name: "scatterVertex"),
118 + let fragmentFunction = library.makeFunction(name: "scatterFragment")
119 + else { return }
120 +
121 + let descriptor = MTLRenderPipelineDescriptor()
122 + descriptor.vertexFunction = vertexFunction
123 + descriptor.fragmentFunction = fragmentFunction
124 + descriptor.colorAttachments[0].pixelFormat = view.colorPixelFormat
125 + descriptor.colorAttachments[0].isBlendingEnabled = true
126 + descriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
127 + descriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
128 + descriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
129 + descriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
130 + pipeline = try? device.makeRenderPipelineState(descriptor: descriptor)
131 + }
132 +
133 + /// Rebuilds the vertex buffer when the plot actually changes.
134 + func load(plot: ZQPlotSpec) {
135 + let signature = plot.series.reduce(plot.series.count) {
136 + $0 &* 31 &+ $1.x.count
137 + }
138 + guard signature != loadedPlotSignature,
139 + let device = commandQueue?.device else { return }
140 + loadedPlotSignature = signature
141 +
142 + let palette: [SIMD4<UInt8>] = [
143 + .init(79, 140, 255, 220), .init(255, 149, 0, 220),
144 + .init(52, 199, 89, 220), .init(175, 82, 222, 220),
145 + .init(255, 59, 48, 220), .init(90, 200, 250, 220),
146 + ]
147 +
148 + var minX = Float.greatestFiniteMagnitude, maxX = -Float.greatestFiniteMagnitude
149 + var minY = Float.greatestFiniteMagnitude, maxY = -Float.greatestFiniteMagnitude
150 + var packed = [UInt8]()
151 + var count = 0
152 + for (seriesIndex, series) in plot.series.enumerated() {
153 + let color = palette[seriesIndex % palette.count]
154 + for i in 0..<series.x.count {
155 + let x = Float(series.x[i])
156 + let y = Float(series.y[i])
157 + guard x.isFinite, y.isFinite else { continue }
158 + minX = min(minX, x); maxX = max(maxX, x)
159 + minY = min(minY, y); maxY = max(maxY, y)
160 + withUnsafeBytes(of: x) { packed.append(contentsOf: $0) }
161 + withUnsafeBytes(of: y) { packed.append(contentsOf: $0) }
162 + packed.append(contentsOf: [color.x, color.y, color.z, color.w])
163 + count += 1
164 + }
165 + }
166 + guard count > 0 else { return }
167 + pointCount = count
168 + vertexBuffer = packed.withUnsafeBytes {
169 + device.makeBuffer(bytes: $0.baseAddress!, length: $0.count)
170 + }
171 +
172 + dataCenter = SIMD2((minX + maxX) / 2, (minY + maxY) / 2)
173 + dataHalfSpan = SIMD2(
174 + max((maxX - minX) / 2, 1e-9) * 1.05,
175 + max((maxY - minY) / 2, 1e-9) * 1.05
176 + )
177 + resetViewport()
178 + }
179 +
180 + func resetViewport() {
181 + zoomLevel = 1
182 + panOffset = .zero
183 + }
184 +
185 + func zoom(by factor: Double) {
186 + zoomLevel = min(max(zoomLevel * Float(factor), 0.1), 10_000)
187 + }
188 +
189 + func pan(deltaX: Double, deltaY: Double, viewSize: CGSize) {
190 + guard viewSize.width > 0, viewSize.height > 0 else { return }
191 + panOffset.x -= Float(deltaX) * 2 * dataHalfSpan.x
192 + / (zoomLevel * Float(viewSize.width))
193 + panOffset.y += Float(deltaY) * 2 * dataHalfSpan.y
194 + / (zoomLevel * Float(viewSize.height))
195 + }
196 +
197 + nonisolated func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
198 +
199 + nonisolated func draw(in view: MTKView) {
200 + MainActor.assumeIsolated {
201 + render(in: view)
202 + }
203 + }
204 +
205 + private func render(in view: MTKView) {
206 + guard let pipeline,
207 + let vertexBuffer,
208 + pointCount > 0,
209 + let descriptor = view.currentRenderPassDescriptor,
210 + let drawable = view.currentDrawable,
211 + let commandBuffer = commandQueue?.makeCommandBuffer(),
212 + let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)
213 + else { return }
214 +
215 + var uniforms = Uniforms(
216 + center: dataCenter + panOffset,
217 + scale: SIMD2(
218 + zoomLevel / dataHalfSpan.x,
219 + zoomLevel / dataHalfSpan.y
220 + ),
221 + pointSize: Float(max(2, 6 - log10(Double(max(pointCount, 10)))))
222 + )
223 + encoder.setRenderPipelineState(pipeline)
224 + encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
225 + encoder.setVertexBytes(&uniforms, length: MemoryLayout<Uniforms>.size, index: 1)
226 + encoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: pointCount)
227 + encoder.endEncoding()
228 + commandBuffer.present(drawable)
229 + commandBuffer.commit()
230 + }
231 + }
232 +}
modified Metrika/Sources/PlotView.swift +16 −1
@@ -16,8 +16,23 @@ import ZQGraphics
16 16 struct PlotView: View {
17 17 @Environment(SessionModel.self) private var model
18 18
19 + /// Past this many points, Swift Charts degrades — the Metal renderer
20 + /// takes over (overridable for tests via METRIKA_METAL_THRESHOLD).
21 + private static let metalThreshold: Int = {
22 + if let text = ProcessInfo.processInfo.environment["METRIKA_METAL_THRESHOLD"],
23 + let value = Int(text) {
24 + return value
25 + }
26 + return 100_000
27 + }()
28 +
19 29 var body: some View {
20 if let plot = model.lastPlot {
30 + if let plot = model.lastPlot,
31 + plot.kind == .scatter,
32 + plot.series.reduce(0, { $0 + $1.x.count }) > Self.metalThreshold {
33 + MetalScatterPane(plot: plot)
34 + .accessibilityLabel("Metal scatter renderer")
35 + } else if let plot = model.lastPlot {
21 36 Chart {
22 37 ForEach(Array(plot.series.enumerated()), id: \.offset) { _, series in
23 38 ForEach(series.x.indices, id: \.self) { index in
added Metrika/Sources/PointRenderer.metal +58 −0
@@ -0,0 +1,58 @@
1 +//
2 +// PointRenderer.metal
3 +// Metrika
4 +//
5 +// Author: Simon-Pierre Boucher
6 +// Contact: contact@spboucher.ai
7 +// Copyright © 2026 Simon-Pierre Boucher. All rights reserved.
8 +//
9 +// Instanced point-sprite renderer for large scatter plots (CLAUDE.md §7:
10 +// custom MTKView renderer past the Swift Charts threshold). One packed
11 +// 12-byte vertex per observation; the fragment stage rounds sprites.
12 +//
13 +
14 +#include <metal_stdlib>
15 +using namespace metal;
16 +
17 +struct PackedPoint {
18 + packed_float2 position; // data coordinates
19 + uchar4 color;
20 +};
21 +
22 +struct Uniforms {
23 + float2 center; // data-space center of the viewport
24 + float2 scale; // data → clip scaling (zoom × aspect)
25 + float pointSize;
26 +};
27 +
28 +struct VertexOut {
29 + float4 position [[position]];
30 + float pointSize [[point_size]];
31 + float4 color;
32 +};
33 +
34 +vertex VertexOut scatterVertex(
35 + uint vertexID [[vertex_id]],
36 + const device PackedPoint *points [[buffer(0)]],
37 + constant Uniforms &uniforms [[buffer(1)]]
38 +) {
39 + PackedPoint point = points[vertexID];
40 + float2 clip = (float2(point.position) - uniforms.center) * uniforms.scale;
41 +
42 + VertexOut out;
43 + out.position = float4(clip, 0.0, 1.0);
44 + out.pointSize = uniforms.pointSize;
45 + out.color = float4(point.color) / 255.0;
46 + return out;
47 +}
48 +
49 +fragment float4 scatterFragment(
50 + VertexOut in [[stage_in]],
51 + float2 pointCoord [[point_coord]]
52 +) {
53 + float2 offset = pointCoord - 0.5;
54 + if (dot(offset, offset) > 0.25) {
55 + discard_fragment();
56 + }
57 + return in.color;
58 +}
modified Metrika/Sources/SessionModel.swift +8 −0
@@ -66,6 +66,14 @@ final class SessionModel {
66 66 try? Data(csv.utf8).write(to: url)
67 67 Task { await run("use \(url.path)") }
68 68 }
69 + // Headless driving: semicolon-separated commands run at startup.
70 + if let script = ProcessInfo.processInfo.environment["METRIKA_AUTORUN"] {
71 + Task {
72 + for command in script.split(separator: ";") {
73 + await run(command.trimmingCharacters(in: .whitespaces))
74 + }
75 + }
76 + }
69 77 #endif
70 78 }
71 79
modified MetrikaKit/Package.swift +1 −0
@@ -80,6 +80,7 @@ let package = Package(
80 80 "ZQParser", "ZQPlanner", "ZQData", "ZQStats",
81 81 "ZQGPU", "ZQGraphics", "ZQPlugins",
82 82 ],
83 + resources: [.copy("Resources/Samples")],
83 84 swiftSettings: strictConcurrency
84 85 ),
85 86 .executableTarget(
added MetrikaKit/Sources/MetrikaCLI/DocsCommand.swift +144 −0
@@ -0,0 +1,144 @@
1 +//
2 +// DocsCommand.swift
3 +// Metrika
4 +//
5 +// Author: Simon-Pierre Boucher
6 +// Contact: contact@spboucher.ai
7 +// Copyright © 2026 Simon-Pierre Boucher. All rights reserved.
8 +//
9 +
10 +import ArgumentParser
11 +import Foundation
12 +import ZQEngine
13 +
14 +/// `metrika-cli docs --output docs/` — renders the shared command
15 +/// reference (the same registry behind the app's Manual pane and the
16 +/// console `help`) into a self-contained static site.
17 +struct DocsCommand: ParsableCommand {
18 + static let configuration = CommandConfiguration(
19 + commandName: "docs",
20 + abstract: "Generate the static documentation site from the command reference."
21 + )
22 +
23 + @Option(name: .shortAndLong, help: "Output directory.")
24 + var output: String = "docs"
25 +
26 + func run() throws {
27 + let directory = URL(fileURLWithPath: output, isDirectory: true)
28 + try FileManager.default.createDirectory(
29 + at: directory, withIntermediateDirectories: true
30 + )
31 + let html = Self.render()
32 + try Data(html.utf8).write(to: directory.appendingPathComponent("index.html"))
33 + print("wrote \(output)/index.html (\(ZQCommandReference.all.count) commands)")
34 + }
35 +
36 + static func escape(_ text: String) -> String {
37 + text.replacingOccurrences(of: "&", with: "&amp;")
38 + .replacingOccurrences(of: "<", with: "&lt;")
39 + .replacingOccurrences(of: ">", with: "&gt;")
40 + }
41 +
42 + static func render() -> String {
43 + var nav = ""
44 + var body = ""
45 +
46 + for category in ZQCommandReference.categories {
47 + let docs = ZQCommandReference.all.filter { $0.category == category }
48 + guard !docs.isEmpty else { continue }
49 + nav += "<h3>\(escape(category))</h3><ul>"
50 + body += "<h2 class=\"category\">\(escape(category))</h2>\n"
51 + for doc in docs {
52 + nav += "<li><a href=\"#\(doc.verb)\"><code>\(doc.verb)</code></a></li>"
53 + body += renderCommand(doc)
54 + }
55 + nav += "</ul>"
56 + }
57 +
58 + return """
59 + <!DOCTYPE html>
60 + <html lang="en">
61 + <head>
62 + <meta charset="utf-8">
63 + <meta name="viewport" content="width=device-width, initial-scale=1">
64 + <title>Metrika — Command Reference</title>
65 + <style>
66 + :root { color-scheme: light dark;
67 + --accent: #4f8cff; --muted: #7a8199; --border: #d9dce6;
68 + --code-bg: rgba(120, 130, 180, 0.12); }
69 + @media (prefers-color-scheme: dark) {
70 + :root { --border: #2c3145; } }
71 + * { box-sizing: border-box; }
72 + body { margin: 0; font: 15px/1.55 -apple-system, "Helvetica Neue", sans-serif;
73 + display: flex; min-height: 100vh; }
74 + nav { width: 230px; flex-shrink: 0; padding: 24px 18px; overflow-y: auto;
75 + position: sticky; top: 0; height: 100vh; border-right: 1px solid var(--border); }
76 + nav h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
77 + color: var(--muted); margin: 18px 0 6px; }
78 + nav ul { list-style: none; margin: 0; padding: 0; }
79 + nav a { text-decoration: none; color: inherit; }
80 + nav a:hover code { color: var(--accent); }
81 + main { flex: 1; max-width: 760px; padding: 32px 40px 80px; }
82 + h1 { font-size: 28px; margin: 0 0 4px; }
83 + .subtitle { color: var(--muted); margin-bottom: 28px; }
84 + h2.category { font-size: 13px; text-transform: uppercase; letter-spacing: .08em;
85 + color: var(--muted); border-bottom: 1px solid var(--border);
86 + padding-bottom: 6px; margin-top: 44px; }
87 + article { margin: 26px 0; }
88 + article h2 { font-size: 20px; margin: 0 0 2px; font-family: ui-monospace, monospace; }
89 + .abbr { font-size: 12px; color: var(--muted); margin-left: 8px; }
90 + pre { background: var(--code-bg); border-radius: 8px; padding: 10px 14px;
91 + overflow-x: auto; font: 13px ui-monospace, "SF Mono", monospace; }
92 + code { font: 13px ui-monospace, "SF Mono", monospace; }
93 + table { border-collapse: collapse; margin: 8px 0; }
94 + td { padding: 3px 14px 3px 0; vertical-align: top; font-size: 14px; }
95 + td:first-child { font-family: ui-monospace, monospace; white-space: nowrap; }
96 + .notes { color: var(--muted); font-size: 14px; }
97 + footer { color: var(--muted); font-size: 12px; margin-top: 56px;
98 + border-top: 1px solid var(--border); padding-top: 12px; }
99 + </style>
100 + </head>
101 + <body>
102 + <nav>\(nav)</nav>
103 + <main>
104 + <h1>Metrika</h1>
105 + <p class="subtitle">Command reference — Stata-class syntax, GPU-accelerated by Apple Silicon.
106 + Every command follows one grammar:
107 + <code>command [varlist] [if] [in] [, options]</code></p>
108 + \(body)
109 + <footer>© 2026 Simon-Pierre Boucher. Generated from the in-app command
110 + reference by <code>metrika-cli docs</code>.</footer>
111 + </main>
112 + </body>
113 + </html>
114 + """
115 + }
116 +
117 + static func renderCommand(_ doc: ZQCommandDoc) -> String {
118 + var html = "<article id=\"\(doc.verb)\">"
119 + html += "<h2>\(escape(doc.verb))"
120 + if let abbreviation = doc.abbreviation {
121 + html += "<span class=\"abbr\">abbreviation: \(escape(abbreviation))</span>"
122 + }
123 + html += "</h2>"
124 + html += "<p>\(escape(doc.summary))</p>"
125 + html += "<pre>\(escape(doc.syntax))</pre>"
126 + if !doc.options.isEmpty {
127 + html += "<table>"
128 + for option in doc.options {
129 + html += "<tr><td>\(escape(option.name))</td><td>\(escape(option.meaning))</td></tr>"
130 + }
131 + html += "</table>"
132 + }
133 + if !doc.examples.isEmpty {
134 + html += "<pre>"
135 + + doc.examples.map { ". " + escape($0) }.joined(separator: "\n")
136 + + "</pre>"
137 + }
138 + if let notes = doc.notes {
139 + html += "<p class=\"notes\">\(escape(notes))</p>"
140 + }
141 + html += "</article>"
142 + return html
143 + }
144 +}
modified MetrikaKit/Sources/MetrikaCLI/MetrikaCLI.swift +13 −2
@@ -12,12 +12,23 @@ import Foundation
12 12 import ZQEngine
13 13
14 14 /// Companion command-line tool: runs ZQL commands or `.zyq` do-files
15 /// against a headless session. Interactive REPL when no input is given.
15 +/// against a headless session (default subcommand), and generates the
16 +/// documentation site (`docs`).
16 17 @main
17 18 struct MetrikaCLI: AsyncParsableCommand {
18 19 static let configuration = CommandConfiguration(
19 20 commandName: "metrika-cli",
20 abstract: "Metrika statistical engine — command-line interface."
21 + abstract: "Metrika statistical engine — command-line interface.",
22 + subcommands: [RunCommand.self, DocsCommand.self],
23 + defaultSubcommand: RunCommand.self
24 + )
25 +}
26 +
27 +/// Default behavior: execute commands, run a do-file, or start a REPL.
28 +struct RunCommand: AsyncParsableCommand {
29 + static let configuration = CommandConfiguration(
30 + commandName: "run",
31 + abstract: "Execute ZQL commands or a do-file (default)."
21 32 )
22 33
23 34 @Option(
modified MetrikaKit/Sources/ZQEngine/CommandHelp.swift +7 −0
@@ -63,6 +63,13 @@ public enum ZQCommandReference {
63 63 examples: ["use sales.parquet", "use ~/data/panel.dta"],
64 64 notes: "Loading goes through DuckDB for columnar formats and a native reader for .dta. The sandboxed app reads user-selected files and its own container."
65 65 ),
66 + ZQCommandDoc(
67 + verb: "sysuse", category: "Data management",
68 + summary: "Load a sample dataset shipped with Metrika. Bare `sysuse` lists what is available.",
69 + syntax: "sysuse [name]",
70 + examples: ["sysuse sales", "sysuse mtcars"],
71 + notes: "sales: 200-firm × 5-year panel (revenue, price, region, employees). mtcars: the R classic, with a string model column."
72 + ),
66 73 ZQCommandDoc(
67 74 verb: "save", category: "Data management",
68 75 summary: "Write the working dataset to disk (.parquet, .csv, or Stata .dta format 118).",
added MetrikaKit/Sources/ZQEngine/Resources/Samples/mtcars.csv +33 −0
@@ -0,0 +1,33 @@
1 +"model","mpg","cyl","disp","hp","drat","wt","qsec","vs","am","gear","carb"
2 +"Mazda RX4",21,6,160,110,3.9,2.62,16.46,0,1,4,4
3 +"Mazda RX4 Wag",21,6,160,110,3.9,2.875,17.02,0,1,4,4
4 +"Datsun 710",22.8,4,108,93,3.85,2.32,18.61,1,1,4,1
5 +"Hornet 4 Drive",21.4,6,258,110,3.08,3.215,19.44,1,0,3,1
6 +"Hornet Sportabout",18.7,8,360,175,3.15,3.44,17.02,0,0,3,2
7 +"Valiant",18.1,6,225,105,2.76,3.46,20.22,1,0,3,1
8 +"Duster 360",14.3,8,360,245,3.21,3.57,15.84,0,0,3,4
9 +"Merc 240D",24.4,4,146.7,62,3.69,3.19,20,1,0,4,2
10 +"Merc 230",22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2
11 +"Merc 280",19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4
12 +"Merc 280C",17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4
13 +"Merc 450SE",16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3
14 +"Merc 450SL",17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3
15 +"Merc 450SLC",15.2,8,275.8,180,3.07,3.78,18,0,0,3,3
16 +"Cadillac Fleetwood",10.4,8,472,205,2.93,5.25,17.98,0,0,3,4
17 +"Lincoln Continental",10.4,8,460,215,3,5.424,17.82,0,0,3,4
18 +"Chrysler Imperial",14.7,8,440,230,3.23,5.345,17.42,0,0,3,4
19 +"Fiat 128",32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1
20 +"Honda Civic",30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2
21 +"Toyota Corolla",33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1
22 +"Toyota Corona",21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1
23 +"Dodge Challenger",15.5,8,318,150,2.76,3.52,16.87,0,0,3,2
24 +"AMC Javelin",15.2,8,304,150,3.15,3.435,17.3,0,0,3,2
25 +"Camaro Z28",13.3,8,350,245,3.73,3.84,15.41,0,0,3,4
26 +"Pontiac Firebird",19.2,8,400,175,3.08,3.845,17.05,0,0,3,2
27 +"Fiat X1-9",27.3,4,79,66,4.08,1.935,18.9,1,1,4,1
28 +"Porsche 914-2",26,4,120.3,91,4.43,2.14,16.7,0,1,5,2
29 +"Lotus Europa",30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2
30 +"Ford Pantera L",15.8,8,351,264,4.22,3.17,14.5,0,1,5,4
31 +"Ferrari Dino",19.7,6,145,175,3.62,2.77,15.5,0,1,5,6
32 +"Maserati Bora",15,8,301,335,3.54,3.57,14.6,0,1,5,8
33 +"Volvo 142E",21.4,4,121,109,4.11,2.78,18.6,1,1,4,2
added MetrikaKit/Sources/ZQEngine/Resources/Samples/sales.csv +1001 −0
@@ -0,0 +1,1001 @@
1 +firm_id,year,region,price,revenue,employees
2 +1,2019,1,13.8188,26.4832,5
3 +1,2020,1,17.7237,26.4094,7
4 +1,2021,1,14.3484,30.6098,9
5 +1,2022,1,20.1911,21.9176,7
6 +1,2023,1,10.5081,65.2711,15
7 +2,2019,1,8.4722,29.1197,7
8 +2,2020,1,22.0202,16.0532,5
9 +2,2021,1,18.2505,18.5946,6
10 +2,2022,1,10.2143,33.0126,4
11 +2,2023,1,23.8342,18.4339,6
12 +3,2019,1,9.4742,28.8469,11
13 +3,2020,1,15.8281,23.0886,5
14 +3,2021,1,9.6621,69.3117,9
15 +3,2022,1,19.0466,17.7664,6
16 +3,2023,1,25.6289,18.0874,4
17 +4,2019,2,17.1929,11.7742,5
18 +4,2020,2,19.6243,15.1478,3
19 +4,2021,2,17.7873,15.3628,13
20 +4,2022,2,20.7242,10.8188,7
21 +4,2023,2,6.7832,36.1605,7
22 +5,2019,1,5.4886,97.4772,9
23 +5,2020,1,15.6086,30.4995,9
24 +5,2021,1,12.8387,36.1265,8
25 +5,2022,1,19.7446,32.3737,7
26 +5,2023,1,24.1304,17.6494,6
27 +6,2019,3,12.0201,45.3476,7
28 +6,2020,3,23.7536,21.1541,8
29 +6,2021,3,23.7348,19.3619,5
30 +6,2022,3,6.6427,67.9187,9
31 +6,2023,3,25.6725,28.3471,5
32 +7,2019,3,5.1948,35.6952,6
33 +7,2020,3,14.5087,31.191,6
34 +7,2021,3,7.5117,33.257,8
35 +7,2022,3,23.7588,11.112,6
36 +7,2023,3,8.2972,32.0576,7
37 +8,2019,4,20.1045,20.4873,16
38 +8,2020,4,20.7911,25.0039,11
39 +8,2021,4,18.5145,44.2152,13
40 +8,2022,4,19.7068,37.5996,8
41 +8,2023,4,17.2698,74.2775,7
42 +9,2019,4,24.2438,13.1857,6
43 +9,2020,4,18.6541,17.3394,4
44 +9,2021,4,17.346,27.1637,7
45 +9,2022,4,16.2658,21.6728,7
46 +9,2023,4,13.9039,32.1277,4
47 +10,2019,4,23.569,15.226,6
48 +10,2020,4,24.3319,13.4569,7
49 +10,2021,4,15.8814,24.2056,5
50 +10,2022,4,21.8208,16.6568,4
51 +10,2023,4,13.5194,30.6397,7
52 +11,2019,1,20.1874,13.9931,3
53 +11,2020,1,19.0085,11.9489,10
54 +11,2021,1,22.9635,7.9024,6
55 +11,2022,1,6.3124,28.8013,4
56 +11,2023,1,14.9045,15.0521,5
57 +12,2019,3,5.6838,43.4331,8
58 +12,2020,3,18.2387,16.394,5
59 +12,2021,3,16.5559,21.7636,6
60 +12,2022,3,23.4144,14.218,8
61 +12,2023,3,26.1635,6.3023,9
62 +13,2019,4,5.5091,49.0725,6
63 +13,2020,4,12.4414,58.2992,9
64 +13,2021,4,15.9237,32.4851,7
65 +13,2022,4,9.7766,49.0527,8
66 +13,2023,4,10.4376,56.2516,8
67 +14,2019,2,13.1873,39.2138,10
68 +14,2020,2,9.0762,37.7254,9
69 +14,2021,2,24.1035,18.0675,7
70 +14,2022,2,10.5768,56.5536,9
71 +14,2023,2,22.1168,24.006,9
72 +15,2019,2,17.8222,16.606,8
73 +15,2020,2,21.9686,13.1297,6
74 +15,2021,2,15.6616,18.2082,7
75 +15,2022,2,15.8311,21.8368,6
76 +15,2023,2,9.2348,42.116,7
77 +16,2019,4,20.4876,18.7643,5
78 +16,2020,4,19.8259,23.7656,15
79 +16,2021,4,23.8775,19.6511,8
80 +16,2022,4,19.9317,31.3511,7
81 +16,2023,4,26.4656,12.5106,4
82 +17,2019,3,18.6442,21.6793,7
83 +17,2020,3,8.4674,55.3388,8
84 +17,2021,3,18.6222,24.9631,8
85 +17,2022,3,13.9804,23.4273,7
86 +17,2023,3,24.3666,16.9619,7
87 +18,2019,3,14.0281,58.2869,10
88 +18,2020,3,10.4087,50.0897,12
89 +18,2021,3,11.9779,59.3314,7
90 +18,2022,3,11.6927,53.4208,8
91 +18,2023,3,9.2506,94.9052,6
92 +19,2019,1,20.3819,40.1318,9
93 +19,2020,1,13.0506,59.2871,7
94 +19,2021,1,20.3265,39.0047,8
95 +19,2022,1,12.3828,63.2152,16
96 +19,2023,1,23.9252,46.8862,15
97 +20,2019,2,19.2034,17.4556,5
98 +20,2020,2,15.3091,15.07,7
99 +20,2021,2,8.9477,24.4905,7
100 +20,2022,2,20.7484,14.7976,15
101 +20,2023,2,13.1751,24.7756,4
102 +21,2019,1,22.3879,25.8707,9
103 +21,2020,1,15.1558,39.5224,11
104 +21,2021,1,10.6058,55.6019,7
105 +21,2022,1,10.331,80.8303,10
106 +21,2023,1,25.8201,28.4423,7
107 +22,2019,4,14.7043,74.488,9
108 +22,2020,4,10.0538,93.7313,15
109 +22,2021,4,19.1966,61.7593,10
110 +22,2022,4,7.2223,92.9447,10
111 +22,2023,4,15.4244,57.2499,10
112 +23,2019,2,14.0132,38.2272,5
113 +23,2020,2,19.7911,19.3886,3
114 +23,2021,2,17.4176,27.8724,9
115 +23,2022,2,8.6236,73.805,5
116 +23,2023,2,11.5446,40.4094,3
117 +24,2019,2,23.3233,15.6071,8
118 +24,2020,2,8.4946,31.5549,5
119 +24,2021,2,12.6422,43.2445,9
120 +24,2022,2,11.2194,50.236,7
121 +24,2023,2,20.317,35.0974,9
122 +25,2019,2,22.3808,8.3779,8
123 +25,2020,2,14.7784,15.7033,4
124 +25,2021,2,6.1567,31.1982,7
125 +25,2022,2,17.5573,16.2456,5
126 +25,2023,2,6.9654,37.9968,4
127 +26,2019,2,21.8867,16.2994,6
128 +26,2020,2,7.3667,57.4986,6
129 +26,2021,2,10.0478,50.7926,6
130 +26,2022,2,23.3628,19.8791,5
131 +26,2023,2,13.1193,55.9471,6
132 +27,2019,2,21.2914,10.0827,8
133 +27,2020,2,11.5517,33.8296,6
134 +27,2021,2,9.7943,45.4652,5
135 +27,2022,2,8.681,31.5088,8
136 +27,2023,2,16.9693,24.482,5
137 +28,2019,1,20.1401,12.7528,5
138 +28,2020,1,20.0696,15.2402,8
139 +28,2021,1,6.8877,43.6088,6
140 +28,2022,1,19.9239,17.6806,7
141 +28,2023,1,19.6463,28.9877,5
142 +29,2019,1,19.0524,20.3629,6
143 +29,2020,1,8.6024,34.5596,12
144 +29,2021,1,17.365,22.1776,8
145 +29,2022,1,9.3791,44.4841,11
146 +29,2023,1,19.16,22.9806,5
147 +30,2019,4,15.3176,50.524,6
148 +30,2020,4,6.898,73.6949,8
149 +30,2021,4,24.7212,28.4771,12
150 +30,2022,4,25.8555,23.2191,6
151 +30,2023,4,23.9406,23.6779,7
152 +31,2019,4,13.5349,36.0615,10
153 +31,2020,4,8.5953,47.8659,9
154 +31,2021,4,19.5735,26.5424,7
155 +31,2022,4,17.5768,44.321,7
156 +31,2023,4,8.3341,63.0547,10
157 +32,2019,1,15.3842,48.5857,23
158 +32,2020,1,12.5495,47.917,11
159 +32,2021,1,7.2682,102.2311,14
160 +32,2022,1,21.7043,36.1054,12
161 +32,2023,1,11.4662,108.2383,13
162 +33,2019,4,16.1648,24.9216,10
163 +33,2020,4,11.4613,45.477,8
164 +33,2021,4,17.8664,32.4567,9
165 +33,2022,4,24.6839,23.8671,10
166 +33,2023,4,8.515,67.8256,14
167 +34,2019,3,10.6412,39.3992,11
168 +34,2020,3,19.174,35.9399,4
169 +34,2021,3,22.9909,16.3234,6
170 +34,2022,3,12.2928,39.9902,20
171 +34,2023,3,10.4235,40.5021,10
172 +35,2019,4,5.968,89.2419,4
173 +35,2020,4,24.0375,16.6661,9
174 +35,2021,4,20.4631,26.4378,7
175 +35,2022,4,9.2094,70.1697,14
176 +35,2023,4,16.7255,49.0901,6
177 +36,2019,3,9.9866,43.0128,6
178 +36,2020,3,10.0302,48.7513,14
179 +36,2021,3,20.1901,16.7248,6
180 +36,2022,3,18.3685,23.5421,6
181 +36,2023,3,20.2568,23.8966,5
182 +37,2019,3,5.6306,50.8153,4
183 +37,2020,3,22.5915,13.165,4
184 +37,2021,3,25.4237,10.8353,7
185 +37,2022,3,6.7835,44.8282,5
186 +37,2023,3,6.7823,37.3267,9
187 +38,2019,2,21.6548,14.4552,7
188 +38,2020,2,17.9643,28.4826,8
189 +38,2021,2,12.0454,34.6841,6
190 +38,2022,2,14.4002,39.2834,10
191 +38,2023,2,15.2778,28.4058,15
192 +39,2019,1,7.3776,45.6802,10
193 +39,2020,1,8.0402,53.7305,12
194 +39,2021,1,18.3892,21.5391,8
195 +39,2022,1,8.5462,54.8142,9
196 +39,2023,1,13.4406,44.2555,6
197 +40,2019,4,19.4334,37.923,5
198 +40,2020,4,24.128,18.2392,4
199 +40,2021,4,17.7082,37.5264,5
200 +40,2022,4,8.8995,76.9189,10
201 +40,2023,4,13.447,81.454,10
202 +41,2019,4,23.1874,30.0381,8
203 +41,2020,4,11.9534,58.2538,8
204 +41,2021,4,19.9954,38.0111,8
205 +41,2022,4,22.087,24.6684,6
206 +41,2023,4,9.5073,82.9634,6
207 +42,2019,2,23.4402,13.4955,10
208 +42,2020,2,13.8917,35.7257,7
209 +42,2021,2,23.9304,16.7005,8
210 +42,2022,2,9.8323,41.5618,6
211 +42,2023,2,22.8915,21.0388,4
212 +43,2019,2,5.454,78.556,13
213 +43,2020,2,22.1021,24.6231,8
214 +43,2021,2,25.2373,14.6737,8
215 +43,2022,2,20.4527,19.561,10
216 +43,2023,2,25.4335,19.1318,20
217 +44,2019,1,20.8129,8.2823,6
218 +44,2020,1,16.5158,15.0429,11
219 +44,2021,1,7.3907,29.783,9
220 +44,2022,1,7.7544,27.7163,5
221 +44,2023,1,19.1189,10.3871,8
222 +45,2019,4,18.921,32.8634,9
223 +45,2020,4,10.1239,64.2378,12
224 +45,2021,4,9.0342,56.9911,11
225 +45,2022,4,12.5873,73.3843,8
226 +45,2023,4,21.0964,36.5048,5
227 +46,2019,1,24.5034,9.2363,8
228 +46,2020,1,21.5453,13.3117,7
229 +46,2021,1,10.3015,34.4094,5
230 +46,2022,1,17.7011,13.1508,8
231 +46,2023,1,9.0349,52.4779,6
232 +47,2019,1,14.4475,23.439,15
233 +47,2020,1,14.2458,29.1136,8
234 +47,2021,1,22.9819,18.193,7
235 +47,2022,1,18.3259,21.5988,13
236 +47,2023,1,23.7578,21.3757,3
237 +48,2019,1,23.5557,15.1709,8
238 +48,2020,1,18.6671,22.9732,12
239 +48,2021,1,21.8548,32.6409,8
240 +48,2022,1,15.2088,34.0245,8
241 +48,2023,1,18.4267,30.9107,11
242 +49,2019,4,21.5134,14.8503,5
243 +49,2020,4,22.8877,22.3818,6
244 +49,2021,4,17.39,33.6805,6
245 +49,2022,4,23.6462,16.8369,7
246 +49,2023,4,25.1243,20.5659,11
247 +50,2019,1,16.651,10.858,5
248 +50,2020,1,7.3978,21.7986,5
249 +50,2021,1,18.1196,12.7901,4
250 +50,2022,1,15.5262,16.6642,3
251 +50,2023,1,8.7964,28.8991,12
252 +51,2019,1,22.2937,18.4591,9
253 +51,2020,1,6.019,41.7161,7
254 +51,2021,1,10.6975,35.1309,9
255 +51,2022,1,16.0107,26.6171,8
256 +51,2023,1,7.4445,57.111,6
257 +52,2019,1,19.5206,14.9707,5
258 +52,2020,1,19.6853,19.7203,6
259 +52,2021,1,17.9342,23.9436,4
260 +52,2022,1,11.7107,29.1194,4
261 +52,2023,1,17.4512,20.3323,4
262 +53,2019,2,11.246,59.2184,6
263 +53,2020,2,12.2349,52.6123,8
264 +53,2021,2,21.3804,22.0941,6
265 +53,2022,2,13.345,48.5669,13
266 +53,2023,2,26.2554,19.1015,7
267 +54,2019,1,16.5002,20.9207,6
268 +54,2020,1,15.7572,31.9234,8
269 +54,2021,1,11.1673,34.2755,8
270 +54,2022,1,22.7648,18.818,5
271 +54,2023,1,20.5493,25.2465,13
272 +55,2019,4,5.3363,98.6396,8
273 +55,2020,4,24.5476,18.7599,12
274 +55,2021,4,16.5205,51.6328,10
275 +55,2022,4,10.2473,98.0067,11
276 +55,2023,4,7.4201,84.6251,5
277 +56,2019,3,16.6451,31.4279,7
278 +56,2020,3,6.287,69.2501,8
279 +56,2021,3,17.4,34.9615,5
280 +56,2022,3,24.6096,27.488,11
281 +56,2023,3,12.4508,80.3708,10
282 +57,2019,3,20.1493,16.8119,7
283 +57,2020,3,19.7343,15.2607,4
284 +57,2021,3,20.1544,16.6808,5
285 +57,2022,3,6.5085,63.5724,8
286 +57,2023,3,12.5311,26.0515,5
287 +58,2019,4,13.253,31.1957,7
288 +58,2020,4,7.3985,44.0591,7
289 +58,2021,4,11.6453,46.1704,11
290 +58,2022,4,19.5252,25.1611,7
291 +58,2023,4,11.9203,39.7328,6
292 +59,2019,2,7.5871,30.7634,9
293 +59,2020,2,5.6229,55.8274,5
294 +59,2021,2,25.6447,10.8964,3
295 +59,2022,2,12.4524,32.8172,7
296 +59,2023,2,17.2239,37.2817,5
297 +60,2019,2,19.4391,30.3346,6
298 +60,2020,2,17.8817,31.0461,14
299 +60,2021,2,13.3978,37.2755,10
300 +60,2022,2,7.3424,51.323,11
301 +60,2023,2,16.3985,35.5379,15
302 +61,2019,1,18.9617,31.8612,6
303 +61,2020,1,17.5619,37.2376,9
304 +61,2021,1,23.2379,17.1737,14
305 +61,2022,1,11.4086,42.2783,10
306 +61,2023,1,19.0527,27.4505,7
307 +62,2019,1,7.4459,33.0857,6
308 +62,2020,1,15.843,13.512,4
309 +62,2021,1,24.0318,12.4345,9
310 +62,2022,1,25.0252,7.6888,9
311 +62,2023,1,13.7501,26.0732,5
312 +63,2019,3,20.2775,15.4599,12
313 +63,2020,3,9.0707,36.9883,11
314 +63,2021,3,20.5413,25.0972,4
315 +63,2022,3,7.9302,42.6822,13
316 +63,2023,3,23.7353,15.118,6
317 +64,2019,1,23.5579,15.1071,14
318 +64,2020,1,18.0757,29.062,4
319 +64,2021,1,7.0514,75.1112,8
320 +64,2022,1,26.1833,12.771,9
321 +64,2023,1,12.9137,43.8833,7
322 +65,2019,4,17.7362,17.6531,8
323 +65,2020,4,22.0763,26.0053,5
324 +65,2021,4,22.9688,20.6137,6
325 +65,2022,4,23.7903,25.2365,5
326 +65,2023,4,23.0056,18.7999,7
327 +66,2019,4,15.5097,32.0628,7
328 +66,2020,4,20.6703,28.2659,7
329 +66,2021,4,24.8611,19.4386,9
330 +66,2022,4,23.288,24.3606,6
331 +66,2023,4,14.4466,51.3954,6
332 +67,2019,3,17.7411,27.6427,8
333 +67,2020,3,15.5607,36.9302,7
334 +67,2021,3,6.3286,73.3364,11
335 +67,2022,3,8.0174,61.7355,12
336 +67,2023,3,10.8328,38.6415,18
337 +68,2019,1,22.2581,10.4369,6
338 +68,2020,1,25.2087,13.4873,13
339 +68,2021,1,24.4331,17.567,7
340 +68,2022,1,25.6525,8.7555,3
341 +68,2023,1,12.2731,25.2594,4
342 +69,2019,4,14.1278,18.8684,4
343 +69,2020,4,22.4529,17.7283,4
344 +69,2021,4,5.9198,50.6942,7
345 +69,2022,4,25.0471,12.1224,13
346 +69,2023,4,11.0748,40.7675,7
347 +70,2019,1,21.8577,11.0033,6
348 +70,2020,1,6.7221,49.077,5
349 +70,2021,1,11.1831,33.9452,13
350 +70,2022,1,7.133,44.4716,6
351 +70,2023,1,15.1896,31.2791,10
352 +71,2019,3,21.5794,18.4243,4
353 +71,2020,3,11.2275,22.5454,6
354 +71,2021,3,16.7944,27.198,4
355 +71,2022,3,6.3993,61.8816,6
356 +71,2023,3,25.0968,16.9554,11
357 +72,2019,3,5.3621,42.0261,6
358 +72,2020,3,7.2505,44.8128,9
359 +72,2021,3,25.3821,12.4979,3
360 +72,2022,3,9.3541,32.9704,6
361 +72,2023,3,11.8472,54.4945,7
362 +73,2019,1,19.7926,15.867,6
363 +73,2020,1,18.9341,21.1625,11
364 +73,2021,1,23.3254,23.7415,5
365 +73,2022,1,8.5271,40.0303,10
366 +73,2023,1,15.222,41.8308,9
367 +74,2019,1,22.4641,11.2364,9
368 +74,2020,1,8.7476,30.6515,11
369 +74,2021,1,9.1204,22.9134,10
370 +74,2022,1,25.5623,9.7274,11
371 +74,2023,1,14.1473,28.448,8
372 +75,2019,3,24.0789,14.7972,5
373 +75,2020,3,20.6201,27.3527,11
374 +75,2021,3,22.8963,19.5118,15
375 +75,2022,3,24.5714,17.9612,9
376 +75,2023,3,22.4468,21.5829,6
377 +76,2019,1,10.3996,36.4523,8
378 +76,2020,1,7.7224,55.0387,6
379 +76,2021,1,21.1102,20.7814,8
380 +76,2022,1,6.6247,59.7236,8
381 +76,2023,1,7.6717,67.5219,3
382 +77,2019,3,24.6634,7.9323,11
383 +77,2020,3,15.328,17.2727,7
384 +77,2021,3,18.1523,21.8578,7
385 +77,2022,3,8.48,28.2438,5
386 +77,2023,3,10.6957,32.4403,4
387 +78,2019,4,7.6353,66.0731,12
388 +78,2020,4,24.1258,30.9646,4
389 +78,2021,4,19.3475,41.6469,12
390 +78,2022,4,11.5867,57.4366,4
391 +78,2023,4,13.7609,55.7488,9
392 +79,2019,1,11.4569,25.8137,6
393 +79,2020,1,17.2349,32.5075,6
394 +79,2021,1,19.227,34.5306,7
395 +79,2022,1,11.3959,89.556,10
396 +79,2023,1,8.2272,73.7338,16
397 +80,2019,2,11.1858,38.7953,8
398 +80,2020,2,11.5533,35.6566,12
399 +80,2021,2,19.0946,16.9234,9
400 +80,2022,2,9.0887,46.8744,3
401 +80,2023,2,12.5711,43.0239,8
402 +81,2019,4,22.5985,16.5098,5
403 +81,2020,4,16.79,28.482,10
404 +81,2021,4,18.3888,23.0889,4
405 +81,2022,4,14.725,28.7159,5
406 +81,2023,4,14.3302,41.5614,6
407 +82,2019,1,24.8652,13.3914,13
408 +82,2020,1,23.5369,23.2579,8
409 +82,2021,1,14.7017,40.1017,14
410 +82,2022,1,15.8373,22.4824,12
411 +82,2023,1,14.2202,40.334,12
412 +83,2019,3,13.2993,46.7931,3
413 +83,2020,3,21.4235,14.2066,7
414 +83,2021,3,16.8723,41.5895,8
415 +83,2022,3,16.7518,43.9622,12
416 +83,2023,3,11.7893,62.1655,8
417 +84,2019,3,18.0207,32.0997,10
418 +84,2020,3,15.8551,35.6036,12
419 +84,2021,3,21.5021,20.66,10
420 +84,2022,3,22.6824,26.8654,6
421 +84,2023,3,9.7822,67.6578,4
422 +85,2019,4,16.6138,46.7029,6
423 +85,2020,4,6.5564,71.3887,4
424 +85,2021,4,24.7601,17.9062,4
425 +85,2022,4,19.6231,36.6426,8
426 +85,2023,4,12.1294,62.9141,8
427 +86,2019,1,18.1839,20.3491,5
428 +86,2020,1,12.9765,37.3367,9
429 +86,2021,1,19.8998,19.4261,6
430 +86,2022,1,24.386,14.7418,11
431 +86,2023,1,18.2709,22.8055,5
432 +87,2019,4,7.2764,81.826,11
433 +87,2020,4,16.2417,40.7633,6
434 +87,2021,4,7.3887,114.897,8
435 +87,2022,4,11.3829,88.0823,5
436 +87,2023,4,21.407,39.3243,7
437 +88,2019,4,20.6402,38.0492,16
438 +88,2020,4,12.2264,57.5365,7
439 +88,2021,4,14.4066,53.953,5
440 +88,2022,4,9.4746,64.417,7
441 +88,2023,4,16.4231,59.846,13
442 +89,2019,2,8.5381,47.424,10
443 +89,2020,2,9.0041,42.8785,7
444 +89,2021,2,21.5205,24.6228,19
445 +89,2022,2,10.2595,40.5864,8
446 +89,2023,2,11.5076,37.0441,10
447 +90,2019,1,17.496,20.8696,4
448 +90,2020,1,15.0678,24.3483,10
449 +90,2021,1,12.544,30.8284,7
450 +90,2022,1,10.5209,34.6935,7
451 +90,2023,1,22.537,14.3061,12
452 +91,2019,4,18.3604,26.0246,9
453 +91,2020,4,21.6307,23.6426,12
454 +91,2021,4,10.8569,55.9923,10
455 +91,2022,4,7.5513,94.4222,6
456 +91,2023,4,20.0724,32.8507,5
457 +92,2019,1,8.1708,38.0751,8
458 +92,2020,1,11.2366,31.8057,5
459 +92,2021,1,5.8331,40.3623,8
460 +92,2022,1,25.5668,8.7614,8
461 +92,2023,1,26.4234,10.9403,4
462 +93,2019,3,18.1237,42.6615,9
463 +93,2020,3,14.3976,45.4623,7
464 +93,2021,3,6.4224,107.0666,22
465 +93,2022,3,15.3436,83.5639,15
466 +93,2023,3,24.9085,30.174,9
467 +94,2019,4,10.2265,45.278,13
468 +94,2020,4,14.8053,56.0205,11
469 +94,2021,4,11.1013,54.7658,7
470 +94,2022,4,18.0019,25.1339,7
471 +94,2023,4,9.4298,52.2677,6
472 +95,2019,3,23.3506,19.9534,8
473 +95,2020,3,20.4062,21.1477,6
474 +95,2021,3,18.9513,18.9059,7
475 +95,2022,3,24.4656,9.4518,10
476 +95,2023,3,26.4957,8.738,10
477 +96,2019,2,14.0326,35.1436,10
478 +96,2020,2,11.3866,43.8979,9
479 +96,2021,2,16.0151,41.52,17
480 +96,2022,2,18.9547,48.1457,7
481 +96,2023,2,20.4859,32.6007,4
482 +97,2019,4,18.6533,19.2461,5
483 +97,2020,4,23.2091,13.5723,4
484 +97,2021,4,21.5158,15.9491,6
485 +97,2022,4,13.7948,33.8443,7
486 +97,2023,4,22.5418,16.2835,11
487 +98,2019,4,21.0055,19.2752,6
488 +98,2020,4,19.5679,14.9849,7
489 +98,2021,4,20.3938,19.1346,9
490 +98,2022,4,8.3414,33.7495,4
491 +98,2023,4,23.6078,14.1014,9
492 +99,2019,2,11.7934,19.7129,6
493 +99,2020,2,21.022,11.2876,8
494 +99,2021,2,16.3663,17.6393,6
495 +99,2022,2,16.6815,19.1286,5
496 +99,2023,2,12.8111,28.4097,9
497 +100,2019,1,6.5443,44.1292,6
498 +100,2020,1,17.9266,23.6334,9
499 +100,2021,1,12.7866,25.9941,7
500 +100,2022,1,10.4578,45.7536,5
501 +100,2023,1,25.5083,15.2897,3
502 +101,2019,4,21.3565,13.8505,6
503 +101,2020,4,9.4734,34.9593,7
504 +101,2021,4,17.4441,37.3167,6
505 +101,2022,4,15.7222,33.6301,5
506 +101,2023,4,20.762,22.5101,4
507 +102,2019,3,9.5817,52.2891,8
508 +102,2020,3,13.6455,30.2011,8
509 +102,2021,3,20.2431,24.7267,13
510 +102,2022,3,11.1537,59.5064,8
511 +102,2023,3,16.1643,34.4965,14
512 +103,2019,3,23.5567,13.3055,7
513 +103,2020,3,17.5788,20.7921,6
514 +103,2021,3,9.6841,58.9606,8
515 +103,2022,3,26.043,16.3515,7
516 +103,2023,3,10.8349,33.5438,7
517 +104,2019,1,16.9383,10.1788,3
518 +104,2020,1,10.3325,13.015,5
519 +104,2021,1,15.317,14.5075,3
520 +104,2022,1,25.5144,7.8196,5
521 +104,2023,1,7.7578,25.6394,8
522 +105,2019,1,14.1658,18.1043,2
523 +105,2020,1,8.4708,24.9161,5
524 +105,2021,1,23.1364,11.9772,6
525 +105,2022,1,17.5384,13.0202,4
526 +105,2023,1,9.6417,46.3364,4
527 +106,2019,4,19.2942,36.5618,20
528 +106,2020,4,12.1148,42.7339,13
529 +106,2021,4,17.3801,65.2994,6
530 +106,2022,4,14.9355,70.9669,10
531 +106,2023,4,18.7781,30.7034,8
532 +107,2019,2,19.8957,15.467,5
533 +107,2020,2,19.2558,14.6017,8
534 +107,2021,2,13.4154,37.0877,9
535 +107,2022,2,8.0546,63.6268,5
536 +107,2023,2,21.7392,18.0726,17
537 +108,2019,1,8.3415,37.9985,4
538 +108,2020,1,16.2979,25.1786,9
539 +108,2021,1,13.7546,21.5529,7
540 +108,2022,1,17.6305,20.8321,6
541 +108,2023,1,14.9493,22.8048,6
542 +109,2019,1,24.661,10.1442,13
543 +109,2020,1,18.1329,20.4569,12
544 +109,2021,1,10.7825,31.664,7
545 +109,2022,1,20.5007,21.2353,11
546 +109,2023,1,23.3058,14.2404,7
547 +110,2019,3,20.2128,27.5967,14
548 +110,2020,3,22.6751,33.0881,9
549 +110,2021,3,22.179,38.791,18
550 +110,2022,3,25.2456,20.366,5
551 +110,2023,3,14.0656,48.2008,16
552 +111,2019,4,17.5415,21.9937,7
553 +111,2020,4,9.4369,41.0045,10
554 +111,2021,4,17.7802,20.8391,13
555 +111,2022,4,8.7179,62.5143,10
556 +111,2023,4,17.8185,18.5746,15
557 +112,2019,3,19.785,19.9574,11
558 +112,2020,3,20.6553,24.5673,4
559 +112,2021,3,10.8011,57.4985,7
560 +112,2022,3,20.785,23.1286,13
561 +112,2023,3,12.3375,67.7582,7
562 +113,2019,1,12.7808,21.2401,8
563 +113,2020,1,22.5534,9.5764,5
564 +113,2021,1,22.0438,10.7072,8
565 +113,2022,1,25.8163,8.8187,7
566 +113,2023,1,21.0757,16.728,7
567 +114,2019,4,18.5793,19.1358,9
568 +114,2020,4,20.5615,21.6451,13
569 +114,2021,4,12.1599,31.6034,6
570 +114,2022,4,26.1512,16.1313,4
571 +114,2023,4,20.3611,26.252,11
572 +115,2019,1,17.1828,23.2065,4
573 +115,2020,1,16.7805,19.0515,8
574 +115,2021,1,11.8743,33.8173,10
575 +115,2022,1,19.4488,12.5854,8
576 +115,2023,1,19.8713,19.4491,15
577 +116,2019,1,7.1383,50.8571,8
578 +116,2020,1,13.3057,45.6311,13
579 +116,2021,1,15.4448,23.5715,15
580 +116,2022,1,10.6109,54.2955,8
581 +116,2023,1,15.2423,32.624,10
582 +117,2019,4,12.0654,49.5846,6
583 +117,2020,4,16.8356,36.8939,13
584 +117,2021,4,16.6777,41.3707,11
585 +117,2022,4,7.3602,70.4167,4
586 +117,2023,4,23.2923,29.0942,9
587 +118,2019,1,12.5323,41.4082,11
588 +118,2020,1,18.189,25.8596,12
589 +118,2021,1,15.6856,31.3334,9
590 +118,2022,1,10.2984,78.1112,6
591 +118,2023,1,16.4288,28.2861,8
592 +119,2019,1,13.7632,33.2103,18
593 +119,2020,1,17.6966,31.4438,6
594 +119,2021,1,7.0609,63.9712,6
595 +119,2022,1,8.6021,75.7238,7
596 +119,2023,1,14.9974,42.8621,3
597 +120,2019,1,13.2627,58.297,6
598 +120,2020,1,7.2968,97.3465,5
599 +120,2021,1,22.3563,21.8127,16
600 +120,2022,1,7.845,84.3395,15
601 +120,2023,1,16.5857,55.4373,7
602 +121,2019,2,21.6402,23.2099,9
603 +121,2020,2,11.7227,24.0362,6
604 +121,2021,2,14.3862,25.4407,8
605 +121,2022,2,9.1155,57.4592,14
606 +121,2023,2,16.6262,35.6942,3
607 +122,2019,4,20.866,9.3432,4
608 +122,2020,4,19.3108,8.7758,6
609 +122,2021,4,20.5489,9.6329,2
610 +122,2022,4,12.6835,28.6155,5
611 +122,2023,4,11.1733,15.9013,7
612 +123,2019,3,13.3296,26.5956,6
613 +123,2020,3,17.4213,25.0398,7
614 +123,2021,3,11.4695,34.7626,7
615 +123,2022,3,14.167,30.8165,7
616 +123,2023,3,13.8384,46.1922,9
617 +124,2019,2,5.7041,37.0234,6
618 +124,2020,2,8.3388,36.6884,3
619 +124,2021,2,24.2021,11.0387,12
620 +124,2022,2,21.8953,16.5392,5
621 +124,2023,2,12.0551,28.9183,5
622 +125,2019,1,19.6424,16.5306,6
623 +125,2020,1,11.6323,27.777,8
624 +125,2021,1,10.12,53.4615,7
625 +125,2022,1,10.3405,48.3498,8
626 +125,2023,1,25.8519,10.765,6
627 +126,2019,3,11.2557,48.7813,9
628 +126,2020,3,13.4264,37.0155,10
629 +126,2021,3,25.1431,18.3529,12
630 +126,2022,3,13.8334,35.6393,7
631 +126,2023,3,15.1012,57.8348,10
632 +127,2019,1,6.3379,29.7579,8
633 +127,2020,1,23.2158,11.3488,7
634 +127,2021,1,16.0729,28.7584,6
635 +127,2022,1,17.9467,21.2021,10
636 +127,2023,1,10.2227,25.9027,8
637 +128,2019,1,9.9508,22.0466,2
638 +128,2020,1,18.147,10.6699,6
639 +128,2021,1,21.2292,10.268,6
640 +128,2022,1,17.7493,23.6356,7
641 +128,2023,1,6.632,44.0715,8
642 +129,2019,3,21.1001,17.0258,9
643 +129,2020,3,23.6421,22.2899,11
644 +129,2021,3,10.8021,59.0901,5
645 +129,2022,3,10.4699,44.0662,4
646 +129,2023,3,7.564,68.6313,8
647 +130,2019,2,7.8144,54.8645,7
648 +130,2020,2,18.435,28.7225,11
649 +130,2021,2,13.9968,45.1696,14
650 +130,2022,2,19.6105,30.4114,8
651 +130,2023,2,13.4378,44.639,4
652 +131,2019,1,23.0085,21.01,12
653 +131,2020,1,14.2961,23.6211,8
654 +131,2021,1,14.5037,17.556,7
655 +131,2022,1,24.3092,10.8911,9
656 +131,2023,1,20.4262,16.7975,7
657 +132,2019,2,8.4924,55.8312,20
658 +132,2020,2,18.9459,17.8376,6
659 +132,2021,2,8.768,38.7763,8
660 +132,2022,2,12.5387,38.8208,5
661 +132,2023,2,21.3592,26.3546,4
662 +133,2019,4,6.1956,83.5927,11
663 +133,2020,4,25.0544,18.8985,13
664 +133,2021,4,10.7081,48.5196,11
665 +133,2022,4,7.3986,119.2048,5
666 +133,2023,4,17.7798,42.7937,9
667 +134,2019,2,17.2723,25.5307,5
668 +134,2020,2,16.4773,31.2628,9
669 +134,2021,2,25.6921,13.22,3
670 +134,2022,2,7.8996,58.6936,13
671 +134,2023,2,12.6476,46.4825,17
672 +135,2019,1,14.2582,17.9212,7
673 +135,2020,1,20.0285,13.8057,12
674 +135,2021,1,8.4348,38.5346,5
675 +135,2022,1,15.1191,16.5641,6
676 +135,2023,1,22.0362,18.4647,9
677 +136,2019,4,20.2973,27.74,8
678 +136,2020,4,16.0192,36.3594,5
679 +136,2021,4,9.2987,58.9829,8
680 +136,2022,4,12.3713,79.9009,7
681 +136,2023,4,12.7741,55.3816,7
682 +137,2019,2,19.5929,9.7444,7
683 +137,2020,2,16.6058,11.096,10
684 +137,2021,2,9.6483,17.8112,6
685 +137,2022,2,12.2726,21.0378,6
686 +137,2023,2,24.1645,8.4471,7
687 +138,2019,4,18.0085,33.7972,8
688 +138,2020,4,9.8146,53.0689,20
689 +138,2021,4,17.3822,45.8399,9
690 +138,2022,4,6.9072,91.4842,9
691 +138,2023,4,22.2819,32.0537,16
692 +139,2019,1,8.589,45.0934,3
693 +139,2020,1,22.298,21.2125,9
694 +139,2021,1,13.015,39.2106,7
695 +139,2022,1,14.8798,27.8796,6
696 +139,2023,1,15.1986,34.2766,7
697 +140,2019,2,24.2575,25.6412,27
698 +140,2020,2,9.6411,106.0409,12
699 +140,2021,2,14.0867,44.1822,9
700 +140,2022,2,13.1447,67.8952,6
701 +140,2023,2,13.8272,55.0177,23
702 +141,2019,4,17.157,21.9251,5
703 +141,2020,4,6.6111,62.2076,9
704 +141,2021,4,17.837,26.8103,11
705 +141,2022,4,19.1909,31.6905,10
706 +141,2023,4,17.4792,40.21,6
707 +142,2019,1,20.3377,16.9369,5
708 +142,2020,1,9.0011,38.5469,5
709 +142,2021,1,8.0194,49.8737,9
710 +142,2022,1,17.8862,20.7945,8
711 +142,2023,1,24.4399,8.8737,7
712 +143,2019,3,23.3948,29.5583,5
713 +143,2020,3,17.2081,37.3813,5
714 +143,2021,3,9.9884,77.2443,14
715 +143,2022,3,8.1582,88.7393,9
716 +143,2023,3,26.0508,23.2629,9
717 +144,2019,3,6.0604,78.9316,12
718 +144,2020,3,21.5333,19.0197,10
719 +144,2021,3,23.6872,24.4969,6
720 +144,2022,3,8.2626,88.7392,5
721 +144,2023,3,20.4554,41.8587,4
722 +145,2019,1,8.3876,33.6834,5
723 +145,2020,1,16.8109,20.3516,8
724 +145,2021,1,13.4161,35.6274,8
725 +145,2022,1,10.76,32.6872,5
726 +145,2023,1,16.5824,28.4128,13
727 +146,2019,1,20.8477,14.6849,9
728 +146,2020,1,9.0121,32.3311,19
729 +146,2021,1,9.8401,37.2056,10
730 +146,2022,1,11.3592,33.7181,7
731 +146,2023,1,21.9407,16.029,11
732 +147,2019,1,17.9331,17.6181,9
733 +147,2020,1,8.8045,45.4022,9
734 +147,2021,1,17.9898,31.1383,13
735 +147,2022,1,15.8874,24.5479,14
736 +147,2023,1,11.1042,30.7907,8
737 +148,2019,1,16.6143,12.6225,6
738 +148,2020,1,11.7615,23.2233,8
739 +148,2021,1,19.1173,14.0679,5
740 +148,2022,1,9.8074,22.7848,12
741 +148,2023,1,23.7231,11.7476,9
742 +149,2019,4,5.0923,95.047,6
743 +149,2020,4,9.1922,81.4536,8
744 +149,2021,4,11.3287,67.7191,9
745 +149,2022,4,15.1028,41.7178,14
746 +149,2023,4,11.8284,97.3957,13
747 +150,2019,1,20.6219,17.604,12
748 +150,2020,1,14.5565,21.7018,13
749 +150,2021,1,10.738,40.3274,8
750 +150,2022,1,9.4694,40.8107,7
751 +150,2023,1,14.2665,20.8947,6
752 +151,2019,1,18.6609,19.5491,7
753 +151,2020,1,15.8767,16.8895,13
754 +151,2021,1,7.3783,37.6058,17
755 +151,2022,1,23.6621,14.9125,12
756 +151,2023,1,25.5711,16.2627,10
757 +152,2019,4,8.5964,55.2207,19
758 +152,2020,4,7.9933,73.9973,8
759 +152,2021,4,11.9811,71.2892,5
760 +152,2022,4,19.5894,34.9085,3
761 +152,2023,4,8.8975,53.7339,7
762 +153,2019,3,24.4265,16.3882,6
763 +153,2020,3,16.139,28.9225,6
764 +153,2021,3,10.6345,47.6038,6
765 +153,2022,3,21.3008,19.4694,6
766 +153,2023,3,13.7009,44.9428,4
767 +154,2019,1,7.9102,38.4471,12
768 +154,2020,1,10.3745,33.1936,6
769 +154,2021,1,10.8057,32.9823,7
770 +154,2022,1,17.0081,36.2498,5
771 +154,2023,1,23.1314,18.4619,3
772 +155,2019,2,19.1993,19.1254,10
773 +155,2020,2,24.5342,20.5041,12
774 +155,2021,2,7.0795,93.3208,7
775 +155,2022,2,16.6908,37.9063,17
776 +155,2023,2,9.6699,62.5666,11
777 +156,2019,4,10.4016,32.6535,3
778 +156,2020,4,7.0432,54.4686,11
779 +156,2021,4,21.3951,17.5711,16
780 +156,2022,4,19.5523,23.3129,5
781 +156,2023,4,13.1959,37.3068,5
782 +157,2019,4,17.8839,24.7735,7
783 +157,2020,4,9.1251,44.5961,8
784 +157,2021,4,10.5802,40.1963,6
785 +157,2022,4,24.7794,24.867,7
786 +157,2023,4,9.6823,66.1214,6
787 +158,2019,4,5.5177,104.698,12
788 +158,2020,4,6.4085,103.4955,11
789 +158,2021,4,17.6914,50.2833,10
790 +158,2022,4,21.2521,56.945,10
791 +158,2023,4,17.6525,49.5179,9
792 +159,2019,1,9.5213,24.5363,8
793 +159,2020,1,20.7381,15.0378,12
794 +159,2021,1,21.0615,17.0428,11
795 +159,2022,1,12.7907,29.3194,4
796 +159,2023,1,26.1396,10.8546,10
797 +160,2019,3,23.9153,15.995,4
798 +160,2020,3,23.6174,19.5928,6
799 +160,2021,3,25.5465,13.4146,10
800 +160,2022,3,10.2829,58.9481,7
801 +160,2023,3,16.4219,20.9642,10
802 +161,2019,2,22.4507,14.3835,7
803 +161,2020,2,23.1817,21.1218,8
804 +161,2021,2,10.8238,37.5395,3
805 +161,2022,2,13.9791,44.6922,8
806 +161,2023,2,18.8689,23.1965,9
807 +162,2019,2,12.2144,49.127,14
808 +162,2020,2,16.7014,27.353,22
809 +162,2021,2,10.8437,46.4488,8
810 +162,2022,2,20.3336,27.1677,6
811 +162,2023,2,11.4524,38.6064,9
812 +163,2019,1,15.0715,18.9326,4
813 +163,2020,1,21.8371,12.5543,7
814 +163,2021,1,18.0928,21.8919,6
815 +163,2022,1,19.3383,13.3424,9
816 +163,2023,1,21.3268,19.4454,6
817 +164,2019,2,16.752,21.9143,8
818 +164,2020,2,5.7381,47.1439,13
819 +164,2021,2,21.0879,21.927,7
820 +164,2022,2,14.0476,33.3084,6
821 +164,2023,2,7.7844,60.9635,10
822 +165,2019,3,17.9551,32.1276,4
823 +165,2020,3,8.5584,46.6657,7
824 +165,2021,3,17.4431,18.754,3
825 +165,2022,3,12.1076,36.7165,6
826 +165,2023,3,17.6349,27.8064,17
827 +166,2019,2,11.0756,26.3579,12
828 +166,2020,2,24.5619,7.7738,11
829 +166,2021,2,20.9266,12.4788,11
830 +166,2022,2,15.1516,20.6202,9
831 +166,2023,2,24.8574,11.5498,4
832 +167,2019,1,15.7186,19.6586,12
833 +167,2020,1,19.7218,16.0048,8
834 +167,2021,1,14.8238,37.3891,5
835 +167,2022,1,17.8113,21.6009,12
836 +167,2023,1,26.4112,9.9517,8
837 +168,2019,2,17.2584,30.1185,10
838 +168,2020,2,10.737,62.2373,15
839 +168,2021,2,10.6572,95.0707,11
840 +168,2022,2,23.4696,31.7027,5
841 +168,2023,2,23.4173,22.9176,9
842 +169,2019,1,22.4486,17.8826,6
843 +169,2020,1,23.4324,15.1913,15
844 +169,2021,1,10.3887,36.9382,17
845 +169,2022,1,11.1062,47.2751,5
846 +169,2023,1,24.4417,31.2778,8
847 +170,2019,3,18.3178,25.0787,6
848 +170,2020,3,19.8455,13.5014,10
849 +170,2021,3,13.6373,31.3819,6
850 +170,2022,3,23.0627,17.7872,7
851 +170,2023,3,8.2349,56.0195,7
852 +171,2019,4,5.796,77.2976,4
853 +171,2020,4,11.7582,66.3972,6
854 +171,2021,4,10.1927,44.6179,6
855 +171,2022,4,24.0623,26.8725,5
856 +171,2023,4,13.5895,46.7825,7
857 +172,2019,2,5.128,44.0484,4
858 +172,2020,2,16.894,27.9061,11
859 +172,2021,2,17.7169,34.0714,7
860 +172,2022,2,15.7061,29.2684,7
861 +172,2023,2,26.2504,15.8539,9
862 +173,2019,2,13.8204,24.9788,6
863 +173,2020,2,10.0712,39.1159,9
864 +173,2021,2,20.309,14.482,12
865 +173,2022,2,12.0214,37.2277,6
866 +173,2023,2,22.992,23.5358,5
867 +174,2019,1,6.0816,75.0252,19
868 +174,2020,1,24.4441,16.0544,8
869 +174,2021,1,8.6726,53.7227,10
870 +174,2022,1,20.7392,17.8278,13
871 +174,2023,1,24.0258,12.9749,9
872 +175,2019,2,7.4523,60.3768,5
873 +175,2020,2,14.3777,29.5974,6
874 +175,2021,2,7.4999,59.9765,5
875 +175,2022,2,13.6036,45.9314,6
876 +175,2023,2,24.7781,25.998,17
877 +176,2019,2,14.5989,46.2644,7
878 +176,2020,2,12.3392,53.3481,7
879 +176,2021,2,10.1026,50.5675,9
880 +176,2022,2,18.9635,38.095,5
881 +176,2023,2,8.163,57.5857,7
882 +177,2019,3,18.1088,22.8679,8
883 +177,2020,3,13.0025,32.1245,5
884 +177,2021,3,20.7506,25.3423,5
885 +177,2022,3,17.9399,36.3474,3
886 +177,2023,3,13.3431,58.2491,4
887 +178,2019,4,18.4073,25.4907,8
888 +178,2020,4,12.8278,33.289,6
889 +178,2021,4,24.9307,13.6035,8
890 +178,2022,4,19.5856,22.2464,2
891 +178,2023,4,8.6954,55.7938,5
892 +179,2019,4,9.3301,53.5654,10
893 +179,2020,4,13.6731,29.1487,5
894 +179,2021,4,14.0055,27.2148,12
895 +179,2022,4,10.9643,30.0508,6
896 +179,2023,4,12.2043,32.9338,7
897 +180,2019,4,22.3181,12.0567,6
898 +180,2020,4,8.1216,35.3416,7
899 +180,2021,4,18.3566,21.3461,7
900 +180,2022,4,17.4897,19.2098,4
901 +180,2023,4,26.437,12.0462,7
902 +181,2019,4,18.4774,25.6966,5
903 +181,2020,4,15.7165,32.2318,12
904 +181,2021,4,16.5831,37.2739,14
905 +181,2022,4,7.5238,59.8429,10
906 +181,2023,4,19.2166,31.7211,5
907 +182,2019,1,9.9788,81.0787,7
908 +182,2020,1,14.6799,39.1507,4
909 +182,2021,1,8.5261,53.9023,14
910 +182,2022,1,14.8074,40.8958,8
911 +182,2023,1,17.272,41.9881,11
912 +183,2019,2,6.1625,31.92,5
913 +183,2020,2,25.1799,7.9973,4
914 +183,2021,2,23.5433,7.3873,4
915 +183,2022,2,8.6801,27.6411,6
916 +183,2023,2,25.1514,12.2199,5
917 +184,2019,1,8.796,37.6609,8
918 +184,2020,1,21.53,17.1787,6
919 +184,2021,1,17.7635,22.0696,7
920 +184,2022,1,20.6473,21.0688,15
921 +184,2023,1,14.9429,42.505,8
922 +185,2019,4,15.6601,41.4365,9
923 +185,2020,4,9.4996,44.7744,9
924 +185,2021,4,12.6644,54.2642,9
925 +185,2022,4,17.8552,34.203,12
926 +185,2023,4,18.1365,39.0706,10
927 +186,2019,3,20.015,17.2019,4
928 +186,2020,3,11.2017,48.9223,10
929 +186,2021,3,25.6174,14.5726,6
930 +186,2022,3,21.3823,22.9055,9
931 +186,2023,3,20.0942,21.7527,6
932 +187,2019,1,18.0913,23.9829,4
933 +187,2020,1,15.0189,18.601,5
934 +187,2021,1,15.8798,20.7015,5
935 +187,2022,1,16.7442,17.5319,6
936 +187,2023,1,14.5831,20.6955,6
937 +188,2019,2,6.3737,35.904,4
938 +188,2020,2,17.7719,21.5719,8
939 +188,2021,2,9.5635,42.89,8
940 +188,2022,2,21.5068,13.8241,6
941 +188,2023,2,26.0504,10.9876,3
942 +189,2019,4,23.4298,34.3331,6
943 +189,2020,4,10.4034,96.8613,11
944 +189,2021,4,7.4963,85.0713,8
945 +189,2022,4,20.5408,37.5218,13
946 +189,2023,4,12.0881,93.1643,7
947 +190,2019,2,14.9988,26.6161,7
948 +190,2020,2,24.5276,11.1471,4
949 +190,2021,2,11.3591,41.1784,11
950 +190,2022,2,14.8978,16.5933,7
951 +190,2023,2,14.4625,37.6344,7
952 +191,2019,3,23.9142,24.982,5
953 +191,2020,3,23.7334,26.8057,11
954 +191,2021,3,10.0448,74.927,5
955 +191,2022,3,14.5782,47.2089,28
956 +191,2023,3,24.2215,26.6729,11
957 +192,2019,3,20.9926,13.4189,4
958 +192,2020,3,9.3002,29.0714,6
959 +192,2021,3,22.3659,18.2463,7
960 +192,2022,3,10.0444,32.4654,7
961 +192,2023,3,9.3207,58.0666,7
962 +193,2019,4,17.3245,53.2245,9
963 +193,2020,4,14.3782,51.2605,7
964 +193,2021,4,13.6344,61.9347,10
965 +193,2022,4,22.3399,46.1584,9
966 +193,2023,4,9.2597,91.665,7
967 +194,2019,1,8.8861,47.6828,7
968 +194,2020,1,8.0868,54.0103,8
969 +194,2021,1,8.2545,64.0067,7
970 +194,2022,1,18.6194,23.1999,6
971 +194,2023,1,21.375,21.9023,12
972 +195,2019,1,7.7762,35.3601,8
973 +195,2020,1,10.7776,25.1827,9
974 +195,2021,1,22.7077,9.7305,9
975 +195,2022,1,25.6171,11.1467,12
976 +195,2023,1,24.6433,14.7998,10
977 +196,2019,4,5.0163,83.1748,5
978 +196,2020,4,7.6746,82.6295,5
979 +196,2021,4,18.1385,43.524,8
980 +196,2022,4,7.0012,118.9301,12
981 +196,2023,4,25.0894,19.8567,11
982 +197,2019,4,5.2815,91.3089,11
983 +197,2020,4,23.4844,17.7966,6
984 +197,2021,4,21.4466,25.6135,5
985 +197,2022,4,22.0334,14.8712,4
986 +197,2023,4,9.3772,74.044,12
987 +198,2019,4,23.2035,17.2201,14
988 +198,2020,4,22.3043,17.0998,5
989 +198,2021,4,10.7436,39.5193,5
990 +198,2022,4,14.5335,35.3105,17
991 +198,2023,4,10.4838,42.6604,5
992 +199,2019,4,6.8068,51.9515,5
993 +199,2020,4,18.3557,19.3203,5
994 +199,2021,4,9.6095,51.1629,4
995 +199,2022,4,9.2797,47.6852,6
996 +199,2023,4,16.9762,27.8802,15
997 +200,2019,4,20.5422,19.0303,6
998 +200,2020,4,24.963,19.9101,4
999 +200,2021,4,9.2598,47.0182,9
1000 +200,2022,4,19.1063,39.1627,3
1001 +200,2023,4,20.913,40.4527,5
modified MetrikaKit/Sources/ZQEngine/Session.swift +27 −0
@@ -181,6 +181,7 @@ public actor ZQSession {
181 181 ) async throws -> ZQResult {
182 182 switch command.verb {
183 183 case "use": return try await handleUse(command)
184 + case "sysuse": return try await handleSysuse(command)
184 185 case "save": return try await handleSave(command)
185 186 case "clear": return handleClear()
186 187 case "describe": return handleDescribe()
@@ -237,6 +238,32 @@ public actor ZQSession {
237 238 )
238 239 }
239 240
241 + /// `sysuse name` — load a bundled sample dataset; bare `sysuse` lists
242 + /// what ships with the app.
243 + private func handleSysuse(_ command: ZQCommand) async throws -> ZQResult {
244 + let samples = ["sales", "mtcars"]
245 + guard let name = command.argument else {
246 + return ZQResult(
247 + text: "Sample datasets: \(samples.joined(separator: ", ")) — load one with 'sysuse <name>'"
248 + )
249 + }
250 + let stem = name.hasSuffix(".csv") ? String(name.dropLast(4)) : name
251 + guard samples.contains(stem),
252 + let url = Bundle.module.url(
253 + forResource: stem, withExtension: "csv", subdirectory: "Samples"
254 + ) else {
255 + throw ZQEngineError(
256 + "sysuse: unknown sample '\(name)' — available: \(samples.joined(separator: ", "))"
257 + )
258 + }
259 + frame = try await store.load(contentsOf: url)
260 + lastEstimation = nil
261 + return ZQResult(
262 + text: "(\(frame.rowCount) observations, \(frame.columns.count) variables — sample dataset '\(stem)')",
263 + scalars: ["N": Double(frame.rowCount), "k": Double(frame.columns.count)]
264 + )
265 + }
266 +
240 267 private func handleSave(_ command: ZQCommand) async throws -> ZQResult {
241 268 guard let path = command.argument else {
242 269 throw ZQEngineError("save: file name required")
modified MetrikaKit/Sources/ZQParser/KnownVerbs.swift +2 −1
@@ -24,6 +24,7 @@ public struct ZQVerbTable: Sendable {
24 24 public static let builtin = ZQVerbTable(
25 25 verbs: [
26 26 "use": 3,
27 + "sysuse": 6,
27 28 "save": 4,
28 29 "import": 3,
29 30 "export": 3,
@@ -71,7 +72,7 @@ public struct ZQVerbTable: Sendable {
71 72 "test": 4,
72 73 "help": 4,
73 74 ],
74 fileVerbs: ["use", "save", "import", "export", "log"],
75 + fileVerbs: ["use", "sysuse", "save", "import", "export", "log"],
75 76 assignmentVerbs: ["generate", "replace", "egen"],
76 77 prefixVerbs: ["bayes", "bootstrap", "permute", "jackknife"],
77 78 compoundVerbs: ["graph", "ivregress"]
modified MetrikaKit/Tests/MetrikaKitTests/EngineTests.swift +17 −1
@@ -30,6 +30,22 @@ struct EngineTests {
30 30 #expect(result.scalars["N"] == 60)
31 31 }
32 32
33 + @Test("sysuse loads bundled samples and lists them")
34 + func sysuse() async throws {
35 + let fresh = try ZQSession(discoverUserCommands: false)
36 + let listing = try await fresh.execute("sysuse")
37 + #expect(listing.text.contains("sales"))
38 + let sales = try await fresh.execute("sysuse sales")
39 + #expect(sales.scalars["N"] == 1000)
40 + let reg = try await fresh.execute("reg revenue price")
41 + #expect(reg.scalars["N"] == 1000)
42 + let cars = try await fresh.execute("sysuse mtcars")
43 + #expect(cars.scalars["N"] == 32)
44 + await #expect(throws: ZQEngineError.self) {
45 + _ = try await fresh.execute("sysuse nosuchsample")
46 + }
47 + }
48 +
33 49 @Test("regress with listwise deletion matches R")
34 50 func regress() async throws {
35 51 let result = try await session.execute("reg log_rev price")
@@ -202,7 +218,7 @@ struct EngineTests {
202 218 func manualCoverage() {
203 219 let documented = Set(ZQCommandReference.all.map(\.verb))
204 220 for verb in [
205 "use", "save", "clear", "describe", "list", "count", "generate",
221 + "use", "sysuse", "save", "clear", "describe", "list", "count", "generate",
206 222 "replace", "drop", "keep", "summarize", "tabulate", "correlate",
207 223 "regress", "logit", "probit", "poisson", "ivregress", "xtreg",
208 224 "xtset", "lasso", "elasticnet", "boost", "bootstrap", "permute", "predict",
modified Tests/UITests/ConsoleSmokeTests.swift +33 −0
@@ -71,6 +71,39 @@ final class ConsoleSmokeTests: XCTestCase {
71 71 XCTAssertTrue(sidebar.waitForExistence(timeout: 5), "sidebar missing log_rev")
72 72 }
73 73
74 + func testMetalScatterPath() throws {
75 + continueAfterFailure = false
76 +
77 + let app = XCUIApplication()
78 + app.launchEnvironment["METRIKA_METAL_THRESHOLD"] = "3"
79 + app.launchEnvironment["METRIKA_UITEST_CSV"] = """
80 + revenue,price
81 + 120.5,10.2
82 + 98.3,12.1
83 + 143.2,9.8
84 + 110.0,11.5
85 + 155.9,8.9
86 + 101.2,12.8
87 + """
88 + app.launch()
89 + XCTAssertTrue(
90 + staticText(containing: "6 observations", in: app).waitForExistence(timeout: 10),
91 + "dataset did not load"
92 + )
93 +
94 + let input = app.textFields.firstMatch
95 + input.click()
96 + input.typeText("scatter revenue price\n")
97 +
98 + // 6 points > threshold 3 → the Metal pane takes over. The pane is
99 + // exposed through its accessibility label.
100 + let metalPane = app.descendants(matching: .any).matching(
101 + NSPredicate(format: "label CONTAINS %@ OR value CONTAINS %@",
102 + "Metal scatter", "Metal scatter")
103 + ).firstMatch
104 + XCTAssertTrue(metalPane.waitForExistence(timeout: 10), "Metal scatter pane missing")
105 + }
106 +
74 107 func testDataBrowserAndDoFile() throws {
75 108 continueAfterFailure = false
76 109
added docs/index.html +78 −0
@@ -0,0 +1,78 @@
1 +<!DOCTYPE html>
2 +<html lang="en">
3 +<head>
4 +<meta charset="utf-8">
5 +<meta name="viewport" content="width=device-width, initial-scale=1">
6 +<title>Metrika — Command Reference</title>
7 +<style>
8 +:root { color-scheme: light dark;
9 + --accent: #4f8cff; --muted: #7a8199; --border: #d9dce6;
10 + --code-bg: rgba(120, 130, 180, 0.12); }
11 +@media (prefers-color-scheme: dark) {
12 + :root { --border: #2c3145; } }
13 +* { box-sizing: border-box; }
14 +body { margin: 0; font: 15px/1.55 -apple-system, "Helvetica Neue", sans-serif;
15 + display: flex; min-height: 100vh; }
16 +nav { width: 230px; flex-shrink: 0; padding: 24px 18px; overflow-y: auto;
17 + position: sticky; top: 0; height: 100vh; border-right: 1px solid var(--border); }
18 +nav h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
19 + color: var(--muted); margin: 18px 0 6px; }
20 +nav ul { list-style: none; margin: 0; padding: 0; }
21 +nav a { text-decoration: none; color: inherit; }
22 +nav a:hover code { color: var(--accent); }
23 +main { flex: 1; max-width: 760px; padding: 32px 40px 80px; }
24 +h1 { font-size: 28px; margin: 0 0 4px; }
25 +.subtitle { color: var(--muted); margin-bottom: 28px; }
26 +h2.category { font-size: 13px; text-transform: uppercase; letter-spacing: .08em;
27 + color: var(--muted); border-bottom: 1px solid var(--border);
28 + padding-bottom: 6px; margin-top: 44px; }
29 +article { margin: 26px 0; }
30 +article h2 { font-size: 20px; margin: 0 0 2px; font-family: ui-monospace, monospace; }
31 +.abbr { font-size: 12px; color: var(--muted); margin-left: 8px; }
32 +pre { background: var(--code-bg); border-radius: 8px; padding: 10px 14px;
33 + overflow-x: auto; font: 13px ui-monospace, "SF Mono", monospace; }
34 +code { font: 13px ui-monospace, "SF Mono", monospace; }
35 +table { border-collapse: collapse; margin: 8px 0; }
36 +td { padding: 3px 14px 3px 0; vertical-align: top; font-size: 14px; }
37 +td:first-child { font-family: ui-monospace, monospace; white-space: nowrap; }
38 +.notes { color: var(--muted); font-size: 14px; }
39 +footer { color: var(--muted); font-size: 12px; margin-top: 56px;
40 + border-top: 1px solid var(--border); padding-top: 12px; }
41 +</style>
42 +</head>
43 +<body>
44 +<nav><h3>Data management</h3><ul><li><a href="#use"><code>use</code></a></li><li><a href="#sysuse"><code>sysuse</code></a></li><li><a href="#save"><code>save</code></a></li><li><a href="#clear"><code>clear</code></a></li><li><a href="#describe"><code>describe</code></a></li><li><a href="#list"><code>list</code></a></li><li><a href="#count"><code>count</code></a></li><li><a href="#generate"><code>generate</code></a></li><li><a href="#replace"><code>replace</code></a></li><li><a href="#drop"><code>drop</code></a></li><li><a href="#keep"><code>keep</code></a></li></ul><h3>Descriptive statistics</h3><ul><li><a href="#summarize"><code>summarize</code></a></li><li><a href="#tabulate"><code>tabulate</code></a></li><li><a href="#correlate"><code>correlate</code></a></li></ul><h3>Estimation</h3><ul><li><a href="#regress"><code>regress</code></a></li><li><a href="#logit"><code>logit</code></a></li><li><a href="#probit"><code>probit</code></a></li><li><a href="#poisson"><code>poisson</code></a></li><li><a href="#ivregress"><code>ivregress</code></a></li><li><a href="#xtreg"><code>xtreg</code></a></li></ul><h3>Machine learning</h3><ul><li><a href="#lasso"><code>lasso</code></a></li><li><a href="#elasticnet"><code>elasticnet</code></a></li><li><a href="#boost"><code>boost</code></a></li></ul><h3>Resampling &amp; simulation</h3><ul><li><a href="#bootstrap"><code>bootstrap</code></a></li><li><a href="#bayes"><code>bayes</code></a></li><li><a href="#permute"><code>permute</code></a></li><li><a href="#set"><code>set</code></a></li></ul><h3>Post-estimation</h3><ul><li><a href="#predict"><code>predict</code></a></li><li><a href="#margins"><code>margins</code></a></li></ul><h3>Graphics</h3><ul><li><a href="#scatter"><code>scatter</code></a></li><li><a href="#histogram"><code>histogram</code></a></li><li><a href="#kdensity"><code>kdensity</code></a></li><li><a href="#graph"><code>graph</code></a></li></ul><h3>Panel data</h3><ul><li><a href="#xtset"><code>xtset</code></a></li></ul><h3>Session &amp; scripting</h3><ul><li><a href="#display"><code>display</code></a></li><li><a href="#log"><code>log</code></a></li><li><a href="#help"><code>help</code></a></li></ul><h3>User commands</h3><ul><li><a href="#zscore"><code>zscore</code></a></li></ul></nav>
45 +<main>
46 +<h1>Metrika</h1>
47 +<p class="subtitle">Command reference — Stata-class syntax, GPU-accelerated by Apple Silicon.
48 +Every command follows one grammar:
49 +<code>command [varlist] [if] [in] [, options]</code></p>
50 +<h2 class="category">Data management</h2>
51 +<article id="use"><h2>use</h2><p>Load a dataset into memory. Format is inferred from the extension: .parquet, .csv, .tsv, .json, .arrow, and Stata .dta (formats 117–119).</p><pre>use filename</pre><pre>. use sales.parquet
52 +. use ~/data/panel.dta</pre><p class="notes">Loading goes through DuckDB for columnar formats and a native reader for .dta. The sandboxed app reads user-selected files and its own container.</p></article><article id="sysuse"><h2>sysuse</h2><p>Load a sample dataset shipped with Metrika. Bare `sysuse` lists what is available.</p><pre>sysuse [name]</pre><pre>. sysuse sales
53 +. sysuse mtcars</pre><p class="notes">sales: 200-firm × 5-year panel (revenue, price, region, employees). mtcars: the R classic, with a string model column.</p></article><article id="save"><h2>save</h2><p>Write the working dataset to disk (.parquet, .csv, or Stata .dta format 118).</p><pre>save filename [, replace]</pre><table><tr><td>replace</td><td>overwrite an existing file</td></tr></table><pre>. save results.dta, replace</pre></article><article id="clear"><h2>clear</h2><p>Drop the working dataset from memory.</p><pre>clear</pre></article><article id="describe"><h2>describe<span class="abbr">abbreviation: d</span></h2><p>List variables with their storage types and missing counts.</p><pre>describe</pre></article><article id="list"><h2>list<span class="abbr">abbreviation: l</span></h2><p>Print observations (first 50 shown).</p><pre>list [varlist] [if] [in]</pre><pre>. list revenue price in 1/10</pre></article><article id="count"><h2>count<span class="abbr">abbreviation: cou</span></h2><p>Count observations satisfying a condition.</p><pre>count [if]</pre><pre>. count if revenue &gt; 100 &amp; !missing(price)</pre></article><article id="generate"><h2>generate<span class="abbr">abbreviation: gen</span></h2><p>Create a new variable from an expression. Observations excluded by `if` become missing.</p><pre>generate newvar = expression [if] [in]</pre><pre>. gen log_rev = ln(revenue)
54 +. gen high = revenue &gt; 120 if !missing(revenue)</pre><p class="notes">Functions: ln, log, log10, exp, sqrt, abs, floor, ceil, round, int, min, max, missing. Missing values propagate through arithmetic; division by zero and ln of a nonpositive number yield missing.</p></article><article id="replace"><h2>replace</h2><p>Overwrite values of an existing variable; reports the number of real changes.</p><pre>replace varname = expression [if] [in]</pre><pre>. replace flag = 0 if missing(flag)</pre></article><article id="drop"><h2>drop</h2><p>Remove variables, or observations with `if`/`in`.</p><pre>drop varlist | drop if expression | drop in range</pre><pre>. drop temp1 temp2
55 +. drop if price &lt; 0</pre></article><article id="keep"><h2>keep</h2><p>Keep only the listed variables, or only observations satisfying a condition.</p><pre>keep varlist | keep if expression | keep in range</pre><pre>. keep in 1/1000</pre></article><h2 class="category">Descriptive statistics</h2>
56 +<article id="summarize"><h2>summarize<span class="abbr">abbreviation: su</span></h2><p>Means, standard deviations, and ranges; `detail` adds percentiles, skewness, and kurtosis (Stata definitions).</p><pre>summarize [varlist] [if] [in] [, detail]</pre><table><tr><td>detail</td><td>percentiles p1–p99, skewness, kurtosis</td></tr></table><pre>. summarize revenue price, detail</pre></article><article id="tabulate"><h2>tabulate<span class="abbr">abbreviation: tab</span></h2><p>One-way frequency table or two-way cross-tabulation with totals.</p><pre>tabulate varname [varname2] [if] [, missing]</pre><table><tr><td>missing</td><td>include missing values as a category</td></tr></table><pre>. tab region
57 +. tabulate region purchase</pre></article><article id="correlate"><h2>correlate<span class="abbr">abbreviation: cor</span></h2><p>Pearson correlation matrix with listwise deletion.</p><pre>correlate [varlist] [if]</pre><pre>. correlate revenue price orders</pre></article><h2 class="category">Estimation</h2>
58 +<article id="regress"><h2>regress<span class="abbr">abbreviation: reg</span></h2><p>Ordinary least squares via LAPACK QR. Factor variables (i.var) expand to indicators; c.a#c.b forms continuous interactions.</p><pre>regress depvar [indepvars] [if] [in] [, robust hc2 hc3 cluster(varname) noconstant level(#)]</pre><table><tr><td>robust</td><td>HC1 heteroskedasticity-consistent SE (Stata's robust)</td></tr><tr><td>hc2 / hc3</td><td>leverage-adjusted sandwich estimators</td></tr><tr><td>cluster(var)</td><td>cluster-robust SE, t on G−1 df</td></tr><tr><td>noconstant</td><td>suppress the intercept</td></tr><tr><td>level(#)</td><td>confidence level, default 95</td></tr></table><pre>. reg log_rev price, robust
59 +. reg log_rev price i.region, cluster(firm_id)
60 +. reg wage c.age#c.age education</pre><p class="notes">Results match R to 1e-10. Listwise deletion is reported explicitly.</p></article><article id="logit"><h2>logit</h2><p>Logistic regression by Fisher scoring; reports LR χ², McFadden pseudo-R², z statistics.</p><pre>logit depvar indepvars [if] [, robust cluster(varname) level(#)]</pre><pre>. logit purchase price, robust</pre></article><article id="probit"><h2>probit</h2><p>Probit regression (normal link), same options as logit.</p><pre>probit depvar indepvars [if] [, robust cluster(varname)]</pre><pre>. probit purchase price</pre></article><article id="poisson"><h2>poisson</h2><p>Poisson regression for counts (log link).</p><pre>poisson depvar indepvars [if] [, robust cluster(varname)]</pre><pre>. poisson orders price, robust</pre></article><article id="ivregress"><h2>ivregress</h2><p>Two-stage least squares. Endogenous regressors and their instruments go in the parenthesized group.</p><pre>ivregress 2sls depvar [exogvars] (endogvars = instruments) [, robust cluster(varname)]</pre><pre>. ivregress 2sls log_rev (price = z1 z2), robust</pre><p class="notes">Residuals come from the original regressors; inference follows Stata's `small` convention (t statistics on N−K df).</p></article><article id="xtreg"><h2>xtreg</h2><p>Panel fixed-effects (within) estimator. Declare the panel with xtset first.</p><pre>xtreg depvar indepvars, fe [cluster(panelvar)]</pre><table><tr><td>fe</td><td>fixed effects (required — the only estimator so far)</td></tr><tr><td>cluster(panelvar)</td><td>panel-clustered SE, t on G−1 df</td></tr></table><pre>. xtset firm_id
61 +. xtreg log_rev price, fe cluster(firm_id)</pre></article><h2 class="category">Machine learning</h2>
62 +<article id="lasso"><h2>lasso</h2><p>L1-penalized linear regression (coordinate descent, glmnet conventions). Selects variables by zeroing coefficients.</p><pre>lasso depvar indepvars, lambda(#)</pre><table><tr><td>lambda(#)</td><td>penalty strength; omit it to see lambda_max for your data</td></tr></table><pre>. lasso log_rev price z1 z2 orders, lambda(0.05)</pre></article><article id="elasticnet"><h2>elasticnet</h2><p>Elastic-net linear regression mixing L1 and L2 penalties.</p><pre>elasticnet depvar indepvars, lambda(#) [alpha(#)]</pre><table><tr><td>alpha(#)</td><td>1 = lasso, 0 = ridge; default 1</td></tr><tr><td>lambda(#)</td><td>penalty strength</td></tr></table><pre>. elasticnet log_rev price z1 z2, alpha(0.4) lambda(0.02)</pre><p class="notes">Matches R glmnet, including its gaussian y-standardization convention (ridge penalties scale with sd of the response).</p></article><article id="boost"><h2>boost</h2><p>Gradient-boosted regression trees (xgboost-style exact greedy, squared loss). Deterministic — no subsampling.</p><pre>boost depvar features, rounds(#) [eta(#) maxdepth(#) lambda(#)]</pre><table><tr><td>rounds(#)</td><td>number of trees</td></tr><tr><td>eta(#)</td><td>learning rate, default 0.3</td></tr><tr><td>maxdepth(#)</td><td>tree depth, default 6</td></tr><tr><td>lambda(#)</td><td>L2 regularization on leaf weights, default 1</td></tr></table><pre>. boost log_rev z1 z2 orders, rounds(100) eta(0.1) maxdepth(3)
63 +. predict yhat</pre><p class="notes">Matches R xgboost predictions on identical settings. Training R² is in-sample — expect it to be optimistic.</p></article><h2 class="category">Resampling &amp; simulation</h2>
64 +<article id="bootstrap"><h2>bootstrap</h2><p>Pairs bootstrap of a regression. Large replication counts run batched on the Apple GPU; results are identical either way for a given seed.</p><pre>bootstrap, reps(#) [seed(#)]: regress depvar indepvars</pre><table><tr><td>reps(#)</td><td>number of replications</td></tr><tr><td>seed(#)</td><td>Philox seed — full reproducibility across CPU and GPU</td></tr></table><pre>. bootstrap, reps(10000) seed(42): reg log_rev price</pre><p class="notes">Replicates are counter-addressable: any subset recomputes identically regardless of chunking or backend. The planner dispatches ≥500 reps to the GPU when available.</p></article><article id="bayes"><h2>bayes</h2><p>Bayesian linear regression by Gibbs sampling: posterior means, standard deviations, and 95% credible intervals.</p><pre>bayes [, mcmcsize(#) burnin(#) seed(#) normalprior(#)]: regress depvar indepvars</pre><table><tr><td>mcmcsize(#)</td><td>posterior draws after burn-in (default 10000)</td></tr><tr><td>burnin(#)</td><td>discarded warm-up iterations (default 2500)</td></tr><tr><td>seed(#)</td><td>Philox seed — chains are exactly reproducible</td></tr><tr><td>normalprior(#)</td><td>prior variance of the N(0, #) coefficient priors (default 10000)</td></tr></table><pre>. bayes, mcmcsize(20000) seed(42): reg log_rev price</pre><p class="notes">Priors: coefficients N(0, normalprior), variance InvGamma(0.01, 0.01) — Stata's bayes defaults. With diffuse priors the posterior reproduces OLS.</p></article><article id="permute"><h2>permute</h2><p>Permutation test: the response is permuted, the model refit, and empirical two-sided p-values reported per coefficient.</p><pre>permute, reps(#) [seed(#)]: regress depvar indepvars</pre><pre>. permute, reps(1000) seed(42): reg log_rev orders</pre></article><article id="set"><h2>set</h2><p>Set session parameters. `set seed` fixes the Philox key used by bootstrap and permute.</p><pre>set seed #</pre><pre>. set seed 42</pre></article><h2 class="category">Post-estimation</h2>
65 +<article id="predict"><h2>predict</h2><p>Generate predictions from the last estimation over all current observations.</p><pre>predict newvar [, xb residuals pr n]</pre><table><tr><td>xb</td><td>linear prediction (default after regress/ivregress)</td></tr><tr><td>residuals</td><td>response residuals</td></tr><tr><td>pr</td><td>predicted probability (default after logit/probit)</td></tr><tr><td>n</td><td>predicted mean count (default after poisson)</td></tr></table><pre>. reg log_rev price
66 +. predict yhat
67 +. predict e, residuals</pre></article><article id="margins"><h2>margins<span class="abbr">abbreviation: marg</span></h2><p>Average marginal effects with delta-method standard errors. After OLS/IV the effect is the coefficient; after logit/probit/poisson it averages dμ/dx over the estimation sample.</p><pre>margins, dydx(varlist) [level(#)]</pre><pre>. logit purchase price
68 +. margins, dydx(price)</pre><p class="notes">Continuous regressors only for now; factor and interaction terms are rejected with a message.</p></article><h2 class="category">Graphics</h2>
69 +<article id="scatter"><h2>scatter</h2><p>Scatter plot (also available as `graph scatter`). `by()` splits into colored series.</p><pre>scatter yvar xvar [if] [, by(varname)]</pre><pre>. scatter log_rev price, by(region)</pre></article><article id="histogram"><h2>histogram<span class="abbr">abbreviation: hist</span></h2><p>Frequency histogram of one variable (Sturges bins by default).</p><pre>histogram varname [if] [, bins(#)]</pre><table><tr><td>bins(#)</td><td>override the bin count</td></tr></table><pre>. histogram revenue, bins(12)</pre></article><article id="kdensity"><h2>kdensity<span class="abbr">abbreviation: kden</span></h2><p>Kernel density estimate (Epanechnikov kernel, Silverman bandwidth).</p><pre>kdensity varname [if]</pre><pre>. kdensity revenue</pre></article><article id="graph"><h2>graph<span class="abbr">abbreviation: gr</span></h2><p>General plotting front end: graph scatter, graph line, graph histogram.</p><pre>graph scatter|line yvar xvar [if] [, by(varname)]</pre><pre>. graph line gdp year</pre></article><h2 class="category">Panel data</h2>
70 +<article id="xtset"><h2>xtset</h2><p>Declare the panel structure (unit and, optionally, time variable) for xtreg.</p><pre>xtset panelvar [timevar]</pre><pre>. xtset firm_id year</pre></article><h2 class="category">Session &amp; scripting</h2>
71 +<article id="display"><h2>display<span class="abbr">abbreviation: di</span></h2><p>Evaluate and print an expression.</p><pre>display expression</pre><pre>. display 2 + 2 * 3
72 +. display ln(100)</pre></article><article id="log"><h2>log</h2><p>Record commands and output to a text file.</p><pre>log using filename | log close</pre><pre>. log using session.log</pre></article><article id="help"><h2>help</h2><p>Show this reference, or the entry for one command.</p><pre>help [command]</pre><pre>. help regress</pre></article><h2 class="category">User commands</h2>
73 +<article id="zscore"><h2>zscore</h2><p>Sample native plugin: generates z_varname, the standardized version of a variable.</p><pre>zscore varname</pre><pre>. zscore revenue</pre><p class="notes">Native plugins are Swift types conforming to ZQCommandPlugin, compiled into the app. Script commands are .zyq files in ~/Library/Application Support/Metrika/Commands/ — an optional leading `args name…` line names positional arguments, referenced as `name' or `1' in the body.</p></article>
74 +<footer>© 2026 Simon-Pierre Boucher. Generated from the in-app command
75 +reference by <code>metrika-cli docs</code>.</footer>
76 +</main>
77 +</body>
78 +</html>
\ No newline at end of file
modified project.yml +1 −1
@@ -49,7 +49,7 @@ targets:
49 49 settings:
50 50 base:
51 51 PRODUCT_BUNDLE_IDENTIFIER: ai.spboucher.metrika
52 MARKETING_VERSION: 0.1.0
52 + MARKETING_VERSION: 1.0.0
53 53 CURRENT_PROJECT_VERSION: 1
54 54 SWIFT_VERSION: "6.0"
55 55 SWIFT_STRICT_CONCURRENCY: complete
added scripts/make_samples.R +49 −0
@@ -0,0 +1,49 @@
1 +#!/usr/bin/env Rscript
2 +#
3 +# make_samples.R — Metrika
4 +#
5 +# Author: Simon-Pierre Boucher
6 +# Contact: contact@spboucher.ai
7 +# Copyright © 2026 Simon-Pierre Boucher. All rights reserved.
8 +#
9 +# Generates the sample datasets bundled with the app (loaded via the
10 +# `sysuse` command). Deterministic: fixed seed, values rounded so the
11 +# CSVs are bit-stable across regenerations.
12 +#
13 +
14 +out <- file.path(
15 + dirname(dirname(normalizePath(sub("--file=", "", grep("--file=", commandArgs(FALSE), value = TRUE))))),
16 + "MetrikaKit", "Sources", "ZQEngine", "Resources", "Samples"
17 +)
18 +dir.create(out, recursive = TRUE, showWarnings = FALSE)
19 +
20 +# sales — the CLAUDE.md flagship example: firm-year panel of revenue and
21 +# price with regional effects. 200 firms × 5 years.
22 +set.seed(2026)
23 +firms <- 200
24 +years <- 2019:2023
25 +n <- firms * length(years)
26 +firm_id <- rep(seq_len(firms), each = length(years))
27 +year <- rep(years, times = firms)
28 +region <- rep(sample(1:4, firms, replace = TRUE), each = length(years))
29 +firm_effect <- rep(round(rnorm(firms, 0, 0.3), 6), each = length(years))
30 +price <- round(runif(n, 5, 25) + 0.4 * (year - 2019), 4)
31 +log_revenue <- 4.2 - 0.07 * price + 0.12 * (region - 1) +
32 + 0.05 * (year - 2019) + firm_effect + round(rnorm(n, 0, 0.2), 6)
33 +sales <- data.frame(
34 + firm_id = firm_id,
35 + year = year,
36 + region = region,
37 + price = price,
38 + revenue = round(exp(log_revenue), 4),
39 + employees = pmax(1, round(exp(2 + 0.5 * firm_effect + rnorm(n, 0, 0.4))))
40 +)
41 +write.csv(sales, file.path(out, "sales.csv"), row.names = FALSE, quote = FALSE)
42 +
43 +# mtcars — the R classic (public dataset), with the car name as a string
44 +# column so string handling has a sample too.
45 +data(mtcars)
46 +cars <- cbind(model = rownames(mtcars), mtcars)
47 +write.csv(cars, file.path(out, "mtcars.csv"), row.names = FALSE)
48 +
49 +cat("wrote", file.path(out, "sales.csv"), "and mtcars.csv\n")
50