Skip to main content

Symbolic regression with automatic null-baseline ΔBIC. Find formulas AND prove they're not noise.

Project description

nullbic

Symbolic regression with automatic null-baseline ΔBIC. Find formulas AND prove they're not noise.

from nullbic import Dataset, discover

ds = Dataset.from_csv("data.csv", target="y")
report = discover(ds, n_generations=40)

print(report.summary())
print("real signal?", report.is_real_signal())

Why this exists

Existing symbolic-regression tools (PySR, Eureqa, gplearn, SymbolicRegression.jl) all give you a formula. None of them, by default, tell you whether that formula meaningfully beats a constant model or a trivial linear fit. Most "discoveries" published with these tools are indistinguishable from noise, and the user has no easy way to tell.

nullbic ships those baselines as a first-class output:

  • ΔBIC vs F = const (the mean predictor)
  • ΔBIC vs F = linear (best OLS on all features)
  • z-score vs shuffled-target distribution (30 shuffles by default)
  • Verdict.STRONG / WEAK / NOISE assigned automatically

If the discovered formula doesn't beat all three baselines, the verdict downgrades. No silent overfit.

Install

git clone https://github.com/yourname/nullbic
cd nullbic
pip install -e .
# optional: pandas integration
pip install -e ".[pandas]"

The Rust core ships as a pre-built shared library inside the package (nullbic/_dll/nullbic.dll on Windows; .so / .dylib on Linux / macOS). No Rust toolchain needed at install time.

Usage

From a pandas DataFrame

import pandas as pd
from nullbic import Dataset, discover

df = pd.read_csv("merge_results.csv")
ds = Dataset.from_pandas(df, target="cosine_sim")
rep = discover(ds, n_generations=40, pop_size=200, max_depth=4)

if rep.is_real_signal():
    print("Formula:", rep.formula)
    print(f"ΔBIC vs const : {rep.delta_bic_const:+.1f}")
    print(f"ΔBIC vs linear: {rep.delta_bic_linear:+.1f}")
    print(f"z vs shuffled : {rep.z_vs_shuffled:+.2f}")

From a CSV

from nullbic import Dataset, discover

ds = Dataset.from_csv("data.csv", target="y")
print(discover(ds).summary())

From a CLI

nullbic data.csv y --gens=40 --pop=200 --depth=4

Exit code is 0 if the verdict is STRONG or WEAK, 1 if NOISE — handy in CI/CD signal-validation pipelines.

Three real use cases

1. Kaggle / tabular insight

Point at any cleaned dataset. Get a formula. Get the proof it's not in the noise.

2. Black-box surrogate

Approximate an XGBoost / NN model with a symbolic surrogate; the verdict tells you when the surrogate is meaningful vs cosmetic.

3. Empirical-law audit

Feed in a paper's claimed empirical relationship. The verdict says whether the relationship really beats a linear baseline on the given data.

How the verdict is assigned

Verdict Criteria
STRONG ΔBIC vs const < −10 AND ΔBIC vs linear < −10 AND z vs shuffled < −2
WEAK Beats const but not all three thresholds
NOISE Doesn't beat const → formula is not extracting any signal

These thresholds match standard model-selection conventions (Kass & Raftery 1995 / Schwarz 1978).

Architecture

nullbic (Python package)
  └── core.py             → public API (Dataset, discover, …)
  └── _bindings.py        → ctypes layer (private)
  └── _dll/nullbic.dll    → Rust shared library (pre-built)

nullbic-core (Rust crate, ~600 LOC)
  └── dataset             → tabular rows + train/test split + shuffled
  └── expr                → expression trees over named features
  └── optimizer           → single-level GA, niching, hyper-mutation
  └── baselines           → F=const + F=linear (OLS) + shuffled-target
  └── c_api               → extern "C" entry points

Performance

Typical run on 500 rows × 10 features, 40 generations, pop 200:

  • ~30–80 ms wall-clock on one core
  • ~150 MB peak RAM
  • Deterministic for a given seed

The GA is parallel via Rayon; throughput scales near-linearly with cores.

Limitations and honest caveats

  • The GA is intentionally simple. PySR is more sophisticated when raw accuracy is the only goal. nullbic trades a few percent of accuracy for the auto-falsification report.
  • The "linear baseline" is plain OLS with a tiny ridge for numerical safety; it's not feature-engineered. If you're comparing to a serious linear model, replace delta_bic_linear with your own ΔBIC.
  • 580 rows is comfortable. Below ~50 rows, results are unreliable — the shuffled-target distribution is too noisy to anchor the z-score.

License

MIT. See LICENSE.

Citation

If nullbic contributes to a paper, please cite it as:

nullbic: symbolic regression with auto-falsification ΔBIC. 2026.

Related

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nullbic-0.1.0-py3-none-win_amd64.whl (671.3 kB view details)

Uploaded Python 3Windows x86-64

File details

Details for the file nullbic-0.1.0-py3-none-win_amd64.whl.

File metadata

  • Download URL: nullbic-0.1.0-py3-none-win_amd64.whl
  • Upload date:
  • Size: 671.3 kB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.6

File hashes

Hashes for nullbic-0.1.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 5a436c378b3ffad3c6f7b033d1ba63e8a1545fbbb50bca57eb40312f305c3331
MD5 9054fd48fa7f785c8d71707464c7bdcc
BLAKE2b-256 6b7e93053e3d8231618e47a01339672a08e03c43344ce06e16301f4d053cf8e3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page