SIRUS (Stable and Interpretable RUle Set): interpretable rule-based models from random forests, scikit-learn compatible
Project description
sirus — SIRUS (Stable and Interpretable RUle Set) in Python
A scikit-learn compatible re-implementation of the R package
sirus (CRAN mirror:
cran/sirus) by Clément Bénard et al.
SIRUS is a regression and binary-classification algorithm based on random forests that takes the form of a short, stable list of rules. It keeps the aggregation principle of random forests but, instead of aggregating predictions, it selects the most frequent nodes of a forest of shallow, quantile-constrained trees. The result combines the simplicity of decision rules with accuracy close to random forests — and, crucially, the selected rules barely change when the data is perturbed.
Proportion of class 1 = 0.333 - Sample size n = 150
if Petal.Length < 1.7 then 1 (n=44) else 0.0566 (n=106)
if Petal.Width < 0.4 then 1 (n=41) else 0.0826 (n=109)
if Sepal.Length >= 5.6 then 0.033 (n=91) else 0.797 (n=59)
...
Validated head-to-head against the R package on identical data: the two implementations select the same rules (bit-identical predictions on the classification benchmarks) — see Benchmarks.
Installation
pip install sirus # or: uv add sirus
From source:
pip install git+https://github.com/asubbaswamy/sirus-py
Requires Python ≥ 3.10 with numpy, scipy, scikit-learn, joblib
(installed automatically). pandas is optional (DataFrames, categorical
variables, rule tables, sirus_cv), as is matplotlib (CV plots):
pip install "sirus[pandas,plot]". The library is a single module — if you
prefer, drop sirus.py next to your code instead of installing.
Quick start
from sirus import SirusClassifier, SirusRegressor
# ---- classification (binary) ------------------------------------------
clf = SirusClassifier(num_rules=10, random_state=42)
clf.fit(X_train, y_train) # X: ndarray or DataFrame
clf.print_rules() # human-readable rule list
proba = clf.predict_proba(X_test) # average of the rule outputs
pred = clf.predict(X_test) # threshold at 0.5
# ---- regression ---------------------------------------------------------
reg = SirusRegressor(num_rules=10, random_state=42)
reg.fit(X_train, y_train) # rules combined by non-negative ridge
reg.print_rules() # weights, intercept, rules
y_hat = reg.predict(X_test)
# rules as data
df = clf.rules_to_dataframe() # rule / frequency / outputs / weight
for rule in clf.rules_: # Rule and Condition dataclasses
print(rule.frequency, rule.conditions, rule.output_in, rule.output_out)
Both estimators are plain scikit-learn estimators: they support clone,
get_params/set_params, cross_val_score, pipelines and grid search.
See example.py for worked examples (iris, breast cancer, diabetes).
How it works (same six steps as the R package)
- Discretization — numerical variables are binned on their empirical
q-quantiles (defaultq=10), variables with ≤discrete_limitdistinct values keep their observed values as split points, and categorical variables (non-numeric DataFrame columns) are target-ordered as in ranger. - Forest — a random forest of depth-
max_depthtrees (default 2) is grown on the binned data, so every split falls on a quantile. By default the number of trees is chosen automatically: batches ofnum_trees_steptrees are grown until the estimated stability of the rule selection reaches1 - alpha(95%). - Path extraction — every tree node defines a path (a hyperrectangle); the frequency of each path across trees is computed.
- Selection — the paths occurring in a fraction
> p0of the trees are selected, or simply thenum_rulesmost frequent ones whenp0=None. - Post-treatment — a rule is discarded when its indicator function is a
linear combination of the intercept and higher-ranked selected rules
(e.g.
X1 >= 2is dropped ifX1 < 2was already selected). - Aggregation — each rule outputs the mean training response inside or
outside its hyperrectangle. For classification the rule outputs are simply
averaged; for regression they are combined by a ridge regression
constrained to non-negative coefficients, with the penalty chosen by
cross-validation (the
glmnet(alpha=0, lower.limits=0)step of the R package).
API mapping from R
R (sirus) |
Python |
|---|---|
sirus.fit(data, y, type='classif') |
SirusClassifier(...).fit(X, y) |
sirus.fit(data, y, type='reg') |
SirusRegressor(...).fit(X, y) |
sirus.predict(m, newdata) |
predict_proba(X)[:, 1] / predict(X) |
sirus.print(m) |
m.print_rules() |
num.rule, p0, num.rule.max |
num_rules, p0, num_rules_max |
q, discrete.limit, max.depth |
q, discrete_limit, max_depth |
num.trees, num.trees.step, alpha |
num_trees, num_trees_step, alpha |
mtry (default p/3) |
mtry |
replace, sample.fraction |
replace, sample_fraction |
m$rules, m$rules.out, m$proba |
m.rules_ (conditions, outputs, frequency) |
m$rule.weights, m$rule.glm |
m.rule_weights_, m.intercept_, m.lambda_ |
m$num.trees, m$mean |
m.num_trees_, m.mean_ |
sirus.cv(data, y, nfold, ncv) |
sirus_cv(X, y, nfold=10, ncv=10, ...) |
cv.grid$p0.pred, $p0.stab |
cv.p0_pred, cv.p0_stab |
cv.grid$error.grid.p0 |
cv.error_grid (pandas DataFrame) |
sirus.plot.cv(cv.grid) |
cv.plot() (matplotlib) |
from sirus import sirus_cv, SirusRegressor
cv = sirus_cv(X, y, nfold=10, ncv=10, random_state=0) # tune p0
model = SirusRegressor(p0=cv.p0_stab).fit(X, y) # p0_pred for classif
cv.plot() # error/stability path
sirus_cv follows the R implementation exactly: one model with a large rule
list is fit per fold, and the whole 500-point p0 path is obtained by
truncating its rule list (selection by any p0 is a prefix of the
post-treatment output, because the redundancy filter is greedy-sequential).
Error is the pooled out-of-fold 1-AUC (classification) or unexplained
variance (regression); stability is the average proportion of rules shared
by two models fit on distinct folds, compared in quantile-index space.
For regression it is slow for the same reason the R version is (one
non-negative-ridge cross-validation per rule-list prefix per fold); pass a
fixed num_trees (a few thousand) to bound the runtime.
Implementation notes / differences from the R package
- The forest is built with scikit-learn regression trees on the binned data
(the R package fits ranger regression trees on the numeric 0/1 output;
variance splitting on a binary target is equivalent to Gini, so the split
criterion matches).
min_samples_split=5reproduces ranger's default minimal node size. - Tree paths are counted symbolically as sets of (feature, quantile, side) splits, exactly as defined in the papers and the R package; region-equivalent paths (e.g. a rule and a redundant refinement of it) are merged afterwards by the linear-dependence post-treatment, as in R.
- The post-treatment implements the paper's linear-dependence filter with a
rank test on data resampled from the product of the marginal bin
distributions — the exact procedure of
paths.filter.din the R sources (used there for depth > 2), augmented with the empirical sample. For depth ≤ 2 the R package uses an equivalent symbolic algorithm. - The automatic stopping criterion estimates the expected Dice–Sørensen stability of the selection between two independent forests via a Gaussian approximation of the path frequencies, averaged over the range of thresholds that actually determines the fitted model. The trajectory and the resulting number of trees (≈10⁴ on small datasets) match the R behavior (live A/B on LA Ozone: R stops at 11000 trees, this port at 8000); the exact C++ metric may differ marginally.
- The non-negative ridge CV rebuilds glmnet's lambda path
(
lambda_max = max|Xᵀy|/(0.001·n), 100 log-spaced values, 10-fold CV) and solves each fit as a small exact NNLS problem, so no glmnet dependency is needed. - Extras not in the R package: arbitrary binary labels (not just 0/1),
n_jobsparallel forest growing,rules_to_dataframe(). - The R package's C++ forest is ~6–20× faster to fit than the sklearn-based
forest here (~0.1 s vs 0.6–2 s for ~10⁴ trees on small data). Use
n_jobs=-1for repeated fits (e.g.sirus_cv); a one-off fit gains little because of the worker-pool start-up cost.
Benchmark against the R package
Two benchmarks live in benchmarks/ (results from July 2026, R sirus 0.3.3).
Live A/B on identical data (live_ab_*)
Both implementations are fit on identical train/test CSVs with identical
settings (10 rules, 10000 trees, q=10, depth 2, same mtry). Summary of
benchmarks/output/live_ab_report.md:
| dataset (task) | shared rules (Dice) | max |Δfreq| | prediction agreement (test) |
|---|---|---|---|
| breast cancer (classif) | 10/10 (1.00) | 0.007 | bit-identical (max Δ 5e-14), AUC 0.9891 both |
| categorical (classif) | 10/10 (1.00) | 0.010 | bit-identical (max Δ 5e-14), AUC 0.7384 both |
| diabetes (regression) | 9/10 (0.90) | 0.011 | r = 0.992, R² 0.331 (R) vs 0.337 (py) |
| LA ozone (regression) | 9/10 (0.90) | 0.009 | r = 0.995, R² 0.667 (R) vs 0.683 (py) |
Each regression disagreement is a frequency near-tie at rank 10 (ozone
0.048 vs 0.043, diabetes 0.051 vs 0.052) — which rule lands the last slot
is seed noise. Classification predictions are bit-identical because the
selected rules, their supports, and their outputs match exactly; regression
predictions differ slightly through the cross-validated ridge penalty
(different fold RNG). sirus.cv / sirus_cv agree too: on ozone p0_stab is
0.030 for both, and the error-vs-rules paths differ by ~0.01 (within CV
noise at ncv=2).
Reproduce with R (install.packages("sirus")) available:
python benchmarks/live_ab_prepare.py # shared CSVs + manifest
Rscript benchmarks/live_ab_run_r.R --cv # R side -> benchmarks/output/r_*.json
python benchmarks/live_ab_run_py.py --cv # Python -> benchmarks/output/py_*.json
python benchmarks/live_ab_compare.py # report -> live_ab_report.md
Published rule list (benchmark_vs_r.py)
Comparison to Table 1 of the AISTATS 2021 paper (LA Ozone, produced by the R
implementation with ~9000 trees): 9 of 11 published rules are recovered
identically at num_rules=11 (11/11 at 13; the last two are frequency
near-ties at the cutoff), rule outputs match exactly, frequencies within
±0.012, intercept −7.3 vs −7.8. Note the paper prints display-rounded
thresholds (temp < 65 for the actual decile 65.4): the live R package run
on the same CSV produces exactly the thresholds this port produces.
Development
git clone https://github.com/asubbaswamy/sirus-py && cd sirus-py
conda create -n sirus python=3.12 -y # interpreter from conda ...
uv pip install --python "$(conda run -n sirus which python)" -e ".[dev]"
conda run -n sirus python -m pytest -q # 17 tests
(Any interpreter works — plain uv venv + uv pip install -e ".[dev]" too.)
CI runs the test suite on Linux/macOS/Windows, Python 3.10–3.13, plus the
examples and the published-table benchmark.
Files
sirus.py— the library (SirusClassifier,SirusRegressor,Rule,Condition,sirus_cv,SirusCVResult).example.py— worked examples on iris, breast cancer and diabetes.test_sirus.py— pytest suite.benchmarks/—benchmark_vs_r.py(published-table comparison, no R needed) and thelive_ab_*scripts (R-vs-Python A/B on identical data);LAozone.csvis the LA Ozone dataset from the ESL website.
References
- Bénard C., Biau G., Da Veiga S., Scornet E. (2021a). SIRUS: Stable and Interpretable RUle Set for classification. Electronic Journal of Statistics, 15:427–505.
- Bénard C., Biau G., Da Veiga S., Scornet E. (2021b). Interpretable Random Forests via Rule Extraction. AISTATS 2021, PMLR 130:937–945.
- Breiman L. (2001). Random forests. Machine Learning, 45:5–32.
License
MIT. This is an independent re-implementation of the published algorithm with the R sources read only as a reference — no code was taken from the GPL-3 R/C++ implementation. If you redistribute this package together with the R package, check license compatibility yourself.
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 sirus-0.1.0.tar.gz.
File metadata
- Download URL: sirus-0.1.0.tar.gz
- Upload date:
- Size: 23.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fea4a4760b52977f62368983b5ef1aacb103355a198fadb0b12aeef722a79527
|
|
| MD5 |
01915795ea8843cd07d3764fdaec2efb
|
|
| BLAKE2b-256 |
66b8bb7ede7c3b309a3a47b45b18054f6d1591064c54bec0319ca1f851874aec
|
Provenance
The following attestation bundles were made for sirus-0.1.0.tar.gz:
Publisher:
publish.yml on asubbaswamy/sirus-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sirus-0.1.0.tar.gz -
Subject digest:
fea4a4760b52977f62368983b5ef1aacb103355a198fadb0b12aeef722a79527 - Sigstore transparency entry: 2139461175
- Sigstore integration time:
-
Permalink:
asubbaswamy/sirus-py@fc046b16d528a6cc7125479ab124819be63ad531 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/asubbaswamy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fc046b16d528a6cc7125479ab124819be63ad531 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sirus-0.1.0-py3-none-any.whl.
File metadata
- Download URL: sirus-0.1.0-py3-none-any.whl
- Upload date:
- Size: 24.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
523b6f0eb33d97d907096c6698dc9ddd7f00917c804a3f4bf4ad81044d096dfb
|
|
| MD5 |
9db133ae1a2c917dd4c5552c100b9935
|
|
| BLAKE2b-256 |
e4f6d48786a5b48520947565d54518438307c7b432847cb24f5a4cb51ff22a97
|
Provenance
The following attestation bundles were made for sirus-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on asubbaswamy/sirus-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sirus-0.1.0-py3-none-any.whl -
Subject digest:
523b6f0eb33d97d907096c6698dc9ddd7f00917c804a3f4bf4ad81044d096dfb - Sigstore transparency entry: 2139461260
- Sigstore integration time:
-
Permalink:
asubbaswamy/sirus-py@fc046b16d528a6cc7125479ab124819be63ad531 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/asubbaswamy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@fc046b16d528a6cc7125479ab124819be63ad531 -
Trigger Event:
push
-
Statement type: