Structboost

Latest version: v0.4.0

Safety actively analyzes 993520 Python packages for vulnerabilities to keep your Python projects secure.

Scan your dependencies

Page 1 of 2

3.12

classifier list says what the requirement already allowed. 3.14 is left out
until the `[bae]` extra's wheels are dependably available there.

`CITATION.cff` gains `version` and `date-released`, which a citation file for a
released version cannot do without, plus a `url` for the documentation site.

0.7

three real datasets it removed 21-52% of recovered marker genes relative to the
fitted encoder, and 0-7% even when the representation had settled. At 0.5 the
worst loss over the same six runs was 6%. The standalone
`structboost.stability_selection` keeps 0.7, because its Meinshausen-Buhlmann
bound is undefined at or below 0.5.

**The `dim_match_quality` warning now fires below 0.85, from 0.5.** It is the gate
on whether iteration frequencies mean anything: they describe gene-set drift only
if the counted iterations describe one representation. Runs sitting at 0.70-0.79 —
comfortably above the old warning — already lost a quarter to a half of their
recovered markers at the default threshold, while runs at 0.93 and above lost
none. The guide now documents it as a lookup: at least 0.93, either threshold is
safe; 0.70-0.79, use 0.3-0.5 and prefer the flat union.

Also fixed: the gene-selection guide still showed
`fit(adata, stability_selection="iteration")`, which stopped being valid in 0.3.0
when that argument became a bool.

0.4.0

Three defaults change. No configuration fields are added or removed, and anyone
who sets these explicitly is unaffected.

**`enable_early_stopping` now defaults to `False`.** The criterion is a
convergence check being used as a quality check. There is no validation split, so
the training loss cannot see a model that is starting to memorize, and patience
fires long before the model is done: measured against ground truth it stopped at
iteration 90 on a dataset peaking at 259, and at 196 on a simulated scenario
peaking at 560, returning marker-recovery F1 0.502 against 0.787. Across three
real datasets (mouse cortex, human pancreas, human immune) the best iteration
ranged from 154 to 1975 — always past where patience fires.

**No stopping rule replaced it, deliberately.** Latent stability, encoder-support
overlap and held-out reconstruction were each measured as candidates and each
rejected. The representation settles long before gene selection does — on one
dataset consecutive latent codes were rank-identical while the gene set still
turned over 65% cumulatively — and no observable signal tracks the quality peak.
A latent-stability rule was built and tuned; it fired at iteration ~109 on all
three real datasets regardless of where quality peaked, and on one of them it was
worse than not stopping at all. It is not shipped, not even off by default: an
option that should never be enabled is pure carrying cost, which is the same
argument that removed four fields in 0.3.0. `max_iterations=1000` is a defensible
middle of the measured range, not an optimum.

**`BAE.stability_selection(threshold=...)` now defaults to `0.5`, from `0.7`.**

0.3.0

**Breaking.** Five settings are gone and `BAE.stability_selection` has one mode
instead of two. Every removed option was off by default, so a fit that took the
defaults is unaffected: the encoder matrix is bitwise identical across a plain
fit, a batch-integrated fit, and both disentanglement methods. What changes is the
surface you have to reason about.

`boosting_nu` stays at `0.1`. Raising it to `0.3` was measured and deferred: at
the default `stepno=50` it matches `0.1`'s marker recovery three to four times
faster and wins outright on low-signal data, but then degrades if training
continues, and the training-MSE stopping rule cannot see it happening. The guide
records the numbers; the two changes belong together and will land together.

**Checkpoints written by 0.2.0 no longer load.** `restore_payload` splats the
stored config into `BAEConfig`, so a dropped field is an unexpected keyword
argument rather than a missing one. The format is bumped to 5 and the loader
refuses 4 by name. No migration is written: the method is under active
development and a checkpoint is cheap to regenerate, whereas a compatibility
shim for options that no longer exist is not.

Removed from `BAEConfig`:

- `decoder_dropout_rate` and `decoder_use_batch_norm`. Batch norm was already
documented as harmful (marker-recovery F1 0.59-0.73) because the boosting
target is computed with the decoder in eval mode and the update applied in
train mode. Dropout has exactly the same inconsistency and it was never written
down: the target comes from the full network, the update from a thinned one.
With both gone the decoder is a deterministic per-cell function, which is the
property `_compute_boosting_targets` has always relied on.
- `disentanglement_standardize`. Both `disentanglement` methods stay.
- `standardize_targets`. It was a genuine trade-off — higher selection precision,
roughly half the recall — but it is superseded by
`stability_selection(threshold=...)` -> `stable_encoder()` -> `apply_encoder()`,
which is the same trade with a dial that reports what it is doing. Two knobs for
one trade-off is worse than one.

Removed from `fit`:

- `balance_obs`, and with it the `sample_weights` parameter that threaded through
nine methods and both stability paths. Its own documentation conceded the
limit: the weights never reached the `allboost` fit, so gene selection stayed
unbalanced no matter what they were set to. The measured effect was modest (per
group reconstruction-MSE spread 0.231 -> 0.150) for a mechanism that promised
more than it delivered.

