spb/metrika Public
Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift — no Electron, no Python runtime, no compromises.
Swift 92.4%
HTML 3.3%
R 3%
Shell 1.3%
1//2// BoostTests.swift3// Metrika4//5// Author: Simon-Pierre Boucher6// Contact: contact@spboucher.ai7// Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Foundation11import Testing12import ZQEngine13import ZQGPU14import ZQStats1516/// Gradient boosting vs R xgboost (exact greedy, no subsampling —17/// deterministic). xgboost computes in float32, so per-observation18/// prediction agreement is asserted at 1e-4 relative.19@Suite("Gradient boosting", .serialized)20struct BoostTests {21 let fixtures: Fixtures22 let session: ZQSession2324 init() async throws {25 self.fixtures = try Fixtures()26 self.session = try ZQSession(discoverUserCommands: false)27 _ = try await session.execute("use \(fixtures.datasetURL.path)")28 _ = try await session.execute("gen log_rev = ln(revenue)")29 }3031 @Test("matches xgboost predictions observation by observation")32 func xgboostParity() async throws {33 let result = try await session.execute(34 "boost log_rev z1 z2 orders, rounds(20) eta(0.3) maxdepth(3) lambda(1)"35 )36 #expect(result.scalars["N"] == 60)37 expectClose(38 try #require(result.scalars["rmse"]),39 fixtures["gbm_rmse"], rtol: 1e-4, "training RMSE"40 )4142 _ = try await session.execute("predict bhat")43 let frame = await session.frame44 let (predictions, _) = try frame.requireNumeric("bhat")45 for (row, key) in [(0, "gbm_pred_1"), (16, "gbm_pred_17"),46 (41, "gbm_pred_42"), (56, "gbm_pred_57")] {47 expectClose(48 predictions[row], fixtures[key], rtol: 1e-4,49 "prediction[\(row + 1)]"50 )51 }52 _ = try await session.execute("drop bhat")53 }5455 @Test("a single stump finds the obvious split")56 func stumpSplit() throws {57 // Step function: y = 0 for x < 5, y = 10 for x ≥ 5.58 let x = (0..<20).map(Double.init)59 let y = x.map { $0 < 5 ? 0.0 : 10.0 }60 let model = try ZQGradientBoosting.fit(61 y: y, features: [("x", x)],62 rounds: 1, learningRate: 1, maxDepth: 1, lambda: 063 )64 let tree = model.trees[0]65 #expect(tree.nodes[0].feature == 0)66 #expect(tree.nodes[0].split == 4.5)67 // Leaf values are exact means of the residuals under lambda 0.68 #expect(abs(model.predict([2]) - 0) < 1e-12)69 #expect(abs(model.predict([9]) - 10) < 1e-12)70 }7172 @Test("training loss decreases every round")73 func monotoneTrainingLoss() throws {74 var stream = PhiloxStream(seed: 5)75 let n = 15076 let x1 = (0..<n).map { _ in stream.nextUniform() * 10 }77 let x2 = (0..<n).map { _ in stream.nextUniform() * 10 }78 let y = (0..<n).map { i in79 Foundation.sin(x1[i]) + 0.3 * x2[i] + 0.2 * stream.nextNormal()80 }8182 var previous = Double.infinity83 for rounds in [1, 5, 20, 80] {84 let model = try ZQGradientBoosting.fit(85 y: y, features: [("x1", x1), ("x2", x2)],86 rounds: rounds, learningRate: 0.3, maxDepth: 387 )88 var rss = 0.089 for i in 0..<n {90 let predicted = model.predict([x1[i], x2[i]])91 rss += (y[i] - predicted) * (y[i] - predicted)92 }93 #expect(rss < previous, "rounds \(rounds): rss \(rss)")94 previous = rss95 }96 // Deep boosting on smooth data should fit very well in-sample.97 #expect(previous / Double(n) < 0.05)98 }99100 @Test("boost requires rounds() and margins refuses afterwards")101 func validation() async throws {102 await #expect(throws: ZQEngineError.self) {103 _ = try await session.execute("boost log_rev z1")104 }105 _ = try await session.execute("boost log_rev z1 z2, rounds(5)")106 await #expect(throws: ZQEngineError.self) {107 _ = try await session.execute("margins, dydx(z1)")108 }109 await #expect(throws: ZQEngineError.self) {110 _ = try await session.execute("predict p, pr")111 }112 }113}114