structboost 0.2.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {structboost-0.2.0 → structboost-0.4.0}/CHANGELOG.md +116 -0
- {structboost-0.2.0 → structboost-0.4.0}/CITATION.cff +2 -2
- {structboost-0.2.0 → structboost-0.4.0}/PKG-INFO +3 -3
- {structboost-0.2.0 → structboost-0.4.0}/README.md +1 -1
- {structboost-0.2.0 → structboost-0.4.0}/pyproject.toml +1 -1
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/_decoder.py +0 -6
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/_model.py +174 -464
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/_persistence.py +12 -6
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/_types.py +29 -46
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/_utils.py +10 -12
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_bae.py +66 -105
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_bae_diagnostics.py +1 -9
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_bae_layer.py +1 -1
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_bae_persistence.py +0 -13
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_bae_transfer.py +4 -12
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_covcache_precompute.py +1 -1
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_rng_isolation.py +1 -1
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_stability.py +114 -148
- {structboost-0.2.0 → structboost-0.4.0}/.gitignore +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/CODE_OF_CONDUCT.md +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/CONTRIBUTING.md +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/LICENSE +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/conftest.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/__init__.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/_annotation.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/_boosting.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/_encoder.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/_explorer.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/_io.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/_plotting.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/_simulation.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/_stability.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/src/structboost/py.typed +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_allboost.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_annotation.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_bae_init.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_batch_integration.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_explorer.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_linear_ceiling.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_obs_encoding.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_public_api.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_resolve_mandatory.py +0 -0
- {structboost-0.2.0 → structboost-0.4.0}/tests/test_simulation.py +0 -0
|
@@ -3,6 +3,122 @@
|
|
|
3
3
|
Releases follow [semantic versioning](https://semver.org). While the project is
|
|
4
4
|
pre-1.0, a minor bump may break API.
|
|
5
5
|
|
|
6
|
+
### [0.4.0] - 2026-08-12
|
|
7
|
+
|
|
8
|
+
Three defaults change. No configuration fields are added or removed, and anyone
|
|
9
|
+
who sets these explicitly is unaffected.
|
|
10
|
+
|
|
11
|
+
**`enable_early_stopping` now defaults to `False`.** The criterion is a
|
|
12
|
+
convergence check being used as a quality check. There is no validation split, so
|
|
13
|
+
the training loss cannot see a model that is starting to memorize, and patience
|
|
14
|
+
fires long before the model is done: measured against ground truth it stopped at
|
|
15
|
+
iteration 90 on a dataset peaking at 259, and at 196 on a simulated scenario
|
|
16
|
+
peaking at 560, returning marker-recovery F1 0.502 against 0.787. Across three
|
|
17
|
+
real datasets (mouse cortex, human pancreas, human immune) the best iteration
|
|
18
|
+
ranged from 154 to 1975 — always past where patience fires.
|
|
19
|
+
|
|
20
|
+
**No stopping rule replaced it, deliberately.** Latent stability, encoder-support
|
|
21
|
+
overlap and held-out reconstruction were each measured as candidates and each
|
|
22
|
+
rejected. The representation settles long before gene selection does — on one
|
|
23
|
+
dataset consecutive latent codes were rank-identical while the gene set still
|
|
24
|
+
turned over 65% cumulatively — and no observable signal tracks the quality peak.
|
|
25
|
+
A latent-stability rule was built and tuned; it fired at iteration ~109 on all
|
|
26
|
+
three real datasets regardless of where quality peaked, and on one of them it was
|
|
27
|
+
worse than not stopping at all. It is not shipped, not even off by default: an
|
|
28
|
+
option that should never be enabled is pure carrying cost, which is the same
|
|
29
|
+
argument that removed four fields in 0.3.0. `max_iterations=1000` is a defensible
|
|
30
|
+
middle of the measured range, not an optimum.
|
|
31
|
+
|
|
32
|
+
**`BAE.stability_selection(threshold=...)` now defaults to `0.5`, from `0.7`.**
|
|
33
|
+
0.7 is too aggressive whenever the latent representation is still moving: across
|
|
34
|
+
three real datasets it removed 21-52% of recovered marker genes relative to the
|
|
35
|
+
fitted encoder, and 0-7% even when the representation had settled. At 0.5 the
|
|
36
|
+
worst loss over the same six runs was 6%. The standalone
|
|
37
|
+
`structboost.stability_selection` keeps 0.7, because its Meinshausen-Buhlmann
|
|
38
|
+
bound is undefined at or below 0.5.
|
|
39
|
+
|
|
40
|
+
**The `dim_match_quality` warning now fires below 0.85, from 0.5.** It is the gate
|
|
41
|
+
on whether iteration frequencies mean anything: they describe gene-set drift only
|
|
42
|
+
if the counted iterations describe one representation. Runs sitting at 0.70-0.79 —
|
|
43
|
+
comfortably above the old warning — already lost a quarter to a half of their
|
|
44
|
+
recovered markers at the default threshold, while runs at 0.93 and above lost
|
|
45
|
+
none. The guide now documents it as a lookup: at least 0.93, either threshold is
|
|
46
|
+
safe; 0.70-0.79, use 0.3-0.5 and prefer the flat union.
|
|
47
|
+
|
|
48
|
+
Also fixed: the gene-selection guide still showed
|
|
49
|
+
`fit(adata, stability_selection="iteration")`, which stopped being valid in 0.3.0
|
|
50
|
+
when that argument became a bool.
|
|
51
|
+
|
|
52
|
+
### [0.3.0] - 2026-08-11
|
|
53
|
+
|
|
54
|
+
**Breaking.** Five settings are gone and `BAE.stability_selection` has one mode
|
|
55
|
+
instead of two. Every removed option was off by default, so a fit that took the
|
|
56
|
+
defaults is unaffected: the encoder matrix is bitwise identical across a plain
|
|
57
|
+
fit, a batch-integrated fit, and both disentanglement methods. What changes is the
|
|
58
|
+
surface you have to reason about.
|
|
59
|
+
|
|
60
|
+
`boosting_nu` stays at `0.1`. Raising it to `0.3` was measured and deferred: at
|
|
61
|
+
the default `stepno=50` it matches `0.1`'s marker recovery three to four times
|
|
62
|
+
faster and wins outright on low-signal data, but then degrades if training
|
|
63
|
+
continues, and the training-MSE stopping rule cannot see it happening. The guide
|
|
64
|
+
records the numbers; the two changes belong together and will land together.
|
|
65
|
+
|
|
66
|
+
**Checkpoints written by 0.2.0 no longer load.** `restore_payload` splats the
|
|
67
|
+
stored config into `BAEConfig`, so a dropped field is an unexpected keyword
|
|
68
|
+
argument rather than a missing one. The format is bumped to 5 and the loader
|
|
69
|
+
refuses 4 by name. No migration is written: the method is under active
|
|
70
|
+
development and a checkpoint is cheap to regenerate, whereas a compatibility
|
|
71
|
+
shim for options that no longer exist is not.
|
|
72
|
+
|
|
73
|
+
Removed from `BAEConfig`:
|
|
74
|
+
|
|
75
|
+
- `decoder_dropout_rate` and `decoder_use_batch_norm`. Batch norm was already
|
|
76
|
+
documented as harmful (marker-recovery F1 0.59-0.73) because the boosting
|
|
77
|
+
target is computed with the decoder in eval mode and the update applied in
|
|
78
|
+
train mode. Dropout has exactly the same inconsistency and it was never written
|
|
79
|
+
down: the target comes from the full network, the update from a thinned one.
|
|
80
|
+
With both gone the decoder is a deterministic per-cell function, which is the
|
|
81
|
+
property `_compute_boosting_targets` has always relied on.
|
|
82
|
+
- `disentanglement_standardize`. Both `disentanglement` methods stay.
|
|
83
|
+
- `standardize_targets`. It was a genuine trade-off — higher selection precision,
|
|
84
|
+
roughly half the recall — but it is superseded by
|
|
85
|
+
`stability_selection(threshold=...)` -> `stable_encoder()` -> `apply_encoder()`,
|
|
86
|
+
which is the same trade with a dial that reports what it is doing. Two knobs for
|
|
87
|
+
one trade-off is worse than one.
|
|
88
|
+
|
|
89
|
+
Removed from `fit`:
|
|
90
|
+
|
|
91
|
+
- `balance_obs`, and with it the `sample_weights` parameter that threaded through
|
|
92
|
+
nine methods and both stability paths. Its own documentation conceded the
|
|
93
|
+
limit: the weights never reached the `allboost` fit, so gene selection stayed
|
|
94
|
+
unbalanced no matter what they were set to. The measured effect was modest (per
|
|
95
|
+
group reconstruction-MSE spread 0.231 -> 0.150) for a mechanism that promised
|
|
96
|
+
more than it delivered.
|
|
97
|
+
|
|
98
|
+
`BAE.stability_selection` is now iteration mode only; `mode`, `subsample_frac`,
|
|
99
|
+
`n_subsamples` and `n_iterations` are gone, and `fit(stability_selection=...)`
|
|
100
|
+
takes a bool. Iteration mode was already the default and measures the lower
|
|
101
|
+
false-discovery rate (0.26 against 0.31). The subsample path stays available as
|
|
102
|
+
the standalone `structboost.stability_selection`, which is where it belongs: it
|
|
103
|
+
resamples cells in the Meinshausen-Buhlmann scheme and works on an `allboost`
|
|
104
|
+
problem, so supervised users with no training loop to iterate over still have it.
|
|
105
|
+
The Meinshausen-Buhlmann bound it reports was measured to be violated by roughly
|
|
106
|
+
an order of magnitude when the targets come from a model fitted on the same
|
|
107
|
+
cells, and that warning moved with it.
|
|
108
|
+
|
|
109
|
+
`uns["bae"]["variance_explained"]` is now written by every fit. It used to appear
|
|
110
|
+
only when a covariate argument was passed, which left the quality workflow the
|
|
111
|
+
guide documents — compare it against `linear_ceiling` — raising `KeyError` on a
|
|
112
|
+
plain `fit(adata)`. The metric has nothing to do with covariates; only the
|
|
113
|
+
per-group breakdown does, and that stays behind the covariate guard.
|
|
114
|
+
|
|
115
|
+
Two fixes found along the way: the `allboost` example in the guide passed
|
|
116
|
+
`mode="standard"`, an argument `allboost` has never accepted, so it raised
|
|
117
|
+
`TypeError` as written. And `disentangle_boosting_targets` projects through the
|
|
118
|
+
origin, with no intercept — exact residualization only on centered targets. That
|
|
119
|
+
was masked by `disentanglement_standardize`, which centered them; with the flag
|
|
120
|
+
gone the assumption is documented instead.
|
|
121
|
+
|
|
6
122
|
### [0.2.0] - 2026-08-05
|
|
7
123
|
|
|
8
124
|
The boosting loop got faster without changing what it computes. Measured
|
|
@@ -2,8 +2,8 @@ cff-version: 1.2.0
|
|
|
2
2
|
message: "If you use structboost in your research, please cite it."
|
|
3
3
|
title: "structboost"
|
|
4
4
|
type: software
|
|
5
|
-
version: "0.
|
|
6
|
-
date-released: "2026-08-
|
|
5
|
+
version: "0.4.0"
|
|
6
|
+
date-released: "2026-08-11"
|
|
7
7
|
license: MIT
|
|
8
8
|
authors:
|
|
9
9
|
- family-names: Brunn
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: structboost
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Boosting Autoencoders (BAE) and componentwise L2 boosting utilities for scverse-style workflows.
|
|
5
5
|
Project-URL: Homepage, https://github.com/NiklasBrunn/structboost
|
|
6
6
|
Project-URL: Documentation, https://niklasbrunn.github.io/structboost
|
|
@@ -153,7 +153,7 @@ is not identifiable: many different sparse gene sets reconstruct the data about
|
|
|
153
153
|
equally well, and a fit returns one of them.
|
|
154
154
|
|
|
155
155
|
```python
|
|
156
|
-
res = model.stability_selection(adata
|
|
156
|
+
res = model.stability_selection(adata)
|
|
157
157
|
genes = [adata.var_names[res.stable_support[:, j]] for j in range(res.frequency.shape[1])]
|
|
158
158
|
```
|
|
159
159
|
|
|
@@ -78,7 +78,7 @@ is not identifiable: many different sparse gene sets reconstruct the data about
|
|
|
78
78
|
equally well, and a fit returns one of them.
|
|
79
79
|
|
|
80
80
|
```python
|
|
81
|
-
res = model.stability_selection(adata
|
|
81
|
+
res = model.stability_selection(adata)
|
|
82
82
|
genes = [adata.var_names[res.stable_support[:, j]] for j in range(res.frequency.shape[1])]
|
|
83
83
|
```
|
|
84
84
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "structboost"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Boosting Autoencoders (BAE) and componentwise L2 boosting utilities for scverse-style workflows."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -55,11 +55,7 @@ class BAEDecoder(nn.Module):
|
|
|
55
55
|
|
|
56
56
|
for i in range(len(dims) - 1):
|
|
57
57
|
layers.append(nn.Linear(dims[i], dims[i + 1]))
|
|
58
|
-
if config.decoder_use_batch_norm:
|
|
59
|
-
layers.append(nn.BatchNorm1d(dims[i + 1]))
|
|
60
58
|
layers.append(self.activation)
|
|
61
|
-
if config.decoder_dropout_rate > 0:
|
|
62
|
-
layers.append(nn.Dropout(config.decoder_dropout_rate))
|
|
63
59
|
|
|
64
60
|
self.hidden = nn.Sequential(*layers)
|
|
65
61
|
# Final projection to output space
|
|
@@ -83,8 +79,6 @@ class BAEDecoder(nn.Module):
|
|
|
83
79
|
for module in self.modules():
|
|
84
80
|
if isinstance(module, nn.Linear):
|
|
85
81
|
module.reset_parameters()
|
|
86
|
-
elif isinstance(module, nn.BatchNorm1d):
|
|
87
|
-
module.reset_parameters()
|
|
88
82
|
|
|
89
83
|
def forward(self, z: torch.Tensor, covariates: torch.Tensor | None = None) -> torch.Tensor:
|
|
90
84
|
"""Decode latent representation to reconstruction.
|