structboost 0.5.0__tar.gz → 0.6.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.
Files changed (45) hide show
  1. {structboost-0.5.0 → structboost-0.6.0}/CHANGELOG.md +94 -0
  2. {structboost-0.5.0 → structboost-0.6.0}/PKG-INFO +5 -1
  3. {structboost-0.5.0 → structboost-0.6.0}/README.md +1 -0
  4. {structboost-0.5.0 → structboost-0.6.0}/pyproject.toml +11 -1
  5. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/__init__.py +13 -0
  6. structboost-0.6.0/src/structboost/_plotting.py +1608 -0
  7. structboost-0.6.0/tests/test_plotting_dimensions.py +506 -0
  8. structboost-0.5.0/src/structboost/_plotting.py +0 -301
  9. {structboost-0.5.0 → structboost-0.6.0}/.gitignore +0 -0
  10. {structboost-0.5.0 → structboost-0.6.0}/CITATION.cff +0 -0
  11. {structboost-0.5.0 → structboost-0.6.0}/CODE_OF_CONDUCT.md +0 -0
  12. {structboost-0.5.0 → structboost-0.6.0}/CONTRIBUTING.md +0 -0
  13. {structboost-0.5.0 → structboost-0.6.0}/LICENSE +0 -0
  14. {structboost-0.5.0 → structboost-0.6.0}/conftest.py +0 -0
  15. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/_annotation.py +0 -0
  16. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/_boosting.py +0 -0
  17. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/_decoder.py +0 -0
  18. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/_encoder.py +0 -0
  19. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/_explorer.py +0 -0
  20. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/_io.py +0 -0
  21. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/_model.py +0 -0
  22. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/_persistence.py +0 -0
  23. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/_simulation.py +0 -0
  24. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/_stability.py +0 -0
  25. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/_types.py +0 -0
  26. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/_utils.py +0 -0
  27. {structboost-0.5.0 → structboost-0.6.0}/src/structboost/py.typed +0 -0
  28. {structboost-0.5.0 → structboost-0.6.0}/tests/test_allboost.py +0 -0
  29. {structboost-0.5.0 → structboost-0.6.0}/tests/test_annotation.py +0 -0
  30. {structboost-0.5.0 → structboost-0.6.0}/tests/test_bae.py +0 -0
  31. {structboost-0.5.0 → structboost-0.6.0}/tests/test_bae_diagnostics.py +0 -0
  32. {structboost-0.5.0 → structboost-0.6.0}/tests/test_bae_init.py +0 -0
  33. {structboost-0.5.0 → structboost-0.6.0}/tests/test_bae_layer.py +0 -0
  34. {structboost-0.5.0 → structboost-0.6.0}/tests/test_bae_persistence.py +0 -0
  35. {structboost-0.5.0 → structboost-0.6.0}/tests/test_bae_transfer.py +0 -0
  36. {structboost-0.5.0 → structboost-0.6.0}/tests/test_batch_integration.py +0 -0
  37. {structboost-0.5.0 → structboost-0.6.0}/tests/test_covcache_precompute.py +0 -0
  38. {structboost-0.5.0 → structboost-0.6.0}/tests/test_explorer.py +0 -0
  39. {structboost-0.5.0 → structboost-0.6.0}/tests/test_linear_ceiling.py +0 -0
  40. {structboost-0.5.0 → structboost-0.6.0}/tests/test_obs_encoding.py +0 -0
  41. {structboost-0.5.0 → structboost-0.6.0}/tests/test_public_api.py +0 -0
  42. {structboost-0.5.0 → structboost-0.6.0}/tests/test_resolve_mandatory.py +0 -0
  43. {structboost-0.5.0 → structboost-0.6.0}/tests/test_rng_isolation.py +0 -0
  44. {structboost-0.5.0 → structboost-0.6.0}/tests/test_simulation.py +0 -0
  45. {structboost-0.5.0 → structboost-0.6.0}/tests/test_stability.py +0 -0
