Anofox Statistics

September 2, 2026 ยท View on GitHub

A statistical analysis extension for DuckDB, providing regression analysis, diagnostics, and inference capabilities directly within your database.

License: BSL 1.1 DuckDB Version WASM

Important

This extension is in early development, so bugs and breaking changes are expected. Please use the issues page to report bugs or request features.


๐Ÿ“‹ Table of Contents


โœจ Key Features

Regression Methods

MethodFunctionDescription
OLSols_fit, ols_fit_aggOrdinary Least Squares
Huberhuber_fit, huber_fit_aggRobust M-estimator (outlier-resistant; reports MAD scale + outlier mask)
RANSACransac_fit, ransac_fit_aggRobust consensus regression (outlier-resistant; reports inlier mask + trial count)
Theil-Sentheil_sen_fit, theil_sen_fit_aggRobust nonparametric regression via spatial-median over OLS subsamples
Ridgeridge_fit, ridge_fit_aggL2 regularization
Elastic Netelasticnet_fit, elasticnet_fit_aggCombined L1+L2 regularization
WLSwls_fit, wls_fit_aggWeighted Least Squares
RLSrls_fit, rls_fit_aggRecursive Least Squares (online)
Poissonpoisson_fit_aggGLM for count data
Binomialbinomial_fit_aggGLM for success-rate data (logit / probit / cloglog links)
Negative Binomialnegbinom_fit_aggGLM for overdispersed counts (dispersion ฮฑ estimated jointly)
Tweedietweedie_fit_aggGLM for positive-skew continuous outcomes
Gammagamma_fit_aggGLM for strictly-positive continuous outcomes (claims, durations)
Logisticlogistic_fit_aggBinary classification (binomial GLM with logit link); reports accuracy + threshold echo, optional L2 penalty
ALMalm_fit_agg24 error distributions
BLS/NNLSbls_fit_agg, nnls_fit_aggBounded/Non-negative LS
PLSpls_fit, pls_fit_aggPartial Least Squares
Isotonicisotonic_fit, isotonic_fit_aggMonotonic regression
Quantilequantile_fit, quantile_fit_aggQuantile/median regression
AFT survivalaft_fit_aggDuration models with right censoring
Mixed effectsglmm_fit_aggRandom intercept over a grouping factor
EB shrinkageeb_shrink_aggPartial pooling of per-group estimates

Statistical Hypothesis Tests

CategoryFunctionDescription
Normalityshapiro_wilk_agg, jarque_bera_agg, dagostino_k2_aggNormality tests
Parametrict_test_agg, one_way_anova_agg, yuen_agg, brown_forsythe_aggParametric tests
Nonparametricmann_whitney_u_agg, kruskal_wallis_agg, wilcoxon_signed_rank_agg, brunner_munzel_agg, permutation_t_test_aggNonparametric tests
Correlationpearson_agg, spearman_agg, kendall_agg, distance_cor_agg, icc_aggCorrelation tests
Categoricalchisq_test_agg, chisq_gof_agg, g_test_agg, fisher_exact_agg, mcnemar_aggContingency table tests
Effect Sizecramers_v_agg, phi_coefficient_agg, contingency_coef_agg, cohen_kappa_aggAssociation measures
Proportionprop_test_one_agg, prop_test_two_agg, binom_test_aggProportion tests
Equivalencetost_t_test_agg, tost_paired_agg, tost_correlation_aggTOST equivalence tests
Distributionenergy_distance_agg, mmd_aggDistribution comparison
Forecastdiebold_mariano_agg, clark_west_aggForecast evaluation

Diagnostics & Utilities

FunctionDescription
vif, vif_aggVariance Inflation Factor
aic, bicModel selection criteria
residuals_diagnostics_aggResidual analysis (raw, standardized, studentized, leverage arrays)
aid_agg, aid_anomaly_aggDemand pattern classification

Fit-Predict Table Macros (*_fit_predict_by)

Table macros for easy per-group model fitting and prediction with a single function call:

MacroDescription
ols_fit_predict_byOLS per-group fit + predict
huber_fit_predict_byHuber robust per-group fit + predict
ransac_fit_predict_byRANSAC robust per-group fit + predict
theil_sen_fit_predict_byTheil-Sen robust per-group fit + predict
ridge_fit_predict_byRidge per-group fit + predict
elasticnet_fit_predict_byElasticNet per-group fit + predict
wls_fit_predict_byWLS per-group fit + predict
rls_fit_predict_byRLS per-group fit + predict
bls_fit_predict_byBounded LS per-group fit + predict
alm_fit_predict_byALM per-group fit + predict
poisson_fit_predict_byPoisson GLM per-group fit + predict
pls_fit_predict_byPLS per-group fit + predict
isotonic_fit_predict_byIsotonic per-group fit + predict
quantile_fit_predict_byQuantile per-group fit + predict

โšก Performance

The extension is built with performance as a first-class concern. The Phase-4 benchmark harness (scripts/bench.sh) covers three representative workloads:

