combatlearn 2.3.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.3.0 → combatlearn-2.4.0}/PKG-INFO +66 -8
- {combatlearn-2.3.0 → combatlearn-2.4.0}/README.md +64 -8
- combatlearn-2.4.0/combatlearn/__init__.py +6 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/combatlearn/_mixed.py +6 -22
- {combatlearn-2.3.0 → combatlearn-2.4.0}/combatlearn/_nested.py +15 -50
- combatlearn-2.4.0/combatlearn/_utils.py +122 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/combatlearn/core.py +218 -216
- 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.3.0 → combatlearn-2.4.0}/combatlearn/inspection.py +11 -21
- {combatlearn-2.3.0 → combatlearn-2.4.0}/combatlearn/metrics.py +21 -228
- {combatlearn-2.3.0 → combatlearn-2.4.0}/combatlearn/sklearn_api.py +291 -82
- combatlearn-2.4.0/combatlearn/transductive/__init__.py +13 -0
- combatlearn-2.4.0/combatlearn/transductive/_transductive.py +301 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/combatlearn/visualization.py +25 -18
- {combatlearn-2.3.0 → combatlearn-2.4.0}/combatlearn.egg-info/PKG-INFO +66 -8
- {combatlearn-2.3.0 → combatlearn-2.4.0}/combatlearn.egg-info/SOURCES.txt +8 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/combatlearn.egg-info/requires.txt +2 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/requirements-dev.txt +1 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/requirements.txt +1 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/tests/test_combat.py +416 -7
- {combatlearn-2.3.0 → combatlearn-2.4.0}/tests/test_eb_degenerate.py +10 -13
- {combatlearn-2.3.0 → combatlearn-2.4.0}/tests/test_gam.py +80 -31
- combatlearn-2.4.0/tests/test_glm.py +539 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/tests/test_inspection.py +0 -1
- {combatlearn-2.3.0 → combatlearn-2.4.0}/tests/test_longitudinal.py +143 -2
- {combatlearn-2.3.0 → combatlearn-2.4.0}/tests/test_nested.py +62 -5
- {combatlearn-2.3.0 → combatlearn-2.4.0}/tests/test_property.py +165 -36
- combatlearn-2.4.0/tests/test_transductive.py +194 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/tests/test_visualization.py +3 -5
- combatlearn-2.3.0/combatlearn/__init__.py +0 -5
- combatlearn-2.3.0/combatlearn/_utils.py +0 -66
- combatlearn-2.3.0/combatlearn/transductive/__init__.py +0 -15
- combatlearn-2.3.0/combatlearn/transductive/_transductive.py +0 -189
- combatlearn-2.3.0/tests/test_transductive.py +0 -74
- {combatlearn-2.3.0 → combatlearn-2.4.0}/LICENSE +0 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/combatlearn/py.typed +0 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/combatlearn.egg-info/dependency_links.txt +0 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/combatlearn.egg-info/top_level.txt +0 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/pyproject.toml +0 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/requirements-docs.txt +0 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/setup.cfg +0 -0
- {combatlearn-2.3.0 → combatlearn-2.4.0}/tests/test_metrics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
License-Expression: MIT
|
|
@@ -18,6 +18,7 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: pandas>=1.3
|
|
20
20
|
Requires-Dist: numpy>=1.21
|
|
21
|
+
Requires-Dist: scipy>=1.8
|
|
21
22
|
Requires-Dist: scikit-learn>=1.2
|
|
22
23
|
Requires-Dist: matplotlib>=3.4
|
|
23
24
|
Requires-Dist: seaborn>=0.12
|
|
@@ -33,6 +34,7 @@ Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
|
33
34
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
34
35
|
Requires-Dist: pandas-stubs; extra == "dev"
|
|
35
36
|
Requires-Dist: statsmodels>=0.14; extra == "dev"
|
|
37
|
+
Requires-Dist: inmoose>=0.7; extra == "dev"
|
|
36
38
|
Provides-Extra: docs
|
|
37
39
|
Requires-Dist: sphinx>=7.0.0; extra == "docs"
|
|
38
40
|
Requires-Dist: pydata-sphinx-theme>=0.15.0; extra == "docs"
|
|
@@ -45,7 +47,7 @@ Dynamic: license-file
|
|
|
45
47
|
|
|
46
48
|
[](https://www.python.org/)
|
|
47
49
|
[](https://github.com/EttoreRocchi/combatlearn/actions/workflows/test.yaml)
|
|
48
|
-
[](https://combatlearn.readthedocs.io)
|
|
50
|
+
[](https://combatlearn.readthedocs.io)
|
|
49
51
|
[](https://pypi.org/project/combatlearn/)
|
|
50
52
|
[](https://github.com/EttoreRocchi/combatlearn/blob/main/LICENSE)
|
|
51
53
|
|
|
@@ -62,9 +64,15 @@ Dynamic: license-file
|
|
|
62
64
|
- `method="gam"` - ComBat-GAM, nonlinear (spline) covariate effects (Pomponio _et al._, 2020)
|
|
63
65
|
- `method="covbat_gam"` - CovBat with the same nonlinear covariate modeling
|
|
64
66
|
|
|
65
|
-
**
|
|
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
|
|
66
74
|
|
|
67
|
-
**Whole-cohort (transductive)** - `combatlearn.transductive.TransductiveComBat
|
|
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.
|
|
68
76
|
|
|
69
77
|
## Installation
|
|
70
78
|
|
|
@@ -151,6 +159,52 @@ X_corrected = nested.fit_transform(X)
|
|
|
151
159
|
print("Harmonization order:", nested.order_)
|
|
152
160
|
```
|
|
153
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
|
+
|
|
154
208
|
## `ComBat` parameters
|
|
155
209
|
|
|
156
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/).
|
|
@@ -169,11 +223,11 @@ The following section provides a detailed explanation of all parameters availabl
|
|
|
169
223
|
|
|
170
224
|
| Parameter | Type | Default | Description |
|
|
171
225
|
| --- | --- | --- | --- |
|
|
172
|
-
| `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.
|
|
173
|
-
| `parametric` | bool | `True` | Whether to use the **parametric empirical Bayes** formulation. If `False`,
|
|
174
|
-
| `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. |
|
|
175
229
|
| `reference_batch` | str or `None` | `None` | If specified, acts as a reference batch - other batches will be corrected to match this one. |
|
|
176
|
-
| `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. |
|
|
177
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. |
|
|
178
232
|
| `spline_df` | int | `10` | For the GAM methods: B-spline degrees of freedom (basis functions) per smooth term. |
|
|
179
233
|
| `spline_degree` | int | `3` | For the GAM methods: B-spline degree (3 = cubic). |
|
|
@@ -235,3 +289,7 @@ Please consider citing the original papers:
|
|
|
235
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)
|
|
236
290
|
|
|
237
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
|
|
|
@@ -19,9 +19,15 @@
|
|
|
19
19
|
- `method="gam"` - ComBat-GAM, nonlinear (spline) covariate effects (Pomponio _et al._, 2020)
|
|
20
20
|
- `method="covbat_gam"` - CovBat with the same nonlinear covariate modeling
|
|
21
21
|
|
|
22
|
-
**
|
|
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
|
|
23
29
|
|
|
24
|
-
**Whole-cohort (transductive)** - `combatlearn.transductive.TransductiveComBat
|
|
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.
|
|
25
31
|
|
|
26
32
|
## Installation
|
|
27
33
|
|
|
@@ -108,6 +114,52 @@ X_corrected = nested.fit_transform(X)
|
|
|
108
114
|
print("Harmonization order:", nested.order_)
|
|
109
115
|
```
|
|
110
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
|
+
|
|
111
163
|
## `ComBat` parameters
|
|
112
164
|
|
|
113
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/).
|
|
@@ -126,11 +178,11 @@ The following section provides a detailed explanation of all parameters availabl
|
|
|
126
178
|
|
|
127
179
|
| Parameter | Type | Default | Description |
|
|
128
180
|
| --- | --- | --- | --- |
|
|
129
|
-
| `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.
|
|
130
|
-
| `parametric` | bool | `True` | Whether to use the **parametric empirical Bayes** formulation. If `False`,
|
|
131
|
-
| `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. |
|
|
132
184
|
| `reference_batch` | str or `None` | `None` | If specified, acts as a reference batch - other batches will be corrected to match this one. |
|
|
133
|
-
| `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. |
|
|
134
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. |
|
|
135
187
|
| `spline_df` | int | `10` | For the GAM methods: B-spline degrees of freedom (basis functions) per smooth term. |
|
|
136
188
|
| `spline_degree` | int | `3` | For the GAM methods: B-spline degree (3 = cubic). |
|
|
@@ -191,4 +243,8 @@ Please consider citing the original papers:
|
|
|
191
243
|
|
|
192
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)
|
|
193
245
|
|
|
194
|
-
- [**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)
|
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
"""Orchestration helpers for NestedComBat (*Horng et al.* 2022).
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
to a :class:`~combatlearn.core.ComBatModel`. This module holds the pieces that
|
|
6
|
-
surround that sequence and add no new empirical-Bayes math:
|
|
7
|
-
|
|
8
|
-
* :func:`fit_nested_sequence` - fit one ``ComBatModel`` per batch variable in a
|
|
9
|
-
given order, chaining the transforms.
|
|
10
|
-
* :func:`anderson_darling_batch_count` - the residual-batch-effect objective the
|
|
11
|
-
OPNested order search minimizes (Anderson-Darling k-sample feature count).
|
|
12
|
-
* :func:`search_order` - exhaustive order search over all ``k!`` orderings, with
|
|
13
|
-
a factorial guardrail that falls back to greedy forward selection.
|
|
14
|
-
* :class:`GMMGrouping` - the per-feature Gaussian-mixture grouping fed back into
|
|
15
|
-
the sequence as an extra batch variable (``+GMM``) or covariate (``-GMM``).
|
|
16
|
-
|
|
17
|
-
Everything here is learned on the training data and frozen for transform, so the
|
|
18
|
-
resulting estimator stays inductive / cross-validation-safe.
|
|
3
|
+
The sequential single-batch ComBat fits, the Anderson-Darling order-search
|
|
4
|
+
objective, and the GMM grouping; all are learned at fit and frozen for transform.
|
|
19
5
|
"""
|
|
20
6
|
|
|
21
7
|
from __future__ import annotations
|
|
@@ -61,12 +47,9 @@ def anderson_darling_batch_count(
|
|
|
61
47
|
) -> int:
|
|
62
48
|
"""Number of features with a significant residual batch effect.
|
|
63
49
|
|
|
64
|
-
|
|
65
|
-
(:func:`scipy.stats.anderson_ksamp`)
|
|
66
|
-
|
|
67
|
-
test is significant at ``alpha``. The counts are summed over all batch
|
|
68
|
-
variables, so a lower value means less residual batch structure. This is the
|
|
69
|
-
objective minimized by the OPNested order search.
|
|
50
|
+
Counts (feature, batch variable) pairs whose Anderson-Darling k-sample test
|
|
51
|
+
(:func:`scipy.stats.anderson_ksamp`) is significant at ``alpha``; degenerate
|
|
52
|
+
features do not count. This is the objective the OPNested order search minimizes.
|
|
70
53
|
|
|
71
54
|
Parameters
|
|
72
55
|
----------
|
|
@@ -86,8 +69,7 @@ def anderson_darling_batch_count(
|
|
|
86
69
|
n_features = col_values.shape[1]
|
|
87
70
|
total = 0
|
|
88
71
|
with warnings.catch_warnings():
|
|
89
|
-
#
|
|
90
|
-
# capping never changes the alpha=0.05 decision, so the warning is considered noise.
|
|
72
|
+
# Warns only when p is capped to [0.001, 0.25]; harmless at alpha=0.05.
|
|
91
73
|
warnings.simplefilter("ignore")
|
|
92
74
|
for batch in batch_vars.values():
|
|
93
75
|
labels = np.asarray(batch)
|
|
@@ -101,8 +83,6 @@ def anderson_darling_batch_count(
|
|
|
101
83
|
try:
|
|
102
84
|
result = anderson_ksamp(samples)
|
|
103
85
|
except ValueError:
|
|
104
|
-
# Degenerate feature (e.g. constant within the pooled sample):
|
|
105
|
-
# no detectable batch effect, so it does not count.
|
|
106
86
|
continue
|
|
107
87
|
if result.significance_level < alpha:
|
|
108
88
|
total += 1
|
|
@@ -119,11 +99,8 @@ def fit_nested_sequence(
|
|
|
119
99
|
) -> tuple[list[ModelStep], pd.DataFrame]:
|
|
120
100
|
"""Fit one ``ComBatModel`` per batch variable in ``order``, chaining transforms.
|
|
121
101
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
Returns the list of ``(batch_variable, fitted_model)`` steps and the fully
|
|
126
|
-
harmonized training data.
|
|
102
|
+
Covariates are protected at every step. Returns the ``(batch_variable, model)``
|
|
103
|
+
steps and the fully harmonized training data.
|
|
127
104
|
"""
|
|
128
105
|
models: list[ModelStep] = []
|
|
129
106
|
X_current = X
|
|
@@ -187,8 +164,7 @@ def _search_greedy(
|
|
|
187
164
|
make_model: Callable[[str], ComBatModel],
|
|
188
165
|
alpha: float,
|
|
189
166
|
) -> tuple[list[str], list[ModelStep]]:
|
|
190
|
-
"""Greedy forward selection:
|
|
191
|
-
the AD feature count, harmonize with it, then continue with the rest."""
|
|
167
|
+
"""Greedy forward selection: repeatedly apply the step that most lowers the AD count."""
|
|
192
168
|
remaining = list(names)
|
|
193
169
|
order: list[str] = []
|
|
194
170
|
models: list[ModelStep] = []
|
|
@@ -232,13 +208,9 @@ def search_order(
|
|
|
232
208
|
) -> tuple[list[str], list[ModelStep], bool]:
|
|
233
209
|
"""Select a harmonization order that minimizes the residual batch effect.
|
|
234
210
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
(``k >= 4``) also warns up front with the number of ComBat fits it will run.
|
|
239
|
-
|
|
240
|
-
Returns the chosen order, the fitted sequence for that order, and whether the
|
|
241
|
-
greedy fallback was used.
|
|
211
|
+
Exhaustive over all ``k!`` orderings when ``k <= max_exhaustive_vars`` (warning
|
|
212
|
+
from ``k >= 4``), else greedy with a warning. Returns the order, its fitted
|
|
213
|
+
sequence, and whether the greedy fallback was used.
|
|
242
214
|
"""
|
|
243
215
|
k = len(names)
|
|
244
216
|
if k <= max_exhaustive_vars:
|
|
@@ -270,16 +242,9 @@ def search_order(
|
|
|
270
242
|
class GMMGrouping:
|
|
271
243
|
"""A per-feature Gaussian-mixture grouping (*Horng et al.* 2022, GMM ComBat).
|
|
272
244
|
|
|
273
|
-
A 2-component
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
feature with the lowest AIC is retained. Its two-component assignment is the
|
|
277
|
-
latent grouping fed back into NestedComBat as an extra batch variable
|
|
278
|
-
(``+GMM``) or a protected covariate (``-GMM``).
|
|
279
|
-
|
|
280
|
-
The mixture is fit on the training data and stored, so held-out samples are
|
|
281
|
-
assigned by :meth:`assign` (``predict`` on the same fitted mixture), keeping
|
|
282
|
-
the grouping leakage-free.
|
|
245
|
+
A 2-component mixture is fit to each feature; among those whose smaller cluster
|
|
246
|
+
exceeds ``min_cluster_frac`` of the samples, the lowest-AIC one gives the grouping,
|
|
247
|
+
used as a batch variable (``+GMM``) or covariate (``-GMM``).
|
|
283
248
|
"""
|
|
284
249
|
|
|
285
250
|
def __init__(self, feature: object, gmm: GaussianMixture) -> None:
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Internal utilities shared across combatlearn modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .core import ArrayLike
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _fit_levels(values: pd.Series) -> pd.Index:
|
|
17
|
+
"""Levels a discrete covariate takes in the fitted rows (unused categories dropped)."""
|
|
18
|
+
levels: pd.Index = pd.Index(pd.Categorical(values).remove_unused_categories().categories)
|
|
19
|
+
return levels
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _code_levels(values: pd.Series, levels: pd.Index, label: str) -> pd.Categorical:
|
|
23
|
+
"""Code values against the levels frozen at fit; a level not seen at fit raises.
|
|
24
|
+
|
|
25
|
+
Dummy coding would otherwise turn it into an all-zero row, silently the reference level.
|
|
26
|
+
"""
|
|
27
|
+
codes = levels.get_indexer(pd.Index(values))
|
|
28
|
+
unseen = pd.unique(values[(codes == -1) & values.notna().to_numpy()])
|
|
29
|
+
if len(unseen):
|
|
30
|
+
raise ValueError(
|
|
31
|
+
f"{label} has level(s) {sorted(map(str, unseen))} that were "
|
|
32
|
+
f"not seen at fit. A level with no training samples has no estimated covariate "
|
|
33
|
+
f"effect, so it cannot be corrected; coding it as the reference level would "
|
|
34
|
+
f"silently apply the wrong one. Seen at fit: {sorted(map(str, levels))}."
|
|
35
|
+
)
|
|
36
|
+
return pd.Categorical.from_codes(codes.tolist(), categories=levels)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _check_positional_alignment(
|
|
40
|
+
X: ArrayLike, auxiliaries: Iterable[tuple[str, ArrayLike | None]]
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Require length-matched auxiliaries when ``X`` carries no index labels.
|
|
43
|
+
|
|
44
|
+
A plain-array ``X`` can only align positionally, which breaks inside CV when the
|
|
45
|
+
splitter subsets ``X`` but construction-time ``batch``/covariates stay full-length.
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(X, pd.DataFrame):
|
|
48
|
+
return
|
|
49
|
+
n = len(X)
|
|
50
|
+
for name, obj in auxiliaries:
|
|
51
|
+
if obj is None:
|
|
52
|
+
continue
|
|
53
|
+
if len(obj) != n:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"`{name}` has length {len(obj)} but X has {n} rows. When X is not a "
|
|
56
|
+
f"pandas DataFrame it has no index labels, so combatlearn can only align "
|
|
57
|
+
f"`{name}` to X positionally, which requires equal lengths. This typically "
|
|
58
|
+
f"happens inside cross-validation, where the splitter subsets X but the "
|
|
59
|
+
f"batch/covariates passed at construction stay full-length. Pass X and "
|
|
60
|
+
f"`{name}` as pandas objects sharing the same index so folds stay "
|
|
61
|
+
f"label-aligned, or pass a same-length `{name}`."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _subset(obj: ArrayLike | None, idx: pd.Index) -> pd.DataFrame | pd.Series | None:
|
|
66
|
+
"""Subset array-like object by index."""
|
|
67
|
+
if obj is None:
|
|
68
|
+
return None
|
|
69
|
+
if isinstance(obj, pd.Series | pd.DataFrame):
|
|
70
|
+
return obj.loc[idx]
|
|
71
|
+
else:
|
|
72
|
+
if isinstance(obj, np.ndarray) and obj.ndim == 1:
|
|
73
|
+
return pd.Series(obj, index=idx)
|
|
74
|
+
else:
|
|
75
|
+
return pd.DataFrame(obj, index=idx)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _warn_if_covariate_determines_target(
|
|
79
|
+
y: ArrayLike | None,
|
|
80
|
+
discrete_covariates: pd.DataFrame | pd.Series | None,
|
|
81
|
+
*,
|
|
82
|
+
estimator: str,
|
|
83
|
+
stacklevel: int = 4,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Warn when a discrete covariate perfectly determines ``y`` (likely the target).
|
|
86
|
+
|
|
87
|
+
Protecting the target inflates cross-validated scores. ``discrete_covariates``
|
|
88
|
+
must already be subset to the fitted rows; covariates averaging fewer than two
|
|
89
|
+
samples per level are skipped to avoid false alarms.
|
|
90
|
+
"""
|
|
91
|
+
if y is None or discrete_covariates is None:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
target = pd.Series(np.asarray(y).ravel())
|
|
95
|
+
if len(target) != len(discrete_covariates) or target.nunique(dropna=False) < 2:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
frame = (
|
|
99
|
+
discrete_covariates.to_frame()
|
|
100
|
+
if isinstance(discrete_covariates, pd.Series)
|
|
101
|
+
else discrete_covariates
|
|
102
|
+
)
|
|
103
|
+
n_samples = len(frame)
|
|
104
|
+
for column in frame.columns:
|
|
105
|
+
levels = pd.Series(np.asarray(frame[column]))
|
|
106
|
+
n_levels = levels.nunique(dropna=False)
|
|
107
|
+
if n_levels < 2 or n_levels * 2 > n_samples:
|
|
108
|
+
continue
|
|
109
|
+
if target.groupby(list(levels), dropna=False).nunique(dropna=False).max() > 1:
|
|
110
|
+
continue
|
|
111
|
+
warnings.warn(
|
|
112
|
+
f"{estimator}: discrete covariate {column!r} determines `y` on the training "
|
|
113
|
+
f"data, so this correction preserves the very signal you are about to predict, "
|
|
114
|
+
f"and `transform` applies it using each held-out sample's own label - which "
|
|
115
|
+
f"would not be available at prediction time. Any cross-validated score will be "
|
|
116
|
+
f"optimistic. If you are harmonizing a whole cohort for differential expression "
|
|
117
|
+
f"or methylation, use combatlearn.transductive.TransductiveComBat(condition=...), "
|
|
118
|
+
f"which protects the term and refuses `transform`. If {column!r} is genuinely "
|
|
119
|
+
f"known at intake and is not your target, this warning does not apply.",
|
|
120
|
+
UserWarning,
|
|
121
|
+
stacklevel=stacklevel,
|
|
122
|
+
)
|