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>
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: "&") | |
| 38 | + .replacingOccurrences(of: "<", with: "<") | |
| 39 | + .replacingOccurrences(of: ">", with: ">") | |
| 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 & 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 & 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 > 100 & !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 > 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 < 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 & 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 & 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 | ||