WorkloadScalePer-group cost
W1 โ€” aggregate dispatch10K groups / 1M rows~3.2 ยตs per OLS fit (3 features)
W2 โ€” fit + predict10K groups / 1M rowsfit โ†’ predict โ†’ marshal pipeline
W3 โ€” FFI micro (inference)500 groups / 50K rows~4.8 ยตs per fit with compute_inference: true

Profiling showed that the dominant cost is DuckDB's own HASH_GROUP_BY dispatch (~66% of query time) โ€” the extension's per-call overhead is a minority. Two optimizations landed in Phase 4:

  • DataArray::to_vec bulk-copy fast path (no-NULL path): eliminated per-element branching for dense columns, yielding a consistent ~3โ€“4% query-time reduction at 5M rows / 50K groups (controlled A/B).
  • FfiVec<T> RAII wrapper + alloc_inference_arrays! macro: replaced 6 hand-written libc::malloc inference blocks, removing manual free/OOM boilerplate without changing allocation count (inherent to the FFI ABI).

Benchmark results and before/after numbers live in bench/PROFILING.md and bench/baseline/. Run bash scripts/bench.sh to reproduce.

๐ŸŽจ User-Friendly API

Phase 5 (ERGO) made the API consistent and ergonomic:

  • Unprefixed function names: all functions are now ols_fit_agg(...), theil_sen_fit(...), etc. The old anofox_stats_ prefix is gone โ€” see docs/API_CONVENTIONS.md ยง5 for migration.
  • MAP-style options with documented keys (e.g. {'fit_intercept': true, 'compute_inference': true}); unknown keys raise InvalidInputException at bind time instead of being silently ignored.
  • Early input validation: dimension mismatches, insufficient rows, all-non-finite input, and constant columns are caught with a descriptive message before any computation.
  • Consistent return-struct field names across families: r_squared (not .r2), residual_std_error, n_observations, z_values for GLM/AFT (not t_values).

The regression algorithms are validated against R's lm(), glmnet, and other standard statistical packages in the anofox-regression Rust crate.


๐Ÿš€ Quick Start

All functions use the v0.3.0 API โ€” unprefixed names and MAP-style options. See docs/API_CONVENTIONS.md for the full naming and options reference.

Step 1 โ€” Create a small dataset and fit an OLS model

-- Dataset: house size (sqm) and sale price (kEUR)
CREATE TABLE houses AS SELECT * FROM (VALUES
    (50.0, 120.0), (65.0, 155.0), (80.0, 190.0),
    (95.0, 225.0), (110.0, 265.0), (125.0, 300.0)
) t(sqm, price_keur);

-- Fit: OLS regression (price_keur ~ sqm) via the aggregate form
SELECT
    round((ols_fit_agg(price_keur, [sqm])).r_squared, 4) AS r_squared,
    (ols_fit_agg(price_keur, [sqm])).coefficients[1]      AS slope,
    (ols_fit_agg(price_keur, [sqm])).intercept            AS intercept
FROM houses;
-- r_squared โ‰ˆ 0.9995, slope โ‰ˆ 2.41, intercept โ‰ˆ -1.67

Step 2 โ€” Predict on new data

The scalar ols_fit takes y and X in column-major format (each inner array is one feature column across all observations). Use the scalar predict(X_new, coefficients, intercept) to score new points:

-- Predict prices for three new house sizes (70, 100, 140 sqm)
-- ols_fit column-major X: [[sqm_col]] = one feature column with all 6 training values
SELECT
    unnest([70.0, 100.0, 140.0]) AS new_sqm,
    unnest(predict(
        [[70.0, 100.0, 140.0]]::DOUBLE[][],
        (ols_fit([120.0, 155.0, 190.0, 225.0, 265.0, 300.0],
                 [[50.0, 65.0, 80.0, 95.0, 110.0, 125.0]])).coefficients,
        (ols_fit([120.0, 155.0, 190.0, 225.0, 265.0, 300.0],
                 [[50.0, 65.0, 80.0, 95.0, 110.0, 125.0]])).intercept
    )) AS predicted_keur;
-- 70 sqm โ†’ ~167 kEUR, 100 sqm โ†’ ~239 kEUR, 140 sqm โ†’ ~336 kEUR

Step 3 โ€” Inspect residuals

-- Residual diagnostics: raw and standardized residuals from in-sample predictions
WITH preds AS (
    SELECT
        unnest([120.0, 155.0, 190.0, 225.0, 265.0, 300.0])::DOUBLE AS actual,
        unnest(predict(
            [[50.0, 65.0, 80.0, 95.0, 110.0, 125.0]]::DOUBLE[][],
            (ols_fit([120.0, 155.0, 190.0, 225.0, 265.0, 300.0],
                     [[50.0, 65.0, 80.0, 95.0, 110.0, 125.0]])).coefficients,
            (ols_fit([120.0, 155.0, 190.0, 225.0, 265.0, 300.0],
                     [[50.0, 65.0, 80.0, 95.0, 110.0, 125.0]])).intercept
        )) AS yhat
)
SELECT
    (residuals_diagnostics_agg(actual, yhat)).raw AS raw_residuals