@@ -3,6 +3,100 @@
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.0] - 2026-09-09
7
+
8
+ **Not breaking.** Nothing existing changes behaviour; this adds a way to read a
9
+ fitted model.
10
+
11
+ **New `plot_latent_dimensions`: one row per latent dimension, up to three
12
+ panels.** The sorted score curve, the per-gene contribution distributions, and
13
+ the scores split by a grouping you name. It exists because a fitted BAE has been
14
+ readable in principle — the encoder *is* the gene list — and awkward in practice:
15
+ `extract_gene_rankings` gives ranked lists, `export_interactive_html` gives an
16
+ explorer, and neither says whether a dimension describes a subgroup or a
17
+ gradient, nor which cells sit at which end.
18
+
19
+ **New `plot_dimension_gene_umaps`: a UMAP grid, dimensions by top genes**, drawn
20
+ through `scanpy.pl.umap` so the panels match a scanpy figure. It answers the one
21
+ question the other panels structurally cannot — whether a dimension's genes light
22
+ up the *same* cells — because both the score curve and the contribution violins
23
+ have already summed over cells. On real data it separates a coherent programme
24
+ from a dimension summing unrelated signals.
25
+
26
+ **A `"shares"` panel and a new `plot_dimension_correlation`.** The contribution
27
+ panel shows a dimension's top handful of genes; the shares panel plots *every*
28
+ selected gene's share, sorted, which is the only view that says whether a
29
+ dimension rests on three genes or spreads evenly over forty — both legitimate,
30
+ and read differently. Ordering it by `rank_by="weight"` while the axis stays on
31
+ the share draws the disagreement between the two rankings directly.
32
+
33
+ The correlation heatmap answers a question no per-dimension panel can: whether
34
+ two dimensions are near-duplicates, and therefore whether they can be read as two
35
+ findings or one. Pearson by default, Spearman on tie-averaged ranks as an option —
36
+ ties are not incidental here, since a sparse encoder leaves many cells at exactly
37
+ one value on a subgroup dimension. Signed values get the diverging map centred on
38
+ zero; `absolute=True` plots `|r|` with a sequential one, since a magnitude has no
39
+ midpoint.
40
+
41
+ **Genes are ranked by variance share, not `|weight|`,** exposed as the new
42
+ `gene_variance_shares`. From `Var(s) = Cov(s, s)` with `s = X @ w` it follows
43
+ that `w_g Cov(X_g, s) / Var(s)` decomposes a dimension's variance exactly and
44
+ sums to 1, needing no orthogonality assumption and splitting credit correctly
45
+ between correlated genes. The weight alone ignores how much a gene actually
46
+ varies: measured on 59k PBMCs the two rankings agree on 8.6 of 10 genes per
47
+ dimension, and where they differ `|w|` promotes genes that barely move — SCT
48
+ ranked #4 by weight and #35 of 38 by share, being near-absent in that tissue.
49
+ The share is a **magnitude**: because a negative-weight gene is anti-correlated
50
+ with the score, the product is positive either way and only ~1% of genes come out
51
+ negative. Direction is therefore the weight's sign, carried by the colour of the
52
+ gene name, and the share prints without one.
53
+
54
+ **The score panel paints in a seeded random order.** Markers are wider than the
55
+ spacing between adjacent ranks — about 112 cells overlap at a given x on 59k —
56
+ so whatever is drawn last wins every overlap. Painting in score order hands each
57
+ contest to the higher-scoring cell, and since score correlates with group, one
58
+ group systematically covers its neighbours. Every point keeps its own
59
+ `(rank, score)`, so the curve is unchanged and only the paint order is permuted.
60
+ This is the *opposite* choice from a UMAP panel, where rare groups are drawn last
61
+ on purpose: there position is data and overlap is unavoidable, here every cell
62
+ has its own x and any deterministic order is a bias.
63
+
64
+ **Seven hues, then grey, and the palette is measured rather than chosen** — see
65
+ the new `palette_audit`. The package's existing Okabe-Ito set separates colours
66
+ from each other under colour-blindness but does not hold contrast against white
67
+ at small mark sizes: its `#F0E442` measures 1.32 WCAG contrast against a floor of
68
+ 3.0, so a cell type holding 5.9% of cells was invisible as 8pt dots while its
69
+ violin read perfectly well. **Contrast requirements scale with mark size, so a
70
+ palette validated on one mark is not validated on another.** At twelve hues
71
+ nothing separates anyway — 1.5 ΔE under simulated deuteranopia against a target
72
+ of 8; Kelly's "20 colours of maximum contrast" reaches 0.4, scanpy's `default_20`
73
+ 0.5. Twenty reliably distinguishable hues do not exist. The default therefore
74
+ follows the group count (`tab10`'s first five, then `tab10`, then scanpy's
75
+ `default_20`), groups past the palette are grey, and they lose nothing because
76
+ the violin panel labels every row — the score panel, which has no labels, is
77
+ where the cap binds.
78
+
79
+ **Signed quantities get a diverging map centred on zero; sequential is for
80
+ magnitudes.** The score always, and the gene panels as soon as `scale="zscore"`,
81
+ where a sequential map would put its neutral colour at the data mean rather than
82
+ at zero. The encoder is bias-free, so zero is a real level — no selected gene
83
+ departs from its mean in that cell — and the point at which a dimension's sign
84
+ flips.
85
+
86
+ `scanpy` becomes a new optional extra, needed only by `plot_dimension_gene_umaps`
87
+ and imported inside it. Absent, that one function raises naming
88
+ `plot_latent_dimensions` as the matplotlib-only alternative; everything else is
89
+ unaffected.
90
+
91
+ Two things are deliberately *not* offered. `c_ig / score_ik`, the intuitive
92
+ "fraction of this cell's score", is unusable: the flat plateau of a subgroup
93
+ dimension is exactly the cells whose score is ≈0, so the ratio diverges (measured
94
+ −8,921 to +1,191 for one gene), flips sign with the score, and inflates without
95
+ bound wherever contributions cancel — `normalize="cell"` divides by total
96
+ *absolute* contribution instead, which is bounded. And nothing here is
97
+ inferential: every number is an exact algebraic decomposition or a descriptive
98
+ statistic.
99
+
6
100
  ### [0.5.0] - 2026-08-21
7
101
 
8
102
  **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.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
@@ -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.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" }
@@ -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
  }