SPB Git

spb/metrika Public

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

Swift 92.4% HTML 3.3% R 3% Shell 1.3%
4.2 KB · 116 lines swift
Raw Blame History
1//2//  PermuteTests.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Foundation11import Testing12import ZQEngine13import ZQGPU1415@Suite("Permutation tests", .serialized)16struct PermuteTests {17    let fixtures: Fixtures18    let session: ZQSession1920    init() async throws {21        self.fixtures = try Fixtures()22        self.session = try ZQSession(discoverUserCommands: false)23        _ = try await session.execute("use \(fixtures.datasetURL.path)")24        _ = try await session.execute("gen log_rev = ln(revenue)")25    }2627    @Test("permutation indices form a valid permutation")28    func validPermutation() {29        let generator = Philox4x32(seed: 42)30        for replicate in [0, 1, 99] {31            let order = ZQResampling.permutationIndices(32                replicate: replicate, sampleSize: 257, generator: generator33            )34            #expect(order.sorted() == Array(0..<257), "replicate \(replicate)")35        }36    }3738    @Test("permutations are replicate-addressable and seed-deterministic")39    func determinism() {40        let a = ZQResampling.permutationIndices(41            replicate: 5, sampleSize: 100, generator: Philox4x32(seed: 42)42        )43        let b = ZQResampling.permutationIndices(44            replicate: 5, sampleSize: 100, generator: Philox4x32(seed: 42)45        )46        let c = ZQResampling.permutationIndices(47            replicate: 5, sampleSize: 100, generator: Philox4x32(seed: 43)48        )49        let d = ZQResampling.permutationIndices(50            replicate: 6, sampleSize: 100, generator: Philox4x32(seed: 42)51        )52        #expect(a == b)53        #expect(a != c)54        #expect(a != d)55    }5657    @Test("permutation stream does not collide with the bootstrap stream")58    func streamSeparation() {59        let generator = Philox4x32(seed: 42)60        let bootstrap = ZQResampling.pairsBootstrapIndices(61            replicate: 0, sampleSize: 100, generator: generator62        )63        let permutation = ZQResampling.permutationIndices(64            replicate: 0, sampleSize: 100, generator: generator65        )66        // A collision would make the permutation a deterministic function67        // of the bootstrap draw; these must be unrelated streams.68        #expect(bootstrap != permutation)69    }7071    @Test("strong relationship gets an extreme empirical p-value")72    func strongRelationship() async throws {73        let result = try await session.execute(74            "permute, reps(200) seed(42): reg log_rev price"75        )76        // R² ≈ 0.88 on n=57 — no permutation should beat the observed |β|.77        #expect(result.scalars["p_price"] == 0)78        #expect(result.scalars["c_price"] == 0)79        #expect(result.scalars["reps"] == 200)80        #expect(result.text.contains("Permutation test"))81        // The degenerate constant row is omitted.82        #expect(result.scalars["p__cons"] == nil)83    }8485    @Test("permute is reproducible for a fixed seed")86    func reproducible() async throws {87        let first = try await session.execute(88            "permute, reps(50) seed(7): reg log_rev orders"89        )90        let second = try await session.execute(91            "permute, reps(50) seed(7): reg log_rev orders"92        )93        #expect(first.scalars["c_orders"] == second.scalars["c_orders"])94    }9596    @Test("histogram and kdensity build sensible plot specs")97    func singleVariablePlots() async throws {98        _ = try await session.execute("histogram revenue")99        let histogram = try #require(await session.lastPlot)100        #expect(histogram.kind == .histogram)101        #expect(histogram.series[0].y.reduce(0, +) == 60)   // counts sum to N102103        _ = try await session.execute("kdensity revenue")104        let density = try #require(await session.lastPlot)105        #expect(density.kind == .kdensity)106        // Density integrates to ~1 (trapezoid over the grid).107        let series = density.series[0]108        var integral = 0.0109        for i in 1..<series.x.count {110            integral += 0.5 * (series.y[i] + series.y[i - 1])111                * (series.x[i] - series.x[i - 1])112        }113        #expect(abs(integral - 1) < 0.05, "density integral \(integral)")114    }115}116