stats-claw 0.2.1

Data science on the hot path: in-process, zero-dependency statistical computing for Rust (distributions, hypothesis tests, resampling) validated against scipy
Documentation
  • Coverage
  • 100%
    576 out of 576 items documented140 out of 286 items with examples
  • Size
  • Source code size: 1.13 MB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 9.72 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 4s Average build duration of successful builds.
  • all releases: 4s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • nitrotap/stats-claw
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • nitrotap

Run inference where it has to live: inside a latency-critical Rust path, in-process and deterministic, with no Python round-trip and nothing to link. The standout is a classical hypothesis-test suite with scipy-grade p-values (exact and asymptotic modes) — the piece the Rust ecosystem otherwise lacks.

[dependencies]
stats-claw = "0.2"
use stats_claw::distributions::{Cdf, Moments, NormalDistribution, Pdf, Quantile};

let z = NormalDistribution { mean: 0.0, standard_deviation: 1.0, ..Default::default() };

assert!((z.pdf(0.0) - 0.398_942_280_401_432_7).abs() < 1e-12); // 1/sqrt(2*pi)
assert!((z.cdf(0.0) - 0.5).abs() < 1e-12);
assert!((z.quantile(0.975) - 1.959_963_984_540_054).abs() < 1e-9);
assert_eq!(z.variance(), Some(1.0));

Why stats-claw

  • Zero runtime dependencies. std-only. No BLAS, no LAPACK, no transitive supply chain — it compiles in seconds and drops into any Rust binary, including constrained targets.
  • scipy-grade, and proven so. Every numeric is checked against committed golden fixtures generated from the reference Python libraries, within documented tolerances (down to 1e-12).
  • Deterministic by construction. A hand-written SplitMix64 PRNG gives byte-identical uniform sampling and resampling across platforms — reproducible tests, reproducible science. (See Compatibility for the one documented exception.)
  • Formally verified where it counts. 68 Kani proof harnesses model-check panic-freedom and index bounds over complete scalar input domains — every RNG state, every hash word — rather than sampled ones. See VERIFICATION.md for what is proven and, just as importantly, what is not.
  • The inference suite Rust was missing. Exact and asymptotic p-values for the classical tests (t, ANOVA, χ², Fisher, Mann–Whitney, Wilcoxon, Kruskal–Wallis, KS, Shapiro), with log-space tails for extreme significance.

Validated against reference libraries

Area Reference Tolerance
Distributions (pdf/cdf/ppf/moments, log-space tails) scipy.stats 1e-9 – 1e-12
Hypothesis tests (t/ANOVA, χ²/Fisher/McNemar, KS/Anderson/Shapiro, Mann–Whitney/Wilcoxon/Kruskal) scipy.stats (incl. method="exact") ≤ 1e-8
Regression (OLS, ridge) scikit-learn ~1e-12
Clustering / decomposition / change-point scikit-learn, ruptures per-test
Association rules mlxtend 1e-12

What's in the box

Distributionspdf/pmf, cdf, quantile, moments, log-space cdf/sf, and seeded sampling for 14 families: normal, uniform, Cauchy, Laplace, exponential, gamma, beta, Weibull, log-normal, chi-squared, Student's t, F, binomial, and Poisson.

Hypothesis tests — parametric (one-sample / independent / paired / Welch t-tests, ANOVA, variance), non-parametric (Mann–Whitney, Wilcoxon, Kruskal–Wallis, Friedman), categorical (χ², Fisher, McNemar, Cochran), goodness-of-fit (KS, Anderson–Darling, Shapiro–Wilk), and correlation — each returning the statistic, p-value (exact or asymptotic), degrees of freedom, and effect size where defined.

Resampling — bootstrap, jackknife, permutation, cross-validation, and percentile confidence / credible intervals, all driven by the deterministic PRNG.

Streaming — single-pass online estimators (RunningMoments via Welford, P2Quantile) for latency-critical and unbounded-data workloads.

Also included — gradient and second-order optimizers (gradient descent, Adam, RMSProp, AdaGrad, SGD, Newton, L-BFGS, conjugate-gradient) plus stochastic search (genetic, simulated annealing); regression (OLS, ridge); clustering (k-means, DBSCAN, GMM, hierarchical, spectral, mean-shift, affinity propagation); decomposition / embedding (PCA, ICA, factor analysis, t-SNE, UMAP, LLE); and density, outlier, feature-selection, change-point (PELT), cardinality, and association-rule utilities. These overlap mature crates like linfa and smartcore; the inference suite above is the focus.

Usage

Distributions are plain parameter structs with Default; the behaviour lives in traits you bring into scope (Pdf, Pmf, Cdf, Quantile, Moments, LogCdf, Sample).

Distributions

use stats_claw::distributions::{Moments, Pmf, BinomialDistribution};

let coins = BinomialDistribution {
    number_of_trials: 10,
    success_probability: 0.5,
    ..Default::default()
};

assert_eq!(coins.mean(), Some(5.0));
assert!(coins.pmf(5) > coins.pmf(3)); // 5 heads is likelier than 3

Deterministic sampling

use stats_claw::distributions::{NormalDistribution, Sample};
use stats_claw::rng::SplitMix64;

let dist = NormalDistribution { mean: 100.0, standard_deviation: 15.0, ..Default::default() };
let mut rng = SplitMix64::new(7); // same seed => same stream, on every platform

