Robust Chauvenet Rejection (RCR), Python reimplementation
Project description
rcrpy
Python reimplementation of Robust Chauvenet Rejection (RCR) — statistical outlier rejection that stays accurate on samples that are >85% contaminated.
Based on the algorithm in
Maples et al. 2018, ApJS 238.1.
This package is a numpy / scipy port of the original C++/pybind11
implementation at ../cpp/, preserving algorithmic parity
while running pure Python — no compiler toolchain required.
Install
pip install rcrpy
Runtime deps: numpy>=1.26, scipy>=1.11. Python ≥3.11.
Quick start
Single-value RCR
Reject outliers from a 1-D dataset and recover the underlying central value and width.
import rcrpy
import numpy as np
# Heavily contaminated data: mu=0, sigma=1, plus one-sided outliers
rng = np.random.default_rng(42)
y = np.concatenate([
rng.normal(0, 1, size=150), # clean
np.abs(rng.normal(0, 10, size=850)), # 85% contamination
])
r = rcrpy.RCR(rcrpy.RejectionTech.LS_MODE_68)
r.perform_rejection(y.tolist())
print(f"Recovered mu = {r.result.mu:.3f} (truth: 0)")
print(f"Recovered sigma = {r.result.sigma:.3f} (truth: 1)")
print(f"Points kept = {r.result.flags.sum()} / {len(y)}")
Functional-form RCR (linear model fit with outlier rejection)
Fit a parametric model to (x, y) data while rejecting outliers.
import rcrpy
import numpy as np
# Linear data with outliers
rng = np.random.default_rng(0)
x = np.linspace(-5, 5, 100)
y = 2.0 + 1.5 * x + rng.normal(0, 0.3, size=x.size)
# Inject 20 outliers
out = rng.choice(x.size, size=20, replace=False)
y[out] += rng.normal(15, 5, size=20)
def linear(x, params):
return params[0] + params[1] * x
def d_linear_b(x, params):
return 1.0
def d_linear_m(x, params):
return x
model = rcrpy.FunctionalForm(
linear, x, y, [d_linear_b, d_linear_m], guess=[0.0, 0.0],
)
r = rcrpy.RCR(rcrpy.RejectionTech.LS_MODE_68)
r.set_parametric_model(model)
r.perform_rejection(y.tolist())
b, m = model.result.parameters
print(f"Recovered b = {b:.3f} (truth: 2.0)")
print(f"Recovered m = {m:.3f} (truth: 1.5)")
print(f"Kept {int(r.result.flags.sum())} / {len(y)} points")
Rejection techniques
RejectionTech.* |
Use case |
|---|---|
LS_MODE_68 |
Symmetric uncontaminated distribution, one-sided contaminants |
LS_MODE_DL |
Mixed one-sided + two-sided contaminants |
SS_MEDIAN_DL |
Symmetric uncontaminated distribution, two-sided contaminants |
ES_MODE_DL |
Mildly asymmetric uncontaminated distribution and/or very small N |
⚠️ Known algorithm bug:
ES_MODE_DLcombined withFunctionalForm(parametric models) exhibits a runaway-rejection cascade that drops ~75% of inliers even on perfectly clean data. This is an algorithm bug inherited from the C++ source — the C++ oracle exhibits the same behavior; it is not a port deficiency. Empirical characterization is inbenchmarks/es_mode_dl_truth_test.py. All other combinations of rejection technique × model type work correctly. For functional-form fits, use one ofLS_MODE_68,LS_MODE_DL, orSS_MEDIAN_DL.ES_MODE_DLremains correct for single-value (non-parametric) rejection.
See agents/python_vs_rust_plan.md and Maples et al. 2018 for the science.
More invocations
| Want… | Call |
|---|---|
| Bulk rejection (faster on large N) | r.perform_bulk_rejection(y) |
| Weighted RCR | r.perform_rejection(y, w=weights) |
| User-defined "candidate" mu | subclass rcrpy.NonParametric, r.set_non_parametric_model(model) |
| Add Gaussian / bounded priors | rcrpy.Priors(prior_type=..., gaussian_params=...) → pass via priors= to FunctionalForm |
| Pivot search for power-law / exponential fits | pass pivot_function=... and pivot_guess=... to FunctionalForm |
| ND independent variable | pass xdata as (N, D) array; user functions take vector x |
Status
| Slice | Status |
|---|---|
| Phase 1 single-value RCR (4 techs × iter/bulk × weighted/unweighted) | ✅ 100%; bit-identical to C++ oracle at rtol=1e-12 (worst case 1.7e-15 = 8 ULPs) |
| Phase 2 functional form (1D + ND, priors, pivot search, paramuncertainty) | ✅ 99%; parity with C++ at rtol=5% on 6 sweep scenarios (RNG-limited) |
| Packaging / CI / docs | 🚧 0.1.0 release; collecting real-user feedback before 1.0 |
85 tests passing (+ 1 documented xfail). Reproduce parity with
benchmarks/reflect_parity.py; benchmark
with benchmarks/diagnostics.py and
benchmarks/diagnostics_functional.py.
Layout
python/
├── pyproject.toml
├── README.md (you are here)
├── LICENSE (mirrors ../cpp/LICENSE — academic / non-commercial)
├── src/rcrpy/
│ ├── __init__.py public API re-exports
│ ├── api.py RCR class, RCRResults, RejectionTech, MuType
│ ├── stats.py median, halfSampleMode, robust-sigma helpers
│ ├── rejection.py iterative + bulk loops (lower/single/each sigma)
│ ├── tables.py unity tables ported verbatim from cpp/src/RCR.cpp
│ ├── nonparametric.py NonParametric base class
│ └── functional.py FunctionalForm + Priors for parametric fits
├── tests/ pytest suite (85 + 1 xfailed)
├── docs/ markdown tutorials + PUBLISHING checklist
├── benchmarks/ parity + perf scripts (not part of the wheel)
└── scripts/ one-off codegen + post-install smoke check
Citing
If you use rcrpy in academic work, please cite the original paper:
@article{maples2018robust,
title = {Robust Chauvenet Outlier Rejection},
author = {Maples, M.P. and Reichart, D.E. and Konz, N.C. and Berger, T.A.
and Trotter, A.S. and Martin, J.R. and Dutton, D.A.
and Paggen, M.L. and Joyner, R.E. and Salemi, C.P.},
journal = {The Astrophysical Journal Supplement Series},
volume = {238}, number = {1}, pages = {2}, year = {2018},
}
License
See LICENSE — mirrors the upstream C++ license. Free for academic and non-commercial use; contact UNC's Office of Commercialization and Economic Development for commercial licensing.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rcrpy-0.1.1.tar.gz.
File metadata
- Download URL: rcrpy-0.1.1.tar.gz
- Upload date:
- Size: 192.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.25 {"installer":{"name":"uv","version":"0.9.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2ac3d3072fe7b7cbcaf3a21e7f46e239bf16af30bc50c1c8bfd6c38a913d8b61
|
|
| MD5 |
ffe6684b007c90e5373f09f8cb8d5478
|
|
| BLAKE2b-256 |
687191090bd4fa5af344f58d593a3a6ab116253b7151751872eeef9756c11301
|
File details
Details for the file rcrpy-0.1.1-py3-none-any.whl.
File metadata
- Download URL: rcrpy-0.1.1-py3-none-any.whl
- Upload date:
- Size: 55.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.25 {"installer":{"name":"uv","version":"0.9.25","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
37fae35e95a3673521e546de5347d8f9695911d895c3a3e41ff085819a8f05d7
|
|
| MD5 |
62f15275ff3a8d7a05bb668e37e14304
|
|
| BLAKE2b-256 |
7afa37793a80b594a78b0a62515acef8af834d262e18759e4e3030b7b9fd4f4d
|