structboost 0.5.0__tar.gz → 0.6.1__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.
Files changed (46) hide show
  1. {structboost-0.5.0 → structboost-0.6.1}/CHANGELOG.md +122 -0
  2. {structboost-0.5.0 → structboost-0.6.1}/PKG-INFO +5 -1
  3. {structboost-0.5.0 → structboost-0.6.1}/README.md +1 -0
  4. {structboost-0.5.0 → structboost-0.6.1}/pyproject.toml +11 -1
  5. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/__init__.py +13 -0
  6. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/_model.py +183 -93
  7. structboost-0.6.1/src/structboost/_plotting.py +1608 -0
  8. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/_utils.py +87 -1
  9. structboost-0.6.1/tests/test_densify.py +236 -0
  10. structboost-0.6.1/tests/test_plotting_dimensions.py +506 -0
  11. structboost-0.5.0/src/structboost/_plotting.py +0 -301
  12. {structboost-0.5.0 → structboost-0.6.1}/.gitignore +0 -0
  13. {structboost-0.5.0 → structboost-0.6.1}/CITATION.cff +0 -0
  14. {structboost-0.5.0 → structboost-0.6.1}/CODE_OF_CONDUCT.md +0 -0
  15. {structboost-0.5.0 → structboost-0.6.1}/CONTRIBUTING.md +0 -0
  16. {structboost-0.5.0 → structboost-0.6.1}/LICENSE +0 -0
  17. {structboost-0.5.0 → structboost-0.6.1}/conftest.py +0 -0
  18. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/_annotation.py +0 -0
  19. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/_boosting.py +0 -0
  20. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/_decoder.py +0 -0
  21. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/_encoder.py +0 -0
  22. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/_explorer.py +0 -0
  23. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/_io.py +0 -0
  24. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/_persistence.py +0 -0
  25. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/_simulation.py +0 -0
  26. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/_stability.py +0 -0
  27. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/_types.py +0 -0
  28. {structboost-0.5.0 → structboost-0.6.1}/src/structboost/py.typed +0 -0
  29. {structboost-0.5.0 → structboost-0.6.1}/tests/test_allboost.py +0 -0
  30. {structboost-0.5.0 → structboost-0.6.1}/tests/test_annotation.py +0 -0
  31. {structboost-0.5.0 → structboost-0.6.1}/tests/test_bae.py +0 -0
  32. {structboost-0.5.0 → structboost-0.6.1}/tests/test_bae_diagnostics.py +0 -0
  33. {structboost-0.5.0 → structboost-0.6.1}/tests/test_bae_init.py +0 -0
  34. {structboost-0.5.0 → structboost-0.6.1}/tests/test_bae_layer.py +0 -0
  35. {structboost-0.5.0 → structboost-0.6.1}/tests/test_bae_persistence.py +0 -0
  36. {structboost-0.5.0 → structboost-0.6.1}/tests/test_bae_transfer.py +0 -0
  37. {structboost-0.5.0 → structboost-0.6.1}/tests/test_batch_integration.py +0 -0
  38. {structboost-0.5.0 → structboost-0.6.1}/tests/test_covcache_precompute.py +0 -0
  39. {structboost-0.5.0 → structboost-0.6.1}/tests/test_explorer.py +0 -0
  40. {structboost-0.5.0 → structboost-0.6.1}/tests/test_linear_ceiling.py +0 -0
  41. {structboost-0.5.0 → structboost-0.6.1}/tests/test_obs_encoding.py +0 -0
  42. {structboost-0.5.0 → structboost-0.6.1}/tests/test_public_api.py +0 -0
  43. {structboost-0.5.0 → structboost-0.6.1}/tests/test_resolve_mandatory.py +0 -0
  44. {structboost-0.5.0 → structboost-0.6.1}/tests/test_rng_isolation.py +0 -0
  45. {structboost-0.5.0 → structboost-0.6.1}/tests/test_simulation.py +0 -0
  46. {structboost-0.5.0 → structboost-0.6.1}/tests/test_stability.py +0 -0
@@ -3,6 +3,128 @@
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.6.1] - 2026-09-24
7
+
8
+ **Not breaking.** CSR and dense input give bit-identical fits; CSC input
9
+ changes in the last bits (below).
10
+
11
+ **Fits need less memory and run faster.** Measured at 300,000 cells by 2,000
12
+ genes: peak memory 10.4 GB to 8.3 GB, wall clock 44.8 s to 25.9 s. Three copies
13
+ of the expression matrix are gone:
14
+
15
+ - The sparse matrix is densified in row blocks straight into float32, instead of
16
+ through a full-size intermediate in its source dtype.
17
+ - `fit` hands the training matrix to the step that writes `adata.obsm["X_bae"]`
18
+ instead of densifying `adata.X` a second time.
19
+ - With batch integration on (`batch_integration_mode` `"encoder"` or `"both"`),
20
+ the boosting design is allocated once with room for the covariate columns,
21
+ instead of copying the whole panel with `np.hstack` to append them. Batch
22
+ integration now costs about the same peak memory as a fit without it, and
23
+ 400,000 cells fit where they ran out of memory before.
24
+
25
+ A float32 `adata.X` is now trained on in place rather than copied. A fit never
26
+ writes to it; a read-only array is still copied.
27
+
28
+ **CSC input now gives the same fit as CSR.** `csc.toarray()` returns a
29
+ Fortran-ordered array, which changed the BLAS reduction order and with it the
30
+ fitted weights (2.3e-9 on a 12-iteration fit). The densified matrix is now
31
+ row-major whatever the input, so a CSC-backed fit will not reproduce its earlier
32
+ result, and matches the CSR result for the same data instead.
33
+
34
+ ### [0.6.0] - 2026-09-09
35
+
36
+ **Not breaking.** Nothing existing changes behaviour; this adds a way to read a
37
+ fitted model.
38
+
39
+ **New `plot_latent_dimensions`: one row per latent dimension, up to three
40
+ panels.** The sorted score curve, the per-gene contribution distributions, and
41
+ the scores split by a grouping you name. It exists because a fitted BAE has been
42
+ readable in principle — the encoder *is* the gene list — and awkward in practice:
43
+ `extract_gene_rankings` gives ranked lists, `export_interactive_html` gives an
44
+ explorer, and neither says whether a dimension describes a subgroup or a
45
+ gradient, nor which cells sit at which end.
46
+
47
+ **New `plot_dimension_gene_umaps`: a UMAP grid, dimensions by top genes**, drawn
48
+ through `scanpy.pl.umap` so the panels match a scanpy figure. It answers the one
49
+ question the other panels structurally cannot — whether a dimension's genes light
50
+ up the *same* cells — because both the score curve and the contribution violins
51
+ have already summed over cells. On real data it separates a coherent programme
52
+ from a dimension summing unrelated signals.
53
+
54
+ **A `"shares"` panel and a new `plot_dimension_correlation`.** The contribution
55
+ panel shows a dimension's top handful of genes; the shares panel plots *every*
56
+ selected gene's share, sorted, which is the only view that says whether a
57
+ dimension rests on three genes or spreads evenly over forty — both legitimate,
58
+ and read differently. Ordering it by `rank_by="weight"` while the axis stays on
59
+ the share draws the disagreement between the two rankings directly.
60
+
61
+ The correlation heatmap answers a question no per-dimension panel can: whether
62
+ two dimensions are near-duplicates, and therefore whether they can be read as two
63
+ findings or one. Pearson by default, Spearman on tie-averaged ranks as an option —
64
+ ties are not incidental here, since a sparse encoder leaves many cells at exactly
65
+ one value on a subgroup dimension. Signed values get the diverging map centred on
66
+ zero; `absolute=True` plots `|r|` with a sequential one, since a magnitude has no
67
+ midpoint.
68
+
69
+ **Genes are ranked by variance share, not `|weight|`,** exposed as the new
70
+ `gene_variance_shares`. From `Var(s) = Cov(s, s)` with `s = X @ w` it follows
71
+ that `w_g Cov(X_g, s) / Var(s)` decomposes a dimension's variance exactly and
72
+ sums to 1, needing no orthogonality assumption and splitting credit correctly
73
+ between correlated genes. The weight alone ignores how much a gene actually
74
+ varies: measured on 59k PBMCs the two rankings agree on 8.6 of 10 genes per
75
+ dimension, and where they differ `|w|` promotes genes that barely move — SCT
76
+ ranked #4 by weight and #35 of 38 by share, being near-absent in that tissue.
77
+ The share is a **magnitude**: because a negative-weight gene is anti-correlated
78
+ with the score, the product is positive either way and only ~1% of genes come out
79
+ negative. Direction is therefore the weight's sign, carried by the colour of the
80
+ gene name, and the share prints without one.
81
+
82
+ **The score panel paints in a seeded random order.** Markers are wider than the
83
+ spacing between adjacent ranks — about 112 cells overlap at a given x on 59k —
84
+ so whatever is drawn last wins every overlap. Painting in score order hands each
85
+ contest to the higher-scoring cell, and since score correlates with group, one
86
+ group systematically covers its neighbours. Every point keeps its own
87
+ `(rank, score)`, so the curve is unchanged and only the paint order is permuted.
88
+ This is the *opposite* choice from a UMAP panel, where rare groups are drawn last
89
+ on purpose: there position is data and overlap is unavoidable, here every cell
90
+ has its own x and any deterministic order is a bias.
91
+
92
+ **Seven hues, then grey, and the palette is measured rather than chosen** — see
93
+ the new `palette_audit`. The package's existing Okabe-Ito set separates colours
94
+ from each other under colour-blindness but does not hold contrast against white
95
+ at small mark sizes: its `#F0E442` measures 1.32 WCAG contrast against a floor of
96
+ 3.0, so a cell type holding 5.9% of cells was invisible as 8pt dots while its
97
+ violin read perfectly well. **Contrast requirements scale with mark size, so a
98
+ palette validated on one mark is not validated on another.** At twelve hues
99
+ nothing separates anyway — 1.5 ΔE under simulated deuteranopia against a target
100
+ of 8; Kelly's "20 colours of maximum contrast" reaches 0.4, scanpy's `default_20`
101
+ 0.5. Twenty reliably distinguishable hues do not exist. The default therefore
102
+ follows the group count (`tab10`'s first five, then `tab10`, then scanpy's
103
+ `default_20`), groups past the palette are grey, and they lose nothing because
104
+ the violin panel labels every row — the score panel, which has no labels, is
105
+ where the cap binds.
106
+
107
+ **Signed quantities get a diverging map centred on zero; sequential is for
108
+ magnitudes.** The score always, and the gene panels as soon as `scale="zscore"`,
109
+ where a sequential map would put its neutral colour at the data mean rather than
110
+ at zero. The encoder is bias-free, so zero is a real level — no selected gene
111
+ departs from its mean in that cell — and the point at which a dimension's sign
112
+ flips.
113
+
114
+ `scanpy` becomes a new optional extra, needed only by `plot_dimension_gene_umaps`
115
+ and imported inside it. Absent, that one function raises naming
116
+ `plot_latent_dimensions` as the matplotlib-only alternative; everything else is
117
+ unaffected.
118
+
119
+ Two things are deliberately *not* offered. `c_ig / score_ik`, the intuitive
120
+ "fraction of this cell's score", is unusable: the flat plateau of a subgroup
121
+ dimension is exactly the cells whose score is ≈0, so the ratio diverges (measured
122
+ −8,921 to +1,191 for one gene), flips sign with the score, and inflates without
123
+ bound wherever contributions cancel — `normalize="cell"` divides by total
124
+ *absolute* contribution instead, which is bounded. And nothing here is
125
+ inferential: every number is an exact algebraic decomposition or a descriptive
126
+ statistic.
127
+
6
128
  ### [0.5.0] - 2026-08-21
7
129
 
8
130
  **Breaking**, in two parts: the decoder update became one pass over the cells,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: structboost
3
- Version: 0.5.0
3
+ Version: 0.6.1
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
@@ -64,6 +64,9 @@ Requires-Dist: pandas>=1.5; extra == 'io'
64
64
  Requires-Dist: pyarrow>=12; extra == 'io'
65
65
  Provides-Extra: plot
66
66
  Requires-Dist: matplotlib>=3.7; extra == 'plot'
67
+ Provides-Extra: scanpy
68
+ Requires-Dist: matplotlib>=3.7; extra == 'scanpy'
69
+ Requires-Dist: scanpy>=1.9; extra == 'scanpy'
67
70
  Provides-Extra: test
68
71
  Requires-Dist: anndata>=0.10; extra == 'test'
69
72
  Requires-Dist: pandas>=1.5; extra == 'test'
@@ -187,6 +190,7 @@ Each of these has a guide page.
187
190
  | --- | --- |
188
191
  | [Batch integration](https://niklasbrunn.github.io/structboost/guide/tasks/batch-integration.html) | `batch_key` names the covariate and `batch_integration_mode` chooses whether it conditions the decoder, protects gene selection, or both. `transform` stays gene-only and needs no batch labels. |
189
192
  | [Mandatory features](https://niklasbrunn.github.io/structboost/guide/tasks/gene-selection.html) | `mandatory_genes` puts known markers in boosting's unpenalized adjustment block, so they are never subject to competitive selection. Flat or per latent dimension. It forces them into the *specification*, not into the fitted support. |
193
+ | [Reading a fit](https://niklasbrunn.github.io/structboost/guide/tasks/reading-dimensions.html) | `plot_latent_dimensions` draws one row per latent dimension — the sorted score curve, the per-gene contributions, and the scores split by any grouping — plus a UMAP grid and a dimension-correlation heatmap. Genes are ranked by their exact share of a dimension's variance, not by coefficient size. |
190
194
  | [Persistence](https://niklasbrunn.github.io/structboost/guide/tasks/persistence.html) | `save` and `load` a fitted model as one checkpoint, readable with `weights_only=True`. |
191
195
  | [Interpretation](https://niklasbrunn.github.io/structboost/guide/tasks/interpreting.html) | Ranked gene lists per dimension, stored functional annotations, and a self-contained interactive HTML explorer. |
192
196
  | [Simulation](https://niklasbrunn.github.io/structboost/guide/tasks/simulating.html) | Negative-binomial counts with planted gene programs and a cell-type hierarchy, so marker recovery can be scored against ground truth. |
@@ -112,6 +112,7 @@ Each of these has a guide page.
112
112
  | --- | --- |
113
113
  | [Batch integration](https://niklasbrunn.github.io/structboost/guide/tasks/batch-integration.html) | `batch_key` names the covariate and `batch_integration_mode` chooses whether it conditions the decoder, protects gene selection, or both. `transform` stays gene-only and needs no batch labels. |
114
114
  | [Mandatory features](https://niklasbrunn.github.io/structboost/guide/tasks/gene-selection.html) | `mandatory_genes` puts known markers in boosting's unpenalized adjustment block, so they are never subject to competitive selection. Flat or per latent dimension. It forces them into the *specification*, not into the fitted support. |
115
+ | [Reading a fit](https://niklasbrunn.github.io/structboost/guide/tasks/reading-dimensions.html) | `plot_latent_dimensions` draws one row per latent dimension — the sorted score curve, the per-gene contributions, and the scores split by any grouping — plus a UMAP grid and a dimension-correlation heatmap. Genes are ranked by their exact share of a dimension's variance, not by coefficient size. |
115
116
  | [Persistence](https://niklasbrunn.github.io/structboost/guide/tasks/persistence.html) | `save` and `load` a fitted model as one checkpoint, readable with `weights_only=True`. |
116
117
  | [Interpretation](https://niklasbrunn.github.io/structboost/guide/tasks/interpreting.html) | Ranked gene lists per dimension, stored functional annotations, and a self-contained interactive HTML explorer. |
117
118
  | [Simulation](https://niklasbrunn.github.io/structboost/guide/tasks/simulating.html) | Negative-binomial counts with planted gene programs and a cell-type hierarchy, so marker recovery can be scored against ground truth. |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "structboost"
7
- version = "0.5.0"
7
+ version = "0.6.1"
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" }
@@ -41,6 +41,16 @@ bae = [
41
41
  plot = [
42
42
  "matplotlib>=3.7",
43
43
  ]
44
+ # scanpy, for `plot_dimension_gene_umaps` alone. It draws through `sc.pl.umap` so
45
+ # its panels match a scanpy figure rather than approximating one, and it is
46
+ # imported inside that function: without it the function raises naming
47
+ # `plot_latent_dimensions` as the matplotlib-only alternative, and nothing else in
48
+ # the package notices. Kept out of `plot` deliberately -- scanpy pulls a far larger
49
+ # tree than matplotlib, and the other plot functions do not need it.
50
+ scanpy = [
51
+ "scanpy>=1.9",
52
+ "structboost[plot]",
53
+ ]
44
54
  # Parquet encoder-weight files (imported lazily; CSV needs only pandas)
45
55
  io = [
46
56
  "pandas>=1.5",
@@ -56,9 +56,14 @@ __all__ = [
56
56
  "encode_obs_covariates",
57
57
  "export_interactive_html",
58
58
  "extract_gene_rankings",
59
+ "gene_variance_shares",
59
60
  "linear_ceiling",
60
61
  "looks_like_ensembl",
62
+ "palette_audit",
61
63
  "plot_boosting_coefficient_paths",
64
+ "plot_dimension_correlation",
65
+ "plot_dimension_gene_umaps",
66
+ "plot_latent_dimensions",
62
67
  "plot_top_boosting_coefficients",
63
68
  "plot_training_diagnostics",
64
69
  "read_encoder_weights",
@@ -70,9 +75,17 @@ __all__ = [
70
75
  "write_encoder_weights",
71
76
  ]
72
77
 
78
+ #: Resolved lazily through `__getattr__`. `gene_variance_shares` and
79
+ #: `palette_audit` live in `_plotting` beside their callers but need no
80
+ #: matplotlib -- that module imports it inside the drawing functions only.
73
81
  _PLOT_FUNCTIONS = frozenset(
74
82
  {
83
+ "gene_variance_shares",
84
+ "palette_audit",
75
85
  "plot_boosting_coefficient_paths",
86
+ "plot_dimension_correlation",
87
+ "plot_dimension_gene_umaps",
88
+ "plot_latent_dimensions",
76
89
  "plot_top_boosting_coefficients",
77
90
  "plot_training_diagnostics",
78
91
  }