`BAE.stability_selection` is now iteration mode only; `mode`, `subsample_frac`,
`n_subsamples` and `n_iterations` are gone, and `fit(stability_selection=...)`
takes a bool. Iteration mode was already the default and measures the lower
false-discovery rate (0.26 against 0.31). The subsample path stays available as
the standalone `structboost.stability_selection`, which is where it belongs: it
resamples cells in the Meinshausen-Buhlmann scheme and works on an `allboost`
problem, so supervised users with no training loop to iterate over still have it.
The Meinshausen-Buhlmann bound it reports was measured to be violated by roughly
an order of magnitude when the targets come from a model fitted on the same
cells, and that warning moved with it.

`uns["bae"]["variance_explained"]` is now written by every fit. It used to appear
only when a covariate argument was passed, which left the quality workflow the
guide documents — compare it against `linear_ceiling` — raising `KeyError` on a
plain `fit(adata)`. The metric has nothing to do with covariates; only the
per-group breakdown does, and that stays behind the covariate guard.

Two fixes found along the way: the `allboost` example in the guide passed
`mode="standard"`, an argument `allboost` has never accepted, so it raised
`TypeError` as written. And `disentangle_boosting_targets` projects through the
origin, with no intercept — exact residualization only on centered targets. That
was masked by `disentanglement_standardize`, which centered them; with the flag
gone the assumption is documented instead.

0.2.0

The boosting loop got faster without changing what it computes. Measured
end-to-end on real preprocessed scRNA-seq (20,000 cells, `latent_dim=10`):
**1.86x at p=2,000, 2.20x at p=3,000, 1.80x at p=8,000**, and 1.46x on a
batch-integrated fit with 20 covariate dummies.

**This is not a methodological improvement.** Across eighteen configurations —
plain, all three `batch_integration_mode` values, flat and per-dimension
`mandatory_genes`, `balance_obs`, `split_softmax`, both disentanglement methods,
`standardize_targets`, frozen/anchored/zero-dimension transfers and both
stability modes — the selected gene set is *identical* and the reconstruction
loss agrees to the sixth decimal. Nothing here recovers a marker that was
previously missed. What it buys is more iterations and more stability runs for
the same budget, which is what a method whose encoder support keeps drifting
actually needs.

Stability selection benefits too, and for the default `n_runs=300` that is the
larger absolute saving: **1.79–1.88x** for iteration mode and **1.31–1.39x** for
subsample mode at p=2,000–3,000, with peak memory during a subsample run falling
from 360 MB to 5 MB at p=3,000 — that figure is exactly the float64 copy of the
expression matrix described below. Subsample mode gains less by construction:
every run draws a different subset of cells, so its column norms genuinely change
and cannot be hoisted, and it uses the lazy column cache rather than the full
matrix.

Precomputing the covariance matrix *inside* subsample mode was measured and
rejected. It runs 3.07x faster at p=2,000 and 2.69x at p=3,000, but 0.73–0.85x
**slower** from p=6,000 upwards: the number of distinct columns a subsample
selects stays roughly flat as p grows, so the full p x p product stops paying for
itself. A memory guard cannot separate those cases — the 800 MB matrix at
p=10,000 fits comfortably and still loses — so that path stays lazy at every p.

`boosting_precompute_covcache` now defaults to `"auto"` and applies to every
boosting path, including iteration-mode stability selection, which previously
ignored the setting entirely. `"auto"` builds the full p x p covariance matrix
whenever it fits a conservative share of system memory; `True` and `False` are
still honoured exactly, and the resolved decision is recorded in
`adata.uns["bae"]["boosting_precompute_covcache"]`.

The flag was documented as a memory-versus-recomputation trade, which undersold
it. Building all `p` columns at once is one compute-bound matrix product near
hardware peak; fetching them one at a time is a sequence of memory-bound
matrix-vector products. Precomputing wins from roughly `p/59` distinct selected
features onwards — a threshold a fit passes in its first iteration.

Three quantities that never change were being recomputed. `col_norms_sq` was
rebuilt on every training iteration although the design matrix is fixed for the
whole fit, allocating a full-size temporary each time; the mandatory-covariate
block was rebuilt on every *boosting step* although it depends only on the
target; and the covariance cache's NaN check re-scanned columns that had already
been verified. The starting residual correlations are now formed for all latent
dimensions in one matrix product rather than one per dimension.

A float64 target passed against float32 predictors used to make NumPy promote
the *design matrix*, materializing a full float64 copy — measured 2.1x slower,
and it silently produced a different answer. The target is now aligned to the
predictors instead.

The three entry points to the same boosting problem ran at two different
precisions: `fit` in float32, both stability paths in float64 on top of a
float32 copy they had already made. They now agree on float32, which removes two
full-size copies of the expression matrix. The precision this gives up was
measured at about one gene in 380 — far inside the run-to-run support variation
this method documents for itself.

Results are **not bit-identical to 0.1.2**. Coefficients move by ~1e-6 relative
and the selected support does not change; a fixed seed no longer reproduces
0.1.2 output exactly.

0.1.2

Project metadata gains `Documentation` and `Changelog` links. PyPI renders
`project.urls` as the sidebar next to the project description, and it carried
only the repository and the issue tracker — so the documentation site, where
every substantive explanation lives, was reachable from the README body but not
from the navigation beside it. Both targets are verified live.

As with 0.1.1, nothing in the package changed: `project.urls` reaches users only
through an upload, so correcting it in the repository has no effect until a
release carries it.

Page 1 of 2

© 2026 Safety CLI Cybersecurity Inc. All Rights Reserved.