FROM preds;

Per-group regression with GROUP BY

All *_fit_agg functions support GROUP BY for per-segment models:

-- Fit a separate OLS model per product category
SELECT
    category,
    (ols_fit_agg(revenue, [units_sold], {'compute_inference': true})).r_squared AS r_squared,
    (ols_fit_agg(revenue, [units_sold], {'compute_inference': true})).coefficients[1] AS slope
FROM sales_data
GROUP BY category;

๐Ÿ“ฆ Installation

Install directly from our public distribution bucket. DuckDB must be started with the -unsigned flag, since the extension binary is not signed by the DuckDB Foundation:

INSTALL 'anofox_statistics' FROM 'http://get.erpl.io';
LOAD 'anofox_statistics';

This pulls the right binary for your platform (Linux amd64/arm64, macOS amd64/arm64, Windows amd64). No build toolchain, no vcpkg, no submodules required.

Community Extension

INSTALL anofox_statistics FROM community;
LOAD anofox_statistics;

Telemetry

This extension collects anonymous usage telemetry to help improve the product. Telemetry is enabled by default and includes:

  • Extension load events (extension name, version, platform)
  • Function execution events (which functions are used)
  • No personal data or query contents are collected

Disable telemetry:

export DATAZOO_DISABLE_TELEMETRY=1
SET anofox_telemetry_enabled = false;

For more information, see the posthog-telemetry repository.


๐Ÿ“š API Reference

The authoritative function reference and naming conventions live in the docs/ directory โ€” not duplicated here to avoid drift:

  • docs/API_REFERENCE.md โ€” complete function signatures, option keys, and return-struct fields for all 100+ functions.
  • docs/API_CONVENTIONS.md โ€” naming convention, MAP-option keys, return-struct field names, error taxonomy, and the v0.3.0 breaking-changes migration guide.

User-facing guides are in the guides/ directory:


๐Ÿ› ๏ธ Development

Building from source

Prerequisites: Rust stable toolchain, a C++17 compiler, and CMake (all provided by extension-ci-tools):

git clone --recurse-submodules https://github.com/DataZooDE/anofox-statistics.git
cd anofox-statistics
make release

This produces build/release/duckdb (the CLI) and build/release/extension/anofox_statistics/anofox_statistics.duckdb_extension.

Rust unit tests (the core regression library):

cargo test

DuckDB SQL test suite (2 000+ assertions across all function families):

build/release/test/unittest --test-dir=test/sql

Doc-SQL validation (all SQL examples in README + guides + API docs must pass):

python3 scripts/validate_docs_sql.py          # full 7-file sweep
python3 scripts/validate_docs_sql.py --file README.md   # single-file fast path

Benchmark harness (repeatable timing across three workloads):

bash scripts/bench.sh          # default: ~1 s total
bash scripts/bench.sh --full   # adds 1M-group workload (~8 GB RAM, ~160 s)

Results are written to bench/results/ as diffable markdown files. See bench/README.md for details.

Contributing

Contributions are welcome:

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes with tests
  4. Submit a pull request

Areas for contribution: additional statistical tests, visualization helpers for diagnostics, documentation and examples, bug reports and fixes, performance optimizations.


๐Ÿ’ฌ Support

If a fit misbehaves or a result looks wrong, please open an issue โ€” regression against real data has failure modes we cannot reproduce from synthetic tests, so a report with your data shape is the fastest path to a fix. Errors from the fit and predict functions include that link.

If it saved you time, a star on the repo helps other people find it.

The first time you load the extension in an interactive terminal each day, a small banner says the same thing. It never prints when output is piped, in notebooks, or in CI. Silence it with SET datazoo_banner = false; or DATAZOO_NO_BANNER=1.


๐Ÿ“– Citation

If you use this extension in research, please cite:

@software{anofox_statistics,
  title = {Anofox Statistics: Statistical Analysis Extension for DuckDB},
  author = {DataZoo DE},
  year = {2025},
  url = {https://github.com/DataZooDE/anofox-statistics},
  version = {1.0.0}
}

โš–๏ธ License

This project is licensed under the Business Source License 1.1 (BSL 1.1).

Key Terms

  • Usage Grant: Free to use, modify, and distribute for non-production purposes
  • Production Use: Permitted after 4 years from release date, or under a commercial license
  • Change Date: [Release Date + 4 years]
  • Change License: Apache License 2.0

See LICENSE for full terms.

Why BSL?

The BSL allows:

  • Free use for development, testing, and research
  • Open source collaboration and contributions
  • Academic and educational use
  • Small-scale production use

While ensuring:

  • Sustainable development funding
  • Protection for the project's long-term viability
  • Future conversion to fully open source (Apache 2.0)

For commercial production use before the Change Date, please contact: contact@datazoo.de