rafinat 0.2.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.
- rafinat-0.2.0/PKG-INFO +132 -0
- rafinat-0.2.0/README.md +102 -0
- rafinat-0.2.0/pyproject.toml +51 -0
- rafinat-0.2.0/rafinat/__init__.py +49 -0
- rafinat-0.2.0/rafinat/_base.py +104 -0
- rafinat-0.2.0/rafinat/_dm.py +313 -0
- rafinat-0.2.0/rafinat/_snr.py +82 -0
- rafinat-0.2.0/rafinat/_trigamma.py +140 -0
- rafinat-0.2.0/rafinat/compost.py +455 -0
- rafinat-0.2.0/rafinat/liou.py +239 -0
- rafinat-0.2.0/rafinat.egg-info/PKG-INFO +132 -0
- rafinat-0.2.0/rafinat.egg-info/SOURCES.txt +15 -0
- rafinat-0.2.0/rafinat.egg-info/dependency_links.txt +1 -0
- rafinat-0.2.0/rafinat.egg-info/requires.txt +15 -0
- rafinat-0.2.0/rafinat.egg-info/top_level.txt +1 -0
- rafinat-0.2.0/setup.cfg +4 -0
- rafinat-0.2.0/tests/test_rafinat.py +168 -0
rafinat-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rafinat
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Bayesian normalizations for RNA-seq
|
|
5
|
+
Author-email: Georgy Meshcheryakov <iam@georgy.top>
|
|
6
|
+
License: BSD-2-Clause
|
|
7
|
+
Keywords: single-cell,scRNA-seq,RNA-seq,normalization,bioinformatics,Dirichlet-multinomial,compositional data,CLR,Fisher-Rao,scikit-learn
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: scipy
|
|
21
|
+
Provides-Extra: rafinat
|
|
22
|
+
Requires-Dist: liudist>=0.1; extra == "rafinat"
|
|
23
|
+
Provides-Extra: gpu
|
|
24
|
+
Requires-Dist: liudist>=0.1; extra == "gpu"
|
|
25
|
+
Provides-Extra: sklearn
|
|
26
|
+
Requires-Dist: scikit-learn; extra == "sklearn"
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest; extra == "test"
|
|
29
|
+
Requires-Dist: scikit-learn; extra == "test"
|
|
30
|
+
|
|
31
|
+
# rafinat
|
|
32
|
+
|
|
33
|
+
Dirichlet-Multinomial posterior normalizations for single-cell RNA-seq count data.
|
|
34
|
+
|
|
35
|
+
Three count normalizations derived from a Dirichlet-Multinomial (Liouville) posterior view of the
|
|
36
|
+
latent expression rates.
|
|
37
|
+
|
|
38
|
+
Each is a scikit-learn-style transformer: you instantiate it with its operating point, then
|
|
39
|
+
`.fit` / `.transform` / `.fit_transform` a count matrix — so it drops into a preprocessing pipeline.
|
|
40
|
+
|
|
41
|
+
| transformer | what it is | default operating point |
|
|
42
|
+
|----------|-----------|--------------------------|
|
|
43
|
+
| `compost` | the DM-posterior generalisation of CLR | `p=0` digamma log-corner, top-2/3-trimmed per-cell centering |
|
|
44
|
+
| `hellnorm` | the Hellinger / Fisher-Rao spherical (sqrt-CLR) normalization | top-2/3-trimmed per-cell sqrt reference |
|
|
45
|
+
| `rafinat` | the complete Fisher-Rao Liouville embedding | composition arc length (sharp `r_snr`) + radial cell-size coordinate, `beta="total"` |
|
|
46
|
+
|
|
47
|
+
All take a dense `(genes, cells)` array of raw counts (genes = features/rows, cells =
|
|
48
|
+
samples/columns) and return `(genes, cells)` (`rafinat` returns `(genes + 1, cells)` — the extra
|
|
49
|
+
row is the cell-size scale coordinate). `compost` and `hellnorm` are pure `numpy`/`scipy` (CPU).
|
|
50
|
+
`rafinat` uses the optional [`liudist`](liudist/) package (Fisher-Rao geometry of Liouville laws;
|
|
51
|
+
pulls in JAX, and benefits from a GPU JAX build) — installed via the `[rafinat]` extra.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import numpy as np, rafinat
|
|
55
|
+
X = np.random.poisson(0.5, size=(2000, 500)).astype(float) # genes x cells
|
|
56
|
+
|
|
57
|
+
Z = rafinat.compost().fit_transform(X) # CLR-dominating default (compost p=0, trimmed)
|
|
58
|
+
H = rafinat.hellnorm().fit_transform(X) # Hellnorm, trimmed reference
|
|
59
|
+
R = rafinat.rafinat().fit_transform(X) # rafinat (complete); (genes + 1) x cells
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### fit / transform
|
|
63
|
+
|
|
64
|
+
`fit` estimates and **freezes** the two data-driven pieces — the concentration `r` and the per-cell
|
|
65
|
+
reference level `beta` — and `transform` re-applies that frozen normalization. Because `beta` is
|
|
66
|
+
per-cell, `transform` expects a matrix of the same `(genes, cells)` shape it was fitted on (re-fit
|
|
67
|
+
for a different gene/cell set). `fit_transform(X)` is the one-shot form.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
tr = rafinat.compost(p=0.5).fit(X) # estimate & freeze tr.r_ and tr.beta_
|
|
71
|
+
Z = tr.transform(X) # apply; == tr.fit_transform(X)
|
|
72
|
+
|
|
73
|
+
from sklearn.pipeline import Pipeline # optional — also works without scikit-learn installed
|
|
74
|
+
pipe = Pipeline([("normalize", rafinat.compost())])
|
|
75
|
+
Z = pipe.fit_transform(X)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
scikit-learn is an **optional** extra: if installed, the transformers inherit
|
|
79
|
+
`BaseEstimator` / `TransformerMixin` (full `Pipeline` / `clone` / `get_params` support); otherwise a
|
|
80
|
+
light built-in shim provides the same `fit` / `transform` / `fit_transform` / `get_params` API.
|
|
81
|
+
|
|
82
|
+
## Choosing the operating point
|
|
83
|
+
|
|
84
|
+
The benchmark-winning defaults are baked in, but every knob is a constructor argument:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
rafinat.compost(p=0.0) # default: digamma log-corner, trimmed centering
|
|
88
|
+
rafinat.compost(p=0.5) # posterior sqrt mean (order-1/2 power-mean cell size)
|
|
89
|
+
rafinat.compost(trim=0.0) # ordinary (non-trimmed) per-cell centering
|
|
90
|
+
rafinat.compost(r="mle") # pooled DM-MLE concentration instead of the CLR-matched r
|
|
91
|
+
rafinat.compost(zscore=True) # + per-gene z-score (the optional '->Z' standardization)
|
|
92
|
+
|
|
93
|
+
rafinat.hellnorm(reference="uniform") # classic log-map references: uniform / extrinsic / frechet
|
|
94
|
+
rafinat.hellnorm(trim=0.5) # lighter top-trim
|
|
95
|
+
|
|
96
|
+
rafinat.rafinat(beta="atop10") # simulation-leaning cell-size estimator
|
|
97
|
+
rafinat.rafinat(r_comp="mle") # smoother directional concentration
|
|
98
|
+
rafinat.rafinat(zcomp=True) # + per-gene z-score of the composition rows ('-> coordZ')
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Each of these returns a transformer; call `.fit_transform(X)` (or `.fit(X)` then `.transform(X)`)
|
|
102
|
+
on it. After fitting, the estimated values are exposed as fitted attributes (trailing underscore):
|
|
103
|
+
`compost.r_` / `compost.beta_`, `rafinat.r_comp_` / `rafinat.ref_` / `rafinat.C_` /
|
|
104
|
+
`rafinat.beta_`.
|
|
105
|
+
|
|
106
|
+
**The optional per-gene standardization (`compost(zscore=True)`, `rafinat(zcomp=True)`) is disabled
|
|
107
|
+
by default**, matching the benchmark's shipped defaults.
|
|
108
|
+
|
|
109
|
+
## Install
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pip install rafinat # compost / hellnorm (numpy + scipy only)
|
|
113
|
+
pip install "rafinat[rafinat]" # + the rafinat() method — adds the liudist backend (pulls in JAX)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`compost` and `hellnorm` need only `numpy`/`scipy`; the heavy `liudist` + JAX stack is pulled in
|
|
117
|
+
**only** by the `[rafinat]` extra, i.e. only if you use the `rafinat()` method.
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
## Method provenance
|
|
122
|
+
|
|
123
|
+
- **compost** — `digamma(r + x)` with `r = 1/(4·alpha) + 1/2` (so `digamma(r+x) ≈ log(x + 1/(4·alpha))`), minus a per-cell location estimated on the **low-expression bulk**
|
|
124
|
+
(the top high-expression genes — the biologically variable ones — are dropped from the cell-size
|
|
125
|
+
estimate).
|
|
126
|
+
- **hellnorm** — `sqrt(x / sum x)` minus a top-trimmed per-cell mean (the sqrt-geometry analog of
|
|
127
|
+
compost's trimmed centering). Dominates the classic uniform/extrinsic/Frechet references.
|
|
128
|
+
- **rafinat** — the Fisher-Rao Liouville distance factors as
|
|
129
|
+
`d² = d_composition² + C·(d ln β)²`; rafinat realises it as the stack
|
|
130
|
+
`[ composition arc length (depth-normalized, sharp r_snr) ; sqrt(C)·ln(β_c) ]`, with the radial
|
|
131
|
+
weight `C = genes · r_mle` decoupled from the (sharp) directional concentration so the cell-size
|
|
132
|
+
axis stays alive at a parameter-free weight.
|
rafinat-0.2.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# rafinat
|
|
2
|
+
|
|
3
|
+
Dirichlet-Multinomial posterior normalizations for single-cell RNA-seq count data.
|
|
4
|
+
|
|
5
|
+
Three count normalizations derived from a Dirichlet-Multinomial (Liouville) posterior view of the
|
|
6
|
+
latent expression rates.
|
|
7
|
+
|
|
8
|
+
Each is a scikit-learn-style transformer: you instantiate it with its operating point, then
|
|
9
|
+
`.fit` / `.transform` / `.fit_transform` a count matrix — so it drops into a preprocessing pipeline.
|
|
10
|
+
|
|
11
|
+
| transformer | what it is | default operating point |
|
|
12
|
+
|----------|-----------|--------------------------|
|
|
13
|
+
| `compost` | the DM-posterior generalisation of CLR | `p=0` digamma log-corner, top-2/3-trimmed per-cell centering |
|
|
14
|
+
| `hellnorm` | the Hellinger / Fisher-Rao spherical (sqrt-CLR) normalization | top-2/3-trimmed per-cell sqrt reference |
|
|
15
|
+
| `rafinat` | the complete Fisher-Rao Liouville embedding | composition arc length (sharp `r_snr`) + radial cell-size coordinate, `beta="total"` |
|
|
16
|
+
|
|
17
|
+
All take a dense `(genes, cells)` array of raw counts (genes = features/rows, cells =
|
|
18
|
+
samples/columns) and return `(genes, cells)` (`rafinat` returns `(genes + 1, cells)` — the extra
|
|
19
|
+
row is the cell-size scale coordinate). `compost` and `hellnorm` are pure `numpy`/`scipy` (CPU).
|
|
20
|
+
`rafinat` uses the optional [`liudist`](liudist/) package (Fisher-Rao geometry of Liouville laws;
|
|
21
|
+
pulls in JAX, and benefits from a GPU JAX build) — installed via the `[rafinat]` extra.
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import numpy as np, rafinat
|
|
25
|
+
X = np.random.poisson(0.5, size=(2000, 500)).astype(float) # genes x cells
|
|
26
|
+
|
|
27
|
+
Z = rafinat.compost().fit_transform(X) # CLR-dominating default (compost p=0, trimmed)
|
|
28
|
+
H = rafinat.hellnorm().fit_transform(X) # Hellnorm, trimmed reference
|
|
29
|
+
R = rafinat.rafinat().fit_transform(X) # rafinat (complete); (genes + 1) x cells
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### fit / transform
|
|
33
|
+
|
|
34
|
+
`fit` estimates and **freezes** the two data-driven pieces — the concentration `r` and the per-cell
|
|
35
|
+
reference level `beta` — and `transform` re-applies that frozen normalization. Because `beta` is
|
|
36
|
+
per-cell, `transform` expects a matrix of the same `(genes, cells)` shape it was fitted on (re-fit
|
|
37
|
+
for a different gene/cell set). `fit_transform(X)` is the one-shot form.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
tr = rafinat.compost(p=0.5).fit(X) # estimate & freeze tr.r_ and tr.beta_
|
|
41
|
+
Z = tr.transform(X) # apply; == tr.fit_transform(X)
|
|
42
|
+
|
|
43
|
+
from sklearn.pipeline import Pipeline # optional — also works without scikit-learn installed
|
|
44
|
+
pipe = Pipeline([("normalize", rafinat.compost())])
|
|
45
|
+
Z = pipe.fit_transform(X)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
scikit-learn is an **optional** extra: if installed, the transformers inherit
|
|
49
|
+
`BaseEstimator` / `TransformerMixin` (full `Pipeline` / `clone` / `get_params` support); otherwise a
|
|
50
|
+
light built-in shim provides the same `fit` / `transform` / `fit_transform` / `get_params` API.
|
|
51
|
+
|
|
52
|
+
## Choosing the operating point
|
|
53
|
+
|
|
54
|
+
The benchmark-winning defaults are baked in, but every knob is a constructor argument:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
rafinat.compost(p=0.0) # default: digamma log-corner, trimmed centering
|
|
58
|
+
rafinat.compost(p=0.5) # posterior sqrt mean (order-1/2 power-mean cell size)
|
|
59
|
+
rafinat.compost(trim=0.0) # ordinary (non-trimmed) per-cell centering
|
|
60
|
+
rafinat.compost(r="mle") # pooled DM-MLE concentration instead of the CLR-matched r
|
|
61
|
+
rafinat.compost(zscore=True) # + per-gene z-score (the optional '->Z' standardization)
|
|
62
|
+
|
|
63
|
+
rafinat.hellnorm(reference="uniform") # classic log-map references: uniform / extrinsic / frechet
|
|
64
|
+
rafinat.hellnorm(trim=0.5) # lighter top-trim
|
|
65
|
+
|
|
66
|
+
rafinat.rafinat(beta="atop10") # simulation-leaning cell-size estimator
|
|
67
|
+
rafinat.rafinat(r_comp="mle") # smoother directional concentration
|
|
68
|
+
rafinat.rafinat(zcomp=True) # + per-gene z-score of the composition rows ('-> coordZ')
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Each of these returns a transformer; call `.fit_transform(X)` (or `.fit(X)` then `.transform(X)`)
|
|
72
|
+
on it. After fitting, the estimated values are exposed as fitted attributes (trailing underscore):
|
|
73
|
+
`compost.r_` / `compost.beta_`, `rafinat.r_comp_` / `rafinat.ref_` / `rafinat.C_` /
|
|
74
|
+
`rafinat.beta_`.
|
|
75
|
+
|
|
76
|
+
**The optional per-gene standardization (`compost(zscore=True)`, `rafinat(zcomp=True)`) is disabled
|
|
77
|
+
by default**, matching the benchmark's shipped defaults.
|
|
78
|
+
|
|
79
|
+
## Install
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install rafinat # compost / hellnorm (numpy + scipy only)
|
|
83
|
+
pip install "rafinat[rafinat]" # + the rafinat() method — adds the liudist backend (pulls in JAX)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`compost` and `hellnorm` need only `numpy`/`scipy`; the heavy `liudist` + JAX stack is pulled in
|
|
87
|
+
**only** by the `[rafinat]` extra, i.e. only if you use the `rafinat()` method.
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
## Method provenance
|
|
92
|
+
|
|
93
|
+
- **compost** — `digamma(r + x)` with `r = 1/(4·alpha) + 1/2` (so `digamma(r+x) ≈ log(x + 1/(4·alpha))`), minus a per-cell location estimated on the **low-expression bulk**
|
|
94
|
+
(the top high-expression genes — the biologically variable ones — are dropped from the cell-size
|
|
95
|
+
estimate).
|
|
96
|
+
- **hellnorm** — `sqrt(x / sum x)` minus a top-trimmed per-cell mean (the sqrt-geometry analog of
|
|
97
|
+
compost's trimmed centering). Dominates the classic uniform/extrinsic/Frechet references.
|
|
98
|
+
- **rafinat** — the Fisher-Rao Liouville distance factors as
|
|
99
|
+
`d² = d_composition² + C·(d ln β)²`; rafinat realises it as the stack
|
|
100
|
+
`[ composition arc length (depth-normalized, sharp r_snr) ; sqrt(C)·ln(β_c) ]`, with the radial
|
|
101
|
+
weight `C = genes · r_mle` decoupled from the (sharp) directional concentration so the cell-size
|
|
102
|
+
axis stays alive at a parameter-free weight.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rafinat"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Bayesian normalizations for RNA-seq"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "BSD-2-Clause" }
|
|
12
|
+
authors = [{ name = "Georgy Meshcheryakov", email = "iam@georgy.top" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"single-cell", "scRNA-seq", "RNA-seq", "normalization", "bioinformatics",
|
|
15
|
+
"Dirichlet-multinomial", "compositional data", "CLR", "Fisher-Rao", "scikit-learn",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"License :: OSI Approved :: BSD License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"numpy",
|
|
30
|
+
"scipy",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
# The rafinat() Fisher-Rao Liouville embedding needs the `liudist` backend, which pulls in JAX.
|
|
35
|
+
# compost() / hellnorm() are pure numpy+scipy -- installing plain `rafinat` needs none of this.
|
|
36
|
+
# pip install "rafinat[rafinat]" # adds liudist (+ JAX) for the rafinat() method
|
|
37
|
+
# For GPU acceleration, additionally install a CUDA JAX build yourself, e.g. `pip install jax-cuda12`.
|
|
38
|
+
rafinat = ["liudist>=0.1"]
|
|
39
|
+
gpu = ["liudist>=0.1"] # alias for the above
|
|
40
|
+
# Optional: inherit sklearn's BaseEstimator/TransformerMixin for full Pipeline/clone support.
|
|
41
|
+
# The transformers work without it (a light built-in shim provides the same fit/transform API).
|
|
42
|
+
sklearn = ["scikit-learn"]
|
|
43
|
+
test = ["pytest", "scikit-learn"]
|
|
44
|
+
|
|
45
|
+
# [project.urls] # fill in before uploading, if you have a repo/homepage
|
|
46
|
+
# Homepage = "https://github.com/<you>/rafinat"
|
|
47
|
+
# Repository = "https://github.com/<you>/rafinat"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools]
|
|
50
|
+
packages = ["rafinat"]
|
|
51
|
+
license-files = ["LICENSE"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""rafinat -- Dirichlet-Multinomial posterior normalizations for single-cell RNA-seq.
|
|
2
|
+
|
|
3
|
+
Three single-cell count normalizations derived from a Dirichlet-Multinomial (Liouville)
|
|
4
|
+
posterior view, with defaults set to the top performers of the Ahlmann-Eltze & Huber kNN
|
|
5
|
+
benchmark. Each is a scikit-learn-style transformer (``fit`` / ``transform`` / ``fit_transform``)
|
|
6
|
+
so it slots straight into data-transformation / preprocessing pipelines:
|
|
7
|
+
|
|
8
|
+
compost -- the DM-posterior generalisation of CLR (digamma log-corner with a top-trimmed
|
|
9
|
+
per-cell centering). ``p`` slides toward the sqrt/Hellinger end.
|
|
10
|
+
hellnorm -- the Hellinger / Fisher-Rao spherical (sqrt-CLR) normalization with a top-trimmed
|
|
11
|
+
per-cell reference.
|
|
12
|
+
rafinat -- the complete Fisher-Rao Liouville embedding (composition arc length + radial
|
|
13
|
+
cell-size coordinate). Needs the ``liudist`` GPU submodule.
|
|
14
|
+
|
|
15
|
+
``compost`` and ``hellnorm`` are pure numpy/scipy (CPU). ``rafinat`` uses the bundled ``liudist``
|
|
16
|
+
git submodule, which requires a GPU JAX build.
|
|
17
|
+
|
|
18
|
+
You instantiate a transformer with its operating point, then ``fit`` it (which estimates and
|
|
19
|
+
FREEZES the two data-driven pieces -- the concentration ``r`` and the per-cell reference level
|
|
20
|
+
``beta``) and ``transform`` with it. Because ``beta`` is per-cell, ``transform`` re-applies the
|
|
21
|
+
frozen normalization to a matrix of the same ``(genes, cells)`` shape. ``fit_transform(X)``
|
|
22
|
+
reproduces the one-shot output of the original functional API. scikit-learn is optional: if it is
|
|
23
|
+
installed the transformers inherit ``BaseEstimator`` / ``TransformerMixin`` (full ``Pipeline`` /
|
|
24
|
+
``clone`` support), otherwise a light built-in shim provides the same API.
|
|
25
|
+
|
|
26
|
+
The optional per-gene standardization (``compost(..., zscore=True)`` and
|
|
27
|
+
``rafinat(..., zcomp=True)``) is DISABLED by default, matching the benchmark's shipped defaults.
|
|
28
|
+
|
|
29
|
+
>>> import numpy as np, rafinat
|
|
30
|
+
>>> X = np.random.poisson(0.5, size=(2000, 500)).astype(float) # genes x cells
|
|
31
|
+
>>> Z = rafinat.compost().fit_transform(X) # = compost (p=0) trimmed (CLR-dominating default)
|
|
32
|
+
>>> H = rafinat.hellnorm().fit_transform(X) # = Hellnorm trimmed
|
|
33
|
+
>>> R = rafinat.rafinat().fit_transform(X) # = rafinat (complete); (genes + 1) x cells
|
|
34
|
+
>>>
|
|
35
|
+
>>> tr = rafinat.compost(p=0.5).fit(X) # estimate & freeze r_ and beta_ once
|
|
36
|
+
>>> Z2 = tr.transform(X) # re-apply the frozen normalization
|
|
37
|
+
"""
|
|
38
|
+
from ._dm import dm_power, estimate_r_pooled, estimate_r_percell, posterior_power_moments, alpha_global
|
|
39
|
+
from ._base import BaseNormalizer
|
|
40
|
+
from .compost import compost, hellnorm, z_score_rows # compost_bend stays in rafinat.compost (experimental)
|
|
41
|
+
from .liou import rafinat, fisher_rao_arclength, resolve_r, depthnorm
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"compost", "hellnorm", "rafinat",
|
|
45
|
+
"BaseNormalizer",
|
|
46
|
+
"fisher_rao_arclength", "z_score_rows", "resolve_r", "depthnorm",
|
|
47
|
+
"dm_power", "estimate_r_pooled", "estimate_r_percell", "posterior_power_moments", "alpha_global",
|
|
48
|
+
]
|
|
49
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Scikit-learn-compatible transformer base for the rafinat normalizers.
|
|
2
|
+
|
|
3
|
+
If scikit-learn is installed, the transformers inherit its ``BaseEstimator`` /
|
|
4
|
+
``TransformerMixin`` -- so they drop straight into ``sklearn.pipeline.Pipeline`` and support
|
|
5
|
+
``clone`` / ``get_params`` / ``set_params``. Otherwise a light shim provides the same
|
|
6
|
+
``get_params`` / ``set_params`` / ``fit_transform`` protocol, keeping the package's numpy +
|
|
7
|
+
scipy-only footprint intact (scikit-learn is an optional extra, never a hard dependency).
|
|
8
|
+
|
|
9
|
+
Data convention (unchanged from the old functional API): every transformer operates on a dense
|
|
10
|
+
``(genes, cells)`` matrix -- genes are the features (rows), cells the samples (columns).
|
|
11
|
+
``fit`` estimates and FREEZES the two data-driven pieces (the concentration ``r`` and the
|
|
12
|
+
per-cell reference level ``beta``); ``transform`` re-applies that exact frozen normalization and
|
|
13
|
+
therefore expects an array of the same ``(genes, cells)`` shape it was fitted on. The one-shot
|
|
14
|
+
``fit_transform(X)`` reproduces the output of the original module-level functions.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import inspect
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
try: # optional: full sklearn integration when available
|
|
23
|
+
from sklearn.base import BaseEstimator as _SkBase, TransformerMixin as _SkMixin
|
|
24
|
+
_HAS_SKLEARN = True
|
|
25
|
+
except Exception: # pragma: no cover - sklearn is an optional extra
|
|
26
|
+
_HAS_SKLEARN = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _ShimBase:
|
|
30
|
+
"""Minimal ``BaseEstimator`` / ``TransformerMixin`` stand-in used when sklearn is absent.
|
|
31
|
+
|
|
32
|
+
Implements the same duck-typed protocol sklearn pipelines rely on: parameters are read off
|
|
33
|
+
the ``__init__`` signature (so every constructor argument must be stored verbatim on the
|
|
34
|
+
instance), and ``fit_transform`` is ``fit(...).transform(...)``.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def _param_names(cls):
|
|
39
|
+
init = cls.__init__
|
|
40
|
+
if init is object.__init__:
|
|
41
|
+
return []
|
|
42
|
+
names = []
|
|
43
|
+
for name, p in inspect.signature(init).parameters.items():
|
|
44
|
+
if name == "self":
|
|
45
|
+
continue
|
|
46
|
+
if p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD):
|
|
47
|
+
raise RuntimeError(
|
|
48
|
+
f"{cls.__name__}.__init__ may not use *args/**kwargs (scikit-learn convention)")
|
|
49
|
+
names.append(name)
|
|
50
|
+
return sorted(names)
|
|
51
|
+
|
|
52
|
+
def get_params(self, deep=True):
|
|
53
|
+
return {k: getattr(self, k) for k in self._param_names()}
|
|
54
|
+
|
|
55
|
+
def set_params(self, **params):
|
|
56
|
+
valid = self._param_names()
|
|
57
|
+
for k, v in params.items():
|
|
58
|
+
if k not in valid:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"invalid parameter {k!r} for estimator {type(self).__name__}; "
|
|
61
|
+
f"valid parameters are {valid}")
|
|
62
|
+
setattr(self, k, v)
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def fit_transform(self, X, y=None, **fit_params):
|
|
66
|
+
return self.fit(X, y, **fit_params).transform(X)
|
|
67
|
+
|
|
68
|
+
def __repr__(self):
|
|
69
|
+
args = ", ".join(f"{k}={getattr(self, k, None)!r}" for k in self._param_names())
|
|
70
|
+
return f"{type(self).__name__}({args})"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if _HAS_SKLEARN:
|
|
74
|
+
_Base, _Mixin = _SkBase, _SkMixin
|
|
75
|
+
else:
|
|
76
|
+
_Base, _Mixin = _ShimBase, object
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class BaseNormalizer(_Base, _Mixin):
|
|
80
|
+
"""Common fit/transform plumbing shared by :class:`compost`, :class:`hellnorm`, :class:`rafinat`."""
|
|
81
|
+
|
|
82
|
+
def _check_2d(self, X):
|
|
83
|
+
X = np.asarray(X, dtype=np.float64)
|
|
84
|
+
if X.ndim != 2:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"{type(self).__name__} expects a 2-D (genes, cells) array, got ndim={X.ndim}")
|
|
87
|
+
return X
|
|
88
|
+
|
|
89
|
+
def _record_shape(self, X):
|
|
90
|
+
self.n_genes_, self.n_cells_ = X.shape
|
|
91
|
+
|
|
92
|
+
def _check_fitted(self):
|
|
93
|
+
if not hasattr(self, "n_cells_"):
|
|
94
|
+
raise RuntimeError(
|
|
95
|
+
f"This {type(self).__name__} instance is not fitted yet; call .fit(X) "
|
|
96
|
+
f"(or .fit_transform(X)) before .transform(X).")
|
|
97
|
+
|
|
98
|
+
def _check_transform_shape(self, X):
|
|
99
|
+
exp = (self.n_genes_, self.n_cells_)
|
|
100
|
+
if X.shape != exp:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"{type(self).__name__}.transform received an array of shape {X.shape}, but the "
|
|
103
|
+
f"transformer was fitted on {exp} (genes, cells). The frozen concentration and "
|
|
104
|
+
f"per-cell reference require the same shape; re-fit for a different gene/cell set.")
|