combatlearn 2.2.0__tar.gz → 2.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.
- {combatlearn-2.2.0 → combatlearn-2.4.0}/PKG-INFO +98 -10
- {combatlearn-2.2.0 → combatlearn-2.4.0}/README.md +89 -8
- combatlearn-2.4.0/combatlearn/__init__.py +6 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/combatlearn/_mixed.py +6 -22
- combatlearn-2.4.0/combatlearn/_nested.py +291 -0
- combatlearn-2.4.0/combatlearn/_utils.py +122 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/combatlearn/core.py +274 -286
- combatlearn-2.4.0/combatlearn/glm/__init__.py +10 -0
- combatlearn-2.4.0/combatlearn/glm/_beta.py +281 -0
- combatlearn-2.4.0/combatlearn/glm/_estimator.py +246 -0
- combatlearn-2.4.0/combatlearn/glm/_interpolate.py +137 -0
- combatlearn-2.4.0/combatlearn/glm/_model.py +623 -0
- combatlearn-2.4.0/combatlearn/glm/_nb.py +480 -0
- combatlearn-2.4.0/combatlearn/glm/_quantile.py +93 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/combatlearn/inspection.py +61 -23
- {combatlearn-2.2.0 → combatlearn-2.4.0}/combatlearn/metrics.py +22 -229
- combatlearn-2.4.0/combatlearn/sklearn_api.py +986 -0
- combatlearn-2.4.0/combatlearn/transductive/__init__.py +13 -0
- combatlearn-2.4.0/combatlearn/transductive/_transductive.py +301 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/combatlearn/visualization.py +36 -28
- {combatlearn-2.2.0 → combatlearn-2.4.0}/combatlearn.egg-info/PKG-INFO +98 -10
- {combatlearn-2.2.0 → combatlearn-2.4.0}/combatlearn.egg-info/SOURCES.txt +14 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/combatlearn.egg-info/requires.txt +3 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/pyproject.toml +8 -3
- {combatlearn-2.2.0 → combatlearn-2.4.0}/requirements-dev.txt +2 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/requirements.txt +1 -0
- combatlearn-2.4.0/tests/test_combat.py +967 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/tests/test_eb_degenerate.py +10 -13
- {combatlearn-2.2.0 → combatlearn-2.4.0}/tests/test_gam.py +93 -25
- combatlearn-2.4.0/tests/test_glm.py +539 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/tests/test_inspection.py +33 -6
- {combatlearn-2.2.0 → combatlearn-2.4.0}/tests/test_longitudinal.py +146 -4
- combatlearn-2.4.0/tests/test_nested.py +392 -0
- combatlearn-2.4.0/tests/test_property.py +426 -0
- combatlearn-2.4.0/tests/test_transductive.py +194 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/tests/test_visualization.py +3 -5
- combatlearn-2.2.0/combatlearn/__init__.py +0 -5
- combatlearn-2.2.0/combatlearn/_utils.py +0 -21
- combatlearn-2.2.0/combatlearn/sklearn_api.py +0 -362
- combatlearn-2.2.0/tests/test_combat.py +0 -523
- {combatlearn-2.2.0 → combatlearn-2.4.0}/LICENSE +0 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/combatlearn/py.typed +0 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/combatlearn.egg-info/dependency_links.txt +0 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/combatlearn.egg-info/top_level.txt +0 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/requirements-docs.txt +0 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/setup.cfg +0 -0
- {combatlearn-2.2.0 → combatlearn-2.4.0}/tests/test_metrics.py +0 -0
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: combatlearn
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Batch-effect harmonization for machine learning frameworks.
|
|
5
5
|
Author-email: Ettore Rocchi <ettoreroc@gmail.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Keywords: machine-learning,harmonization,combat,preprocessing
|
|
8
|
-
Classifier: Development Status ::
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
9
|
Classifier: Intended Audience :: Science/Research
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
16
|
Requires-Python: >=3.10
|
|
13
17
|
Description-Content-Type: text/markdown
|
|
14
18
|
License-File: LICENSE
|
|
15
19
|
Requires-Dist: pandas>=1.3
|
|
16
20
|
Requires-Dist: numpy>=1.21
|
|
21
|
+
Requires-Dist: scipy>=1.8
|
|
17
22
|
Requires-Dist: scikit-learn>=1.2
|
|
18
23
|
Requires-Dist: matplotlib>=3.4
|
|
19
24
|
Requires-Dist: seaborn>=0.12
|
|
@@ -23,11 +28,13 @@ Requires-Dist: umap-learn>=0.5
|
|
|
23
28
|
Provides-Extra: dev
|
|
24
29
|
Requires-Dist: pytest>=7; extra == "dev"
|
|
25
30
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
31
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
26
32
|
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
27
33
|
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
28
34
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
29
35
|
Requires-Dist: pandas-stubs; extra == "dev"
|
|
30
36
|
Requires-Dist: statsmodels>=0.14; extra == "dev"
|
|
37
|
+
Requires-Dist: inmoose>=0.7; extra == "dev"
|
|
31
38
|
Provides-Extra: docs
|
|
32
39
|
Requires-Dist: sphinx>=7.0.0; extra == "docs"
|
|
33
40
|
Requires-Dist: pydata-sphinx-theme>=0.15.0; extra == "docs"
|
|
@@ -40,7 +47,7 @@ Dynamic: license-file
|
|
|
40
47
|
|
|
41
48
|
[](https://www.python.org/)
|
|
42
49
|
[](https://github.com/EttoreRocchi/combatlearn/actions/workflows/test.yaml)
|
|
43
|
-
[](https://combatlearn.readthedocs.io)
|
|
50
|
+
[](https://combatlearn.readthedocs.io)
|
|
44
51
|
[](https://pypi.org/project/combatlearn/)
|
|
45
52
|
[](https://github.com/EttoreRocchi/combatlearn/blob/main/LICENSE)
|
|
46
53
|
|
|
@@ -50,14 +57,23 @@ Dynamic: license-file
|
|
|
50
57
|
|
|
51
58
|
**combatlearn** makes the popular _ComBat_ (and _CovBat_) batch-effect correction algorithm available for use into machine learning frameworks. It lets you harmonise high-dimensional data inside a scikit-learn `Pipeline`, so that cross-validation and grid-search automatically take batch structure into account, **without data leakage**.
|
|
52
59
|
|
|
53
|
-
**
|
|
60
|
+
**Inductive `ComBat` methods** (fit on train, apply to held-out data, cross-validation-safe):
|
|
54
61
|
- `method="johnson"` - classic ComBat (Johnson _et al._, 2007)
|
|
55
62
|
- `method="fortin"` - neuroComBat (Fortin _et al._, 2018)
|
|
56
63
|
- `method="chen"` - CovBat (Chen _et al._, 2022)
|
|
57
|
-
- `method="longitudinal"` - Longitudinal ComBat for repeated measures (Beer _et al._, 2020)
|
|
58
64
|
- `method="gam"` - ComBat-GAM, nonlinear (spline) covariate effects (Pomponio _et al._, 2020)
|
|
59
65
|
- `method="covbat_gam"` - CovBat with the same nonlinear covariate modeling
|
|
60
66
|
|
|
67
|
+
**Inductive estimators with their own signature** (same fit/transform contract, separate classes):
|
|
68
|
+
- `NestedComBat` - harmonizes over several batch variables at once (e.g. site, scanner, protocol) with optional order optimization and Gaussian-mixture grouping (Nested / OPNested / GMM ComBat, Horng _et al._, 2022)
|
|
69
|
+
- `LongitudinalComBat` - repeated-measures / longitudinal designs with a per-subject random intercept (Beer _et al._, 2020). Inductive under a stricter precondition than `ComBat`: the batch **and** the subject must have been seen at fit; unseen subjects degrade to the population correction, or raise with `on_unseen_subject="error"`
|
|
70
|
+
- `GLMComBat` - non-Gaussian data via a parametric GLM plus a strictly per-observation quantile map:
|
|
71
|
+
- `method="seq"` - ComBat-seq for raw RNA-seq counts, negative-binomial with a library-size offset (Zhang _et al._, 2020). Integer in, integer out
|
|
72
|
+
- `method="met"` - ComBat-met for DNA-methylation beta-values, beta regression with a logit link (Wang, 2025). Output stays strictly inside `(0, 1)`
|
|
73
|
+
- Both are **cross-validation-safe**, which no published implementation of either method offers: upstream they are whole-cohort functions
|
|
74
|
+
|
|
75
|
+
**Whole-cohort (transductive)** - `combatlearn.transductive.TransductiveComBat` holds configurations that cannot be frozen at fit, namely those protecting a condition-of-interest / outcome term in the design, used as a one-shot `fit_transform`. Its principal occupants are `method="seq"` and `method="met"`, the condition-protected ComBat-seq / ComBat-met variants for differential expression and differential methylation. `method="longitudinal"` is deprecated in favor of `LongitudinalComBat`, as is `ComBat(method="longitudinal")`; both are removed in v3.0.0.
|
|
76
|
+
|
|
61
77
|
## Installation
|
|
62
78
|
|
|
63
79
|
```bash
|
|
@@ -121,6 +137,74 @@ print(f"Best CV AUROC: {grid.best_score_:.3f}")
|
|
|
121
137
|
|
|
122
138
|
For a full example of how to use **combatlearn** see the [notebook demo](https://github.com/EttoreRocchi/combatlearn/blob/main/docs/source/demo/combatlearn_demo.ipynb)
|
|
123
139
|
|
|
140
|
+
## Multiple batch variables (`NestedComBat`)
|
|
141
|
+
|
|
142
|
+
When more than one technical variable needs harmonizing, `NestedComBat` applies ComBat to each in sequence. It picks the harmonization order that minimizes the residual batch effect, and can optionally add a latent Gaussian-mixture grouping. It is inductive and fits inside a `Pipeline` just like `ComBat`.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import pandas as pd
|
|
146
|
+
from combatlearn import NestedComBat
|
|
147
|
+
|
|
148
|
+
batch = pd.DataFrame({"site": site, "scanner": scanner}) # one column per batch variable
|
|
149
|
+
|
|
150
|
+
nested = NestedComBat(
|
|
151
|
+
batch=batch,
|
|
152
|
+
continuous_covariates=age,
|
|
153
|
+
discrete_covariates=diag,
|
|
154
|
+
method="fortin", # per-step engine: "fortin"/"chen"/"gam"/"covbat_gam"
|
|
155
|
+
optimize_order=True, # choose the order (OPNested)
|
|
156
|
+
gmm=None, # or "batch" (+GMM) / "covariate" (-GMM)
|
|
157
|
+
)
|
|
158
|
+
X_corrected = nested.fit_transform(X)
|
|
159
|
+
print("Harmonization order:", nested.order_)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Counts and proportions (`GLMComBat`)
|
|
163
|
+
|
|
164
|
+
RNA-seq counts and methylation beta-values are not Gaussian, and standardizing them destroys the structure downstream tools rely on. `GLMComBat` fits a negative-binomial GLM (`method="seq"`) or a beta regression (`method="met"`) and then maps each observation through the CDF of its own batch's fitted distribution into the corresponding quantile of a batch-free one.
|
|
165
|
+
|
|
166
|
+
That quantile map is **strictly per observation** - it uses only the sample's own measurements, batch label, covariates and (for `"seq"`) library size - so the model freezes at `fit` and `transform` applies to held-out data. The published ComBat-seq and ComBat-met implementations are whole-cohort functions with no such split.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from combatlearn import GLMComBat
|
|
170
|
+
|
|
171
|
+
harmonizer = GLMComBat(
|
|
172
|
+
batch=batch,
|
|
173
|
+
method="seq", # raw integer counts; "met" for 0-1 beta-values
|
|
174
|
+
discrete_covariates=tissue, # ordinary covariates, preserved
|
|
175
|
+
reference_batch="site_A", # recommended under cross-validation
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
counts_corrected = harmonizer.fit_transform(counts) # reproduces the reference exactly
|
|
179
|
+
|
|
180
|
+
harmonizer.fit(counts_train) # ... or freeze and reuse
|
|
181
|
+
counts_test_corrected = harmonizer.transform(counts_test)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`fit_transform` on a whole cohort is verified bit-exact against InMoose `pycombat_seq`, and to within 1e-6 against committed fixtures from the ComBat-met R package. The **inductive** path has no external reference to compare with - it is validated by its defining property, that a held-out sample is corrected identically alone or in company.
|
|
185
|
+
|
|
186
|
+
If the term you want to protect *is* the supervised target of a differential analysis on the same cohort, use `TransductiveComBat(method="seq"/"met", condition=...)` instead; if it is known at intake, it is an ordinary covariate and belongs here.
|
|
187
|
+
|
|
188
|
+
## Repeated measures (`LongitudinalComBat`)
|
|
189
|
+
|
|
190
|
+
When the same subjects are measured more than once, `LongitudinalComBat` adds a per-subject random intercept (REML) to the Fortin model, so within-subject correlation is modeled rather than treated as noise. It keeps the `fit`/`transform` contract, so a frozen correction can be applied to later visits of subjects already in the reference cohort.
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from combatlearn import LongitudinalComBat
|
|
194
|
+
|
|
195
|
+
harmonizer = LongitudinalComBat(
|
|
196
|
+
batch=batch,
|
|
197
|
+
subject_id=subject, # required: one label per sample
|
|
198
|
+
time_covariate=visit, # optional: continuous time variable
|
|
199
|
+
continuous_covariates=age,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
X_corrected = harmonizer.fit_transform(X) # whole cohort in one pass
|
|
203
|
+
X_new = harmonizer.fit(X_ref).transform(X_visits) # or freeze and apply
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Its precondition is one step stricter than `ComBat`'s: `ComBat` needs the *batch* to have been seen at fit, `LongitudinalComBat` needs the *batch and the subject*. A subject unseen at fit degrades to the population correction (zero random intercept), or raises with `on_unseen_subject="error"`. The transform is leakage-free in every split - each subject's intercept is estimated from that subject's training visits only - but for cross-validated prediction on *genuinely new* subjects there is no advantage over `method="fortin"`; the payoff is incremental harmonization of returning subjects.
|
|
207
|
+
|
|
124
208
|
## `ComBat` parameters
|
|
125
209
|
|
|
126
210
|
The following section provides a detailed explanation of all parameters available in the scikit-learn-compatible `ComBat` class. For complete API documentation, see the [API Reference](https://combatlearn.readthedocs.io/en/latest/api/).
|
|
@@ -139,11 +223,11 @@ The following section provides a detailed explanation of all parameters availabl
|
|
|
139
223
|
|
|
140
224
|
| Parameter | Type | Default | Description |
|
|
141
225
|
| --- | --- | --- | --- |
|
|
142
|
-
| `method` | str | `"johnson"` | ComBat method to use: <ul><li>`"johnson"` - Classical ComBat (_Johnson et al. 2007_)</li><li>`"fortin"` - ComBat with covariates (_Fortin et al. 2018_)</li><li>`"chen"` - CovBat, PCA-based correction (_Chen et al. 2022_)</li><li>`"longitudinal"` - Longitudinal ComBat with a per-subject random intercept (_Beer et al. 2020_)
|
|
143
|
-
| `parametric` | bool | `True` | Whether to use the **parametric empirical Bayes** formulation. If `False`,
|
|
144
|
-
| `mean_only` | bool | `False` | If `True`, only the **mean** is corrected, while variances are left unchanged. Useful for preserving variance structure in the data. |
|
|
226
|
+
| `method` | str | `"johnson"` | ComBat method to use: <ul><li>`"johnson"` - Classical ComBat (_Johnson et al. 2007_)</li><li>`"fortin"` - ComBat with covariates (_Fortin et al. 2018_)</li><li>`"chen"` - CovBat, PCA-based correction (_Chen et al. 2022_)</li><li>`"longitudinal"` - Longitudinal ComBat with a per-subject random intercept (_Beer et al. 2020_); **deprecated**, use `combatlearn.LongitudinalComBat`</li><li>`"gam"` - ComBat-GAM, nonlinear (spline) covariates (_Pomponio et al. 2020_)</li><li>`"covbat_gam"` - CovBat with the same nonlinear covariate modeling</li></ul> Case- and separator-insensitive literature aliases are also accepted (e.g. `"covbat"`, `"neurocombat"`, `"combat_gam"`). |
|
|
227
|
+
| `parametric` | bool | `True` | Whether to use the **parametric empirical Bayes** formulation. If `False`, the non-parametric estimate of `sva::ComBat` (`int.eprior`) is used, whose cost grows with the square of the number of features. |
|
|
228
|
+
| `mean_only` | bool | `False` | If `True`, only the **mean** is corrected, while variances are left unchanged. Useful for preserving variance structure in the data. For `"chen"` and `"covbat_gam"` it applies to the ComBat step only; the covariance step still harmonizes the PC-score variances. |
|
|
145
229
|
| `reference_batch` | str or `None` | `None` | If specified, acts as a reference batch - other batches will be corrected to match this one. |
|
|
146
|
-
| `covbat_cov_thresh` | float, int | `0.9` | For `"chen"`
|
|
230
|
+
| `covbat_cov_thresh` | float, int | `0.9` | For the `"chen"` and `"covbat_gam"` methods (also inside `NestedComBat`): Cumulative variance threshold in `(0, 1]` to retain PCs in PCA space (e.g., 0.9 = retain 90% explained variance). If an integer is provided, it represents the number of principal components to use. |
|
|
147
231
|
| `smooth_terms` | list of str or int, or `None` | `None` | For `"gam"`/`"covbat_gam"` only: continuous covariates to model nonlinearly with B-splines, by column name or integer position. `None` smooths all continuous covariates. |
|
|
148
232
|
| `spline_df` | int | `10` | For the GAM methods: B-spline degrees of freedom (basis functions) per smooth term. |
|
|
149
233
|
| `spline_degree` | int | `3` | For the GAM methods: B-spline degree (3 = cubic). |
|
|
@@ -205,3 +289,7 @@ Please consider citing the original papers:
|
|
|
205
289
|
- [**Longitudinal ComBat**](https://github.com/jcbeer/longCombat) - Beer JC et al. _Neuroimage_. 2020. doi: [10.1016/j.neuroimage.2020.117129](https://doi.org/10.1016/j.neuroimage.2020.117129)
|
|
206
290
|
|
|
207
291
|
- [**ComBat-GAM**](https://github.com/rpomponio/neuroHarmonize) - Pomponio R et al. _Neuroimage_. 2020. doi: [10.1016/j.neuroimage.2019.116450](https://doi.org/10.1016/j.neuroimage.2019.116450)
|
|
292
|
+
|
|
293
|
+
- [**ComBat-seq**](https://github.com/zhangyuqing/ComBat-seq) - Zhang Y, Parmigiani G, Johnson WE. _NAR Genom Bioinform_. 2020. doi: [10.1093/nargab/lqaa078](https://doi.org/10.1093/nargab/lqaa078)
|
|
294
|
+
|
|
295
|
+
- [**ComBat-met**](https://github.com/JmWangBio/ComBatMet) - Wang J. _NAR Genom Bioinform_. 2025. doi: [10.1093/nargab/lqaf062](https://doi.org/10.1093/nargab/lqaf062)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.python.org/)
|
|
4
4
|
[](https://github.com/EttoreRocchi/combatlearn/actions/workflows/test.yaml)
|
|
5
|
-
[](https://combatlearn.readthedocs.io)
|
|
5
|
+
[](https://combatlearn.readthedocs.io)
|
|
6
6
|
[](https://pypi.org/project/combatlearn/)
|
|
7
7
|
[](https://github.com/EttoreRocchi/combatlearn/blob/main/LICENSE)
|
|
8
8
|
|
|
@@ -12,14 +12,23 @@
|
|
|
12
12
|
|
|
13
13
|
**combatlearn** makes the popular _ComBat_ (and _CovBat_) batch-effect correction algorithm available for use into machine learning frameworks. It lets you harmonise high-dimensional data inside a scikit-learn `Pipeline`, so that cross-validation and grid-search automatically take batch structure into account, **without data leakage**.
|
|
14
14
|
|
|
15
|
-
**
|
|
15
|
+
**Inductive `ComBat` methods** (fit on train, apply to held-out data, cross-validation-safe):
|
|
16
16
|
- `method="johnson"` - classic ComBat (Johnson _et al._, 2007)
|
|
17
17
|
- `method="fortin"` - neuroComBat (Fortin _et al._, 2018)
|
|
18
18
|
- `method="chen"` - CovBat (Chen _et al._, 2022)
|
|
19
|
-
- `method="longitudinal"` - Longitudinal ComBat for repeated measures (Beer _et al._, 2020)
|
|
20
19
|
- `method="gam"` - ComBat-GAM, nonlinear (spline) covariate effects (Pomponio _et al._, 2020)
|
|
21
20
|
- `method="covbat_gam"` - CovBat with the same nonlinear covariate modeling
|
|
22
21
|
|
|
22
|
+
**Inductive estimators with their own signature** (same fit/transform contract, separate classes):
|
|
23
|
+
- `NestedComBat` - harmonizes over several batch variables at once (e.g. site, scanner, protocol) with optional order optimization and Gaussian-mixture grouping (Nested / OPNested / GMM ComBat, Horng _et al._, 2022)
|
|
24
|
+
- `LongitudinalComBat` - repeated-measures / longitudinal designs with a per-subject random intercept (Beer _et al._, 2020). Inductive under a stricter precondition than `ComBat`: the batch **and** the subject must have been seen at fit; unseen subjects degrade to the population correction, or raise with `on_unseen_subject="error"`
|
|
25
|
+
- `GLMComBat` - non-Gaussian data via a parametric GLM plus a strictly per-observation quantile map:
|
|
26
|
+
- `method="seq"` - ComBat-seq for raw RNA-seq counts, negative-binomial with a library-size offset (Zhang _et al._, 2020). Integer in, integer out
|
|
27
|
+
- `method="met"` - ComBat-met for DNA-methylation beta-values, beta regression with a logit link (Wang, 2025). Output stays strictly inside `(0, 1)`
|
|
28
|
+
- Both are **cross-validation-safe**, which no published implementation of either method offers: upstream they are whole-cohort functions
|
|
29
|
+
|
|
30
|
+
**Whole-cohort (transductive)** - `combatlearn.transductive.TransductiveComBat` holds configurations that cannot be frozen at fit, namely those protecting a condition-of-interest / outcome term in the design, used as a one-shot `fit_transform`. Its principal occupants are `method="seq"` and `method="met"`, the condition-protected ComBat-seq / ComBat-met variants for differential expression and differential methylation. `method="longitudinal"` is deprecated in favor of `LongitudinalComBat`, as is `ComBat(method="longitudinal")`; both are removed in v3.0.0.
|
|
31
|
+
|
|
23
32
|
## Installation
|
|
24
33
|
|
|
25
34
|
```bash
|
|
@@ -83,6 +92,74 @@ print(f"Best CV AUROC: {grid.best_score_:.3f}")
|
|
|
83
92
|
|
|
84
93
|
For a full example of how to use **combatlearn** see the [notebook demo](https://github.com/EttoreRocchi/combatlearn/blob/main/docs/source/demo/combatlearn_demo.ipynb)
|
|
85
94
|
|
|
95
|
+
## Multiple batch variables (`NestedComBat`)
|
|
96
|
+
|
|
97
|
+
When more than one technical variable needs harmonizing, `NestedComBat` applies ComBat to each in sequence. It picks the harmonization order that minimizes the residual batch effect, and can optionally add a latent Gaussian-mixture grouping. It is inductive and fits inside a `Pipeline` just like `ComBat`.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import pandas as pd
|
|
101
|
+
from combatlearn import NestedComBat
|
|
102
|
+
|
|
103
|
+
batch = pd.DataFrame({"site": site, "scanner": scanner}) # one column per batch variable
|
|
104
|
+
|
|
105
|
+
nested = NestedComBat(
|
|
106
|
+
batch=batch,
|
|
107
|
+
continuous_covariates=age,
|
|
108
|
+
discrete_covariates=diag,
|
|
109
|
+
method="fortin", # per-step engine: "fortin"/"chen"/"gam"/"covbat_gam"
|
|
110
|
+
optimize_order=True, # choose the order (OPNested)
|
|
111
|
+
gmm=None, # or "batch" (+GMM) / "covariate" (-GMM)
|
|
112
|
+
)
|
|
113
|
+
X_corrected = nested.fit_transform(X)
|
|
114
|
+
print("Harmonization order:", nested.order_)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Counts and proportions (`GLMComBat`)
|
|
118
|
+
|
|
119
|
+
RNA-seq counts and methylation beta-values are not Gaussian, and standardizing them destroys the structure downstream tools rely on. `GLMComBat` fits a negative-binomial GLM (`method="seq"`) or a beta regression (`method="met"`) and then maps each observation through the CDF of its own batch's fitted distribution into the corresponding quantile of a batch-free one.
|
|
120
|
+
|
|
121
|
+
That quantile map is **strictly per observation** - it uses only the sample's own measurements, batch label, covariates and (for `"seq"`) library size - so the model freezes at `fit` and `transform` applies to held-out data. The published ComBat-seq and ComBat-met implementations are whole-cohort functions with no such split.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from combatlearn import GLMComBat
|
|
125
|
+
|
|
126
|
+
harmonizer = GLMComBat(
|
|
127
|
+
batch=batch,
|
|
128
|
+
method="seq", # raw integer counts; "met" for 0-1 beta-values
|
|
129
|
+
discrete_covariates=tissue, # ordinary covariates, preserved
|
|
130
|
+
reference_batch="site_A", # recommended under cross-validation
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
counts_corrected = harmonizer.fit_transform(counts) # reproduces the reference exactly
|
|
134
|
+
|
|
135
|
+
harmonizer.fit(counts_train) # ... or freeze and reuse
|
|
136
|
+
counts_test_corrected = harmonizer.transform(counts_test)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`fit_transform` on a whole cohort is verified bit-exact against InMoose `pycombat_seq`, and to within 1e-6 against committed fixtures from the ComBat-met R package. The **inductive** path has no external reference to compare with - it is validated by its defining property, that a held-out sample is corrected identically alone or in company.
|
|
140
|
+
|
|
141
|
+
If the term you want to protect *is* the supervised target of a differential analysis on the same cohort, use `TransductiveComBat(method="seq"/"met", condition=...)` instead; if it is known at intake, it is an ordinary covariate and belongs here.
|
|
142
|
+
|
|
143
|
+
## Repeated measures (`LongitudinalComBat`)
|
|
144
|
+
|
|
145
|
+
When the same subjects are measured more than once, `LongitudinalComBat` adds a per-subject random intercept (REML) to the Fortin model, so within-subject correlation is modeled rather than treated as noise. It keeps the `fit`/`transform` contract, so a frozen correction can be applied to later visits of subjects already in the reference cohort.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from combatlearn import LongitudinalComBat
|
|
149
|
+
|
|
150
|
+
harmonizer = LongitudinalComBat(
|
|
151
|
+
batch=batch,
|
|
152
|
+
subject_id=subject, # required: one label per sample
|
|
153
|
+
time_covariate=visit, # optional: continuous time variable
|
|
154
|
+
continuous_covariates=age,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
X_corrected = harmonizer.fit_transform(X) # whole cohort in one pass
|
|
158
|
+
X_new = harmonizer.fit(X_ref).transform(X_visits) # or freeze and apply
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Its precondition is one step stricter than `ComBat`'s: `ComBat` needs the *batch* to have been seen at fit, `LongitudinalComBat` needs the *batch and the subject*. A subject unseen at fit degrades to the population correction (zero random intercept), or raises with `on_unseen_subject="error"`. The transform is leakage-free in every split - each subject's intercept is estimated from that subject's training visits only - but for cross-validated prediction on *genuinely new* subjects there is no advantage over `method="fortin"`; the payoff is incremental harmonization of returning subjects.
|
|
162
|
+
|
|
86
163
|
## `ComBat` parameters
|
|
87
164
|
|
|
88
165
|
The following section provides a detailed explanation of all parameters available in the scikit-learn-compatible `ComBat` class. For complete API documentation, see the [API Reference](https://combatlearn.readthedocs.io/en/latest/api/).
|
|
@@ -101,11 +178,11 @@ The following section provides a detailed explanation of all parameters availabl
|
|
|
101
178
|
|
|
102
179
|
| Parameter | Type | Default | Description |
|
|
103
180
|
| --- | --- | --- | --- |
|
|
104
|
-
| `method` | str | `"johnson"` | ComBat method to use: <ul><li>`"johnson"` - Classical ComBat (_Johnson et al. 2007_)</li><li>`"fortin"` - ComBat with covariates (_Fortin et al. 2018_)</li><li>`"chen"` - CovBat, PCA-based correction (_Chen et al. 2022_)</li><li>`"longitudinal"` - Longitudinal ComBat with a per-subject random intercept (_Beer et al. 2020_)
|
|
105
|
-
| `parametric` | bool | `True` | Whether to use the **parametric empirical Bayes** formulation. If `False`,
|
|
106
|
-
| `mean_only` | bool | `False` | If `True`, only the **mean** is corrected, while variances are left unchanged. Useful for preserving variance structure in the data. |
|
|
181
|
+
| `method` | str | `"johnson"` | ComBat method to use: <ul><li>`"johnson"` - Classical ComBat (_Johnson et al. 2007_)</li><li>`"fortin"` - ComBat with covariates (_Fortin et al. 2018_)</li><li>`"chen"` - CovBat, PCA-based correction (_Chen et al. 2022_)</li><li>`"longitudinal"` - Longitudinal ComBat with a per-subject random intercept (_Beer et al. 2020_); **deprecated**, use `combatlearn.LongitudinalComBat`</li><li>`"gam"` - ComBat-GAM, nonlinear (spline) covariates (_Pomponio et al. 2020_)</li><li>`"covbat_gam"` - CovBat with the same nonlinear covariate modeling</li></ul> Case- and separator-insensitive literature aliases are also accepted (e.g. `"covbat"`, `"neurocombat"`, `"combat_gam"`). |
|
|
182
|
+
| `parametric` | bool | `True` | Whether to use the **parametric empirical Bayes** formulation. If `False`, the non-parametric estimate of `sva::ComBat` (`int.eprior`) is used, whose cost grows with the square of the number of features. |
|
|
183
|
+
| `mean_only` | bool | `False` | If `True`, only the **mean** is corrected, while variances are left unchanged. Useful for preserving variance structure in the data. For `"chen"` and `"covbat_gam"` it applies to the ComBat step only; the covariance step still harmonizes the PC-score variances. |
|
|
107
184
|
| `reference_batch` | str or `None` | `None` | If specified, acts as a reference batch - other batches will be corrected to match this one. |
|
|
108
|
-
| `covbat_cov_thresh` | float, int | `0.9` | For `"chen"`
|
|
185
|
+
| `covbat_cov_thresh` | float, int | `0.9` | For the `"chen"` and `"covbat_gam"` methods (also inside `NestedComBat`): Cumulative variance threshold in `(0, 1]` to retain PCs in PCA space (e.g., 0.9 = retain 90% explained variance). If an integer is provided, it represents the number of principal components to use. |
|
|
109
186
|
| `smooth_terms` | list of str or int, or `None` | `None` | For `"gam"`/`"covbat_gam"` only: continuous covariates to model nonlinearly with B-splines, by column name or integer position. `None` smooths all continuous covariates. |
|
|
110
187
|
| `spline_df` | int | `10` | For the GAM methods: B-spline degrees of freedom (basis functions) per smooth term. |
|
|
111
188
|
| `spline_degree` | int | `3` | For the GAM methods: B-spline degree (3 = cubic). |
|
|
@@ -166,4 +243,8 @@ Please consider citing the original papers:
|
|
|
166
243
|
|
|
167
244
|
- [**Longitudinal ComBat**](https://github.com/jcbeer/longCombat) - Beer JC et al. _Neuroimage_. 2020. doi: [10.1016/j.neuroimage.2020.117129](https://doi.org/10.1016/j.neuroimage.2020.117129)
|
|
168
245
|
|
|
169
|
-
- [**ComBat-GAM**](https://github.com/rpomponio/neuroHarmonize) - Pomponio R et al. _Neuroimage_. 2020. doi: [10.1016/j.neuroimage.2019.116450](https://doi.org/10.1016/j.neuroimage.2019.116450)
|
|
246
|
+
- [**ComBat-GAM**](https://github.com/rpomponio/neuroHarmonize) - Pomponio R et al. _Neuroimage_. 2020. doi: [10.1016/j.neuroimage.2019.116450](https://doi.org/10.1016/j.neuroimage.2019.116450)
|
|
247
|
+
|
|
248
|
+
- [**ComBat-seq**](https://github.com/zhangyuqing/ComBat-seq) - Zhang Y, Parmigiani G, Johnson WE. _NAR Genom Bioinform_. 2020. doi: [10.1093/nargab/lqaa078](https://doi.org/10.1093/nargab/lqaa078)
|
|
249
|
+
|
|
250
|
+
- [**ComBat-met**](https://github.com/JmWangBio/ComBatMet) - Wang J. _NAR Genom Bioinform_. 2025. doi: [10.1093/nargab/lqaf062](https://doi.org/10.1093/nargab/lqaf062)
|
|
@@ -1,23 +1,8 @@
|
|
|
1
|
-
"""Random-intercept linear mixed model
|
|
1
|
+
"""Random-intercept linear mixed model, fit by REML, for Longitudinal ComBat.
|
|
2
2
|
|
|
3
|
-
Fits
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
where ``Z`` is the subject (random-intercept) indicator matrix. Estimation is
|
|
8
|
-
by REML, profiling out ``beta`` and ``sigma^2`` so that only the scalar
|
|
9
|
-
variance ratio ``lambda = tau^2 / sigma^2`` is optimized per feature.
|
|
10
|
-
|
|
11
|
-
The single random-intercept grouping makes the marginal covariance
|
|
12
|
-
``V = sigma^2 (I + lambda Z Z^T)`` block diagonal (one block per subject), so
|
|
13
|
-
``V^{-1}`` is available in closed form via Sherman-Morrison and every operation
|
|
14
|
-
is O(n) per feature. The variance ratio is found by a vectorized grid search
|
|
15
|
-
over ``lambda`` followed by a parabolic refinement, after which the fixed
|
|
16
|
-
effects, residual variance, and random-intercept BLUPs are computed in a single
|
|
17
|
-
per-feature pass.
|
|
18
|
-
|
|
19
|
-
This keeps Longitudinal ComBat dependency-free (numpy only); correctness is
|
|
20
|
-
cross-checked against ``statsmodels.MixedLM`` in the test suite.
|
|
3
|
+
Fits ``y = X beta + Z u + eps`` per feature, profiling out ``beta`` and ``sigma^2``
|
|
4
|
+
so only ``lambda = tau^2 / sigma^2`` is searched; the block-diagonal covariance is
|
|
5
|
+
inverted in O(n) via Sherman-Morrison.
|
|
21
6
|
"""
|
|
22
7
|
|
|
23
8
|
from __future__ import annotations
|
|
@@ -84,6 +69,8 @@ def fit_random_intercept(
|
|
|
84
69
|
) -> tuple[FloatArray, FloatArray, FloatArray, FloatArray]:
|
|
85
70
|
"""Fit a random-intercept LME per feature by REML.
|
|
86
71
|
|
|
72
|
+
``lambda`` is found by a log-spaced grid search refined by a parabolic step.
|
|
73
|
+
|
|
87
74
|
Parameters
|
|
88
75
|
----------
|
|
89
76
|
X : ndarray of shape (n, p)
|
|
@@ -120,7 +107,6 @@ def fit_random_intercept(
|
|
|
120
107
|
n_features = Y.shape[1]
|
|
121
108
|
ridge = eps * float(np.trace(X.T @ X)) / max(p, 1)
|
|
122
109
|
|
|
123
|
-
# Vectorized grid search for the variance ratio (per feature).
|
|
124
110
|
log_grid = np.linspace(-4.0, 4.0, n_grid)
|
|
125
111
|
lam_grid = np.concatenate([[0.0], 10.0**log_grid])
|
|
126
112
|
fvals = np.empty((lam_grid.size, n_features), dtype=np.float64)
|
|
@@ -129,7 +115,6 @@ def fit_random_intercept(
|
|
|
129
115
|
best = np.argmin(fvals, axis=0)
|
|
130
116
|
lam_opt = lam_grid[best].astype(np.float64)
|
|
131
117
|
|
|
132
|
-
# Vectorized parabolic refinement (log10 space) for interior positive optima.
|
|
133
118
|
interior = (best >= 2) & (best <= n_grid - 1)
|
|
134
119
|
if np.any(interior):
|
|
135
120
|
bi = best[interior]
|
|
@@ -142,7 +127,6 @@ def fit_random_intercept(
|
|
|
142
127
|
step = log_grid[1] - log_grid[0]
|
|
143
128
|
lam_opt[interior] = 10.0 ** (log_grid[bi - 1] + delta * step)
|
|
144
129
|
|
|
145
|
-
# Final per-feature fixed effects, residual variance, and random-intercept BLUPs.
|
|
146
130
|
beta = np.empty((p, n_features), dtype=np.float64)
|
|
147
131
|
sigma2 = np.empty(n_features, dtype=np.float64)
|
|
148
132
|
blup = np.zeros((n_groups, n_features), dtype=np.float64)
|