let draws: Vec<f64> = (0..1_000).map(|_| dist.sample(&mut rng)).collect();
assert_eq!(draws.len(), 1_000);

Hypothesis testing

use stats_claw::tests_stat::{parametric::t_test_ind, Alternative};

let control = [5.1, 4.9, 5.3, 5.0, 4.8, 5.2];
let treated = [6.2, 6.0, 5.9, 6.4, 6.1, 6.3];

// Returns a TestResult; `?` works in any function returning stats_claw::error::Result.
let r = t_test_ind(&control, &treated, Alternative::TwoSided)?;

println!("t = {:.3}, p = {:.6}", r.statistic, r.p_value);
assert!(r.p_value < 0.05);             // significantly different means
assert_eq!(r.df, Some(10.0));          // n_a + n_b - 2

For ordinal or non-normal data, reach for the rank tests:

use stats_claw::tests_stat::{nonparametric::mann_whitney_u, Alternative};

let a = [1.0, 2.0, 3.0, 4.0];
let b = [10.0, 11.0, 12.0, 13.0];

let r = mann_whitney_u(&a, &b, Alternative::TwoSided, /* continuity */ true)?;
assert!(r.p_value < 0.05);
assert!(r.effect_size.is_some());      // rank-biserial correlation

Bootstrap confidence intervals

use stats_claw::resampling::{bootstrap_statistic, percentile_ci};
use stats_claw::rng::SplitMix64;

let sample = [4.1, 5.5, 6.3, 4.8, 5.9, 6.1, 5.2, 4.6];
let mean = |xs: &[f64]| xs.iter().sum::<f64>() / xs.len() as f64;

let boots = bootstrap_statistic(&sample, 10_000, &mut SplitMix64::new(42), mean)?;
let (lo, hi) = percentile_ci(&boots, 0.05)?; // 95% interval
assert!(lo < hi);

Streaming estimators

use stats_claw::streaming::RunningMoments;

let mut acc = RunningMoments::new();
for x in [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
    acc.update(x); // O(1) memory, single pass
}

assert_eq!(acc.count(), 8);
assert!((acc.mean() - 5.0).abs() < 1e-12);
assert!(acc.variance() > 0.0); // Bessel-corrected (n-1) sample variance

Regression

use stats_claw::algorithms::regression::ols;

let x = vec![vec![0.0], vec![1.0], vec![2.0], vec![3.0]]; // one row per observation
let y = vec![2.0, 5.0, 8.0, 11.0];                        // y = 3x + 2

let model = ols(&x, &y)?;
assert!((model.intercept() - 2.0).abs() < 1e-9);
assert!((model.coefficients()[0] - 3.0).abs() < 1e-9);

Performance

The hot paths are written for throughput: a SIMD ziggurat normal sampler (with a scalar fallback and runtime feature detection), log-space tail evaluation to keep extreme p-values accurate instead of underflowing to zero, and single-pass streaming estimators that hold O(1) state. unsafe is confined to feature-gated SIMD intrinsics, each carrying a // SAFETY: justification; everything else is safe Rust.

Verification

Beyond the test suite, the crate carries 68 Kani proof harnesses — model-checking properties over every input of a type rather than a sampled set. They establish panic-freedom, index-bounds, and structural invariants, for example:

  • next_f64() lands in [0, 1) for every one of the PRNG's 2^64 states.
  • Every index derived into the 128-entry ziggurat tables is in bounds, for every u64 drawn.
  • The HyperLogLog register index stays below 2^p for every hash word at every supported precision.
  • The regression 2×2 solver is total: Ok or Singular, never a panic.

Two things they do not establish. They say nothing about numerical accuracy — Kani over-approximates exp/ln, so accuracy remains the job of the golden-fixture equivalence suite above. And because CBMC is a bounded model checker, properties parameterised by a collection size (permutation bijectivity, k-fold partitioning) are proven at small fixed sizes rather than for all N; VERIFICATION.md states the bound for each. unsafe (the SIMD paths) is covered dynamically by Miri instead, since model checkers cannot see through core::arch intrinsics.

The harnesses live behind #[cfg(kani)], so they are invisible to cargo build/test and Kani is never a dependency — the zero-runtime-dependency guarantee is unchanged. Full detail, including the documented limits, is in VERIFICATION.md.

Compatibility

  • MSRV: Rust 1.93 (edition 2024). The minimum supported version is tested in CI.
  • Dependencies: none at runtime (std-only).
  • Determinism: the PRNG stream is byte-identical across operating systems and architectures, and so is anything computed from it by plain arithmetic — Rust does not reassociate floating-point, so even a 100k-term reduction over the uniform stream is bit-identical on every target and at every optimisation level. The documented exception is the normal sampler: Box–Muller evaluates ln, sin, and cos through the platform math library, which is accurate to well under an ULP but is not correctly rounded. So individual standard_normal draws — and any statistic accumulated from them — can differ in the last ULP or two between targets, and even between optimisation levels of the same source. Compare those with a tolerance rather than bit-for-bit.

Status

Pre-stable (0.x): the public API is still being refined and may change before 1.0, following semantic versioning. The validation harness and tolerances are part of the test suite, so numeric behaviour is held stable even while names settle.

License

Dual-licensed under either of

at your option. Contributions are accepted under the same dual license.