MaldiBatchKit 0.1.0__py3-none-any.whl
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.
- maldibatchkit/__init__.py +56 -0
- maldibatchkit/_base.py +176 -0
- maldibatchkit/_cli_io.py +211 -0
- maldibatchkit/_utils.py +122 -0
- maldibatchkit/cli.py +497 -0
- maldibatchkit/corrections/__init__.py +20 -0
- maldibatchkit/corrections/baselines.py +235 -0
- maldibatchkit/corrections/combat.py +84 -0
- maldibatchkit/corrections/harmony.py +445 -0
- maldibatchkit/corrections/limma.py +165 -0
- maldibatchkit/corrections/maldi.py +167 -0
- maldibatchkit/corrections/quality_weighted.py +338 -0
- maldibatchkit/diagnostics/__init__.py +39 -0
- maldibatchkit/diagnostics/generic.py +327 -0
- maldibatchkit/diagnostics/maldi.py +165 -0
- maldibatchkit/diagnostics/report.py +151 -0
- maldibatchkit/integrations/__init__.py +9 -0
- maldibatchkit/integrations/maldiset.py +184 -0
- maldibatchkit/py.typed +0 -0
- maldibatchkit/viz/__init__.py +11 -0
- maldibatchkit/viz/peaks.py +82 -0
- maldibatchkit/viz/summary.py +155 -0
- maldibatchkit/viz/umap.py +118 -0
- maldibatchkit-0.1.0.dist-info/METADATA +344 -0
- maldibatchkit-0.1.0.dist-info/RECORD +29 -0
- maldibatchkit-0.1.0.dist-info/WHEEL +5 -0
- maldibatchkit-0.1.0.dist-info/entry_points.txt +2 -0
- maldibatchkit-0.1.0.dist-info/licenses/LICENSE +21 -0
- maldibatchkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Simple batch-correction baselines.
|
|
2
|
+
|
|
3
|
+
These are the "obvious" things to try before reaching for ComBat /
|
|
4
|
+
Harmony. They are useful as sanity-check baselines in benchmarks, and
|
|
5
|
+
they remain well-defined even when per-batch sample counts are tiny.
|
|
6
|
+
|
|
7
|
+
None of these methods models biological covariates. If you need to
|
|
8
|
+
preserve species structure while correcting batch, use
|
|
9
|
+
:class:`maldibatchkit.ComBat` (Fortin) or
|
|
10
|
+
:class:`maldibatchkit.SpeciesAwareComBat`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import numpy.typing as npt
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
from .._base import BaseBatchCorrector
|
|
22
|
+
from .._utils import ArrayLike
|
|
23
|
+
|
|
24
|
+
__all__ = ["MedianCentering", "ReferenceScaling", "ZScorePerBatch"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _per_batch_reduce(
|
|
28
|
+
X: pd.DataFrame,
|
|
29
|
+
batch: npt.NDArray[Any],
|
|
30
|
+
reducer,
|
|
31
|
+
) -> dict[Any, pd.Series]:
|
|
32
|
+
"""Apply ``reducer(X_b)`` for each distinct batch label."""
|
|
33
|
+
out: dict[Any, pd.Series] = {}
|
|
34
|
+
for lvl in np.unique(batch):
|
|
35
|
+
mask = batch == lvl
|
|
36
|
+
out[lvl] = reducer(X.loc[mask])
|
|
37
|
+
return out
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MedianCentering(BaseBatchCorrector):
|
|
41
|
+
"""Subtract per-batch medians from each feature.
|
|
42
|
+
|
|
43
|
+
The fitted parameter is a ``(n_batches, n_features)`` table of
|
|
44
|
+
medians. At ``transform`` time, the median for the batch each sample
|
|
45
|
+
belongs to is subtracted. Batches that were absent at ``fit`` time
|
|
46
|
+
fall back to the grand median learned from training data (with a
|
|
47
|
+
warning); this keeps the transformer defined on unseen folds without
|
|
48
|
+
leaking information from the test set.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
batch : array-like of shape (n_samples,)
|
|
53
|
+
Batch labels.
|
|
54
|
+
|
|
55
|
+
Attributes
|
|
56
|
+
----------
|
|
57
|
+
batch_medians_ : pd.DataFrame
|
|
58
|
+
Per-batch feature medians learned from the training data
|
|
59
|
+
(index: batch level, columns: feature names).
|
|
60
|
+
grand_median_ : pd.Series
|
|
61
|
+
Feature-wise median across all training rows. Used as a
|
|
62
|
+
fallback for unseen batch levels at transform time.
|
|
63
|
+
|
|
64
|
+
Examples
|
|
65
|
+
--------
|
|
66
|
+
>>> from maldibatchkit import MedianCentering
|
|
67
|
+
>>> corrector = MedianCentering(batch=batches)
|
|
68
|
+
>>> X_corrected = corrector.fit_transform(X)
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def _fit_impl(self, X_df: pd.DataFrame, batch: npt.NDArray[Any]) -> None:
|
|
72
|
+
meds = _per_batch_reduce(X_df, batch, lambda df: df.median(axis=0))
|
|
73
|
+
self.batch_medians_ = pd.DataFrame(meds).T
|
|
74
|
+
self.batch_medians_.index.name = "batch"
|
|
75
|
+
self.grand_median_ = X_df.median(axis=0)
|
|
76
|
+
|
|
77
|
+
def _transform_impl(
|
|
78
|
+
self, X_df: pd.DataFrame, batch: npt.NDArray[Any]
|
|
79
|
+
) -> pd.DataFrame:
|
|
80
|
+
out = X_df.copy().astype(float)
|
|
81
|
+
known = set(self.batch_medians_.index)
|
|
82
|
+
for lvl in np.unique(batch):
|
|
83
|
+
mask = batch == lvl
|
|
84
|
+
offset = (
|
|
85
|
+
self.batch_medians_.loc[lvl].to_numpy()
|
|
86
|
+
if lvl in known
|
|
87
|
+
else self.grand_median_.to_numpy()
|
|
88
|
+
)
|
|
89
|
+
out.loc[mask] = out.loc[mask].to_numpy() - offset
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ZScorePerBatch(BaseBatchCorrector):
|
|
94
|
+
"""Per-batch z-score normalisation.
|
|
95
|
+
|
|
96
|
+
For each training batch we learn a mean and standard deviation per
|
|
97
|
+
feature. At ``transform`` time we subtract the mean and divide by
|
|
98
|
+
the standard deviation of the sample's batch (again falling back to
|
|
99
|
+
grand statistics for unseen batches).
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
batch : array-like of shape (n_samples,)
|
|
104
|
+
Batch labels.
|
|
105
|
+
eps : float, default=1e-8
|
|
106
|
+
Floor applied to the standard deviation to avoid division by
|
|
107
|
+
zero on constant features.
|
|
108
|
+
|
|
109
|
+
Attributes
|
|
110
|
+
----------
|
|
111
|
+
batch_means_ : pd.DataFrame
|
|
112
|
+
Per-batch feature means.
|
|
113
|
+
batch_stds_ : pd.DataFrame
|
|
114
|
+
Per-batch feature standard deviations (floored at ``eps``).
|
|
115
|
+
grand_mean_ : pd.Series
|
|
116
|
+
Feature-wise mean across all training rows.
|
|
117
|
+
grand_std_ : pd.Series
|
|
118
|
+
Feature-wise standard deviation across all training rows.
|
|
119
|
+
|
|
120
|
+
Examples
|
|
121
|
+
--------
|
|
122
|
+
>>> from maldibatchkit import ZScorePerBatch
|
|
123
|
+
>>> corrector = ZScorePerBatch(batch=batches)
|
|
124
|
+
>>> X_corrected = corrector.fit_transform(X)
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, batch: ArrayLike, *, eps: float = 1e-8) -> None:
|
|
128
|
+
super().__init__(batch=batch)
|
|
129
|
+
self.eps = eps
|
|
130
|
+
|
|
131
|
+
def _fit_impl(self, X_df: pd.DataFrame, batch: npt.NDArray[Any]) -> None:
|
|
132
|
+
means = _per_batch_reduce(X_df, batch, lambda df: df.mean(axis=0))
|
|
133
|
+
stds = _per_batch_reduce(
|
|
134
|
+
X_df, batch, lambda df: df.std(axis=0, ddof=0).clip(lower=self.eps)
|
|
135
|
+
)
|
|
136
|
+
self.batch_means_ = pd.DataFrame(means).T
|
|
137
|
+
self.batch_stds_ = pd.DataFrame(stds).T
|
|
138
|
+
self.batch_means_.index.name = "batch"
|
|
139
|
+
self.batch_stds_.index.name = "batch"
|
|
140
|
+
self.grand_mean_ = X_df.mean(axis=0)
|
|
141
|
+
self.grand_std_ = X_df.std(axis=0, ddof=0).clip(lower=self.eps)
|
|
142
|
+
|
|
143
|
+
def _transform_impl(
|
|
144
|
+
self, X_df: pd.DataFrame, batch: npt.NDArray[Any]
|
|
145
|
+
) -> pd.DataFrame:
|
|
146
|
+
out = X_df.copy().astype(float)
|
|
147
|
+
known = set(self.batch_means_.index)
|
|
148
|
+
for lvl in np.unique(batch):
|
|
149
|
+
mask = batch == lvl
|
|
150
|
+
if lvl in known:
|
|
151
|
+
mean = self.batch_means_.loc[lvl].to_numpy()
|
|
152
|
+
std = self.batch_stds_.loc[lvl].to_numpy()
|
|
153
|
+
else:
|
|
154
|
+
mean = self.grand_mean_.to_numpy()
|
|
155
|
+
std = self.grand_std_.to_numpy()
|
|
156
|
+
out.loc[mask] = (out.loc[mask].to_numpy() - mean) / std
|
|
157
|
+
return out
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ReferenceScaling(BaseBatchCorrector):
|
|
161
|
+
"""Rescale each batch so its per-feature mean matches a reference batch.
|
|
162
|
+
|
|
163
|
+
Every non-reference batch is multiplied by
|
|
164
|
+
``reference_mean / batch_mean`` (feature-wise), which is a simple
|
|
165
|
+
multiplicative drift correction useful for mass-spectrometry
|
|
166
|
+
intensities where batch-specific gain terms are plausible. The
|
|
167
|
+
reference batch itself is left unchanged.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
batch : array-like of shape (n_samples,)
|
|
172
|
+
Batch labels.
|
|
173
|
+
reference_batch : str, optional
|
|
174
|
+
Batch level to use as the reference. If ``None``, the batch with
|
|
175
|
+
the most training samples is chosen.
|
|
176
|
+
eps : float, default=1e-8
|
|
177
|
+
Floor on the denominator mean to avoid division by zero.
|
|
178
|
+
|
|
179
|
+
Attributes
|
|
180
|
+
----------
|
|
181
|
+
reference_batch_ : Any
|
|
182
|
+
The batch level actually used as reference at ``fit`` time.
|
|
183
|
+
scale_factors_ : pd.DataFrame
|
|
184
|
+
Per-batch feature scale factors
|
|
185
|
+
(``reference_mean / batch_mean``).
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(
|
|
189
|
+
self,
|
|
190
|
+
batch: ArrayLike,
|
|
191
|
+
*,
|
|
192
|
+
reference_batch: Any | None = None,
|
|
193
|
+
eps: float = 1e-8,
|
|
194
|
+
) -> None:
|
|
195
|
+
super().__init__(batch=batch)
|
|
196
|
+
self.reference_batch = reference_batch
|
|
197
|
+
self.eps = eps
|
|
198
|
+
|
|
199
|
+
def _fit_impl(self, X_df: pd.DataFrame, batch: npt.NDArray[Any]) -> None:
|
|
200
|
+
levels, counts = np.unique(batch, return_counts=True)
|
|
201
|
+
if self.reference_batch is None:
|
|
202
|
+
ref = levels[int(np.argmax(counts))]
|
|
203
|
+
else:
|
|
204
|
+
ref = self.reference_batch
|
|
205
|
+
if ref not in levels:
|
|
206
|
+
raise ValueError(
|
|
207
|
+
f"reference_batch={ref!r} not found among training "
|
|
208
|
+
f"batch levels {list(levels)}."
|
|
209
|
+
)
|
|
210
|
+
self.reference_batch_ = ref
|
|
211
|
+
ref_mean = X_df.loc[batch == ref].mean(axis=0)
|
|
212
|
+
scales: dict[Any, pd.Series] = {}
|
|
213
|
+
for lvl in levels:
|
|
214
|
+
if lvl == ref:
|
|
215
|
+
scales[lvl] = pd.Series(np.ones(X_df.shape[1]), index=X_df.columns)
|
|
216
|
+
continue
|
|
217
|
+
lvl_mean = X_df.loc[batch == lvl].mean(axis=0)
|
|
218
|
+
denom = np.where(np.abs(lvl_mean) < self.eps, self.eps, lvl_mean)
|
|
219
|
+
scales[lvl] = pd.Series(ref_mean.to_numpy() / denom, index=X_df.columns)
|
|
220
|
+
self.scale_factors_ = pd.DataFrame(scales).T
|
|
221
|
+
self.scale_factors_.index.name = "batch"
|
|
222
|
+
|
|
223
|
+
def _transform_impl(
|
|
224
|
+
self, X_df: pd.DataFrame, batch: npt.NDArray[Any]
|
|
225
|
+
) -> pd.DataFrame:
|
|
226
|
+
out = X_df.copy().astype(float)
|
|
227
|
+
known = set(self.scale_factors_.index)
|
|
228
|
+
for lvl in np.unique(batch):
|
|
229
|
+
mask = batch == lvl
|
|
230
|
+
if lvl in known:
|
|
231
|
+
factor = self.scale_factors_.loc[lvl].to_numpy()
|
|
232
|
+
else:
|
|
233
|
+
factor = np.ones(X_df.shape[1])
|
|
234
|
+
out.loc[mask] = out.loc[mask].to_numpy() * factor
|
|
235
|
+
return out
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""ComBat variants and convenience wrappers.
|
|
2
|
+
|
|
3
|
+
The core ComBat implementation (Johnson 2007, Fortin 2018, Chen 2022 /
|
|
4
|
+
CovBat) lives in the :mod:`combatlearn` package. MaldiBatchKit re-exports
|
|
5
|
+
the sklearn-compatible ``ComBat`` transformer from ``combatlearn`` and adds
|
|
6
|
+
a small MALDI-specific convenience wrapper, :class:`SpeciesAwareComBat`,
|
|
7
|
+
which preselects the Fortin variant with ``species`` as a protected
|
|
8
|
+
biological covariate.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from combatlearn import ComBat
|
|
14
|
+
|
|
15
|
+
from .._utils import ArrayLike
|
|
16
|
+
|
|
17
|
+
__all__ = ["ComBat", "SpeciesAwareComBat"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SpeciesAwareComBat(ComBat):
|
|
21
|
+
"""ComBat-Fortin preset with species as a protected biological covariate.
|
|
22
|
+
|
|
23
|
+
This is a *thin* convenience wrapper: all the work is delegated to
|
|
24
|
+
:class:`combatlearn.ComBat` with ``method='fortin'`` and ``species``
|
|
25
|
+
plugged in as the ``discrete_covariates`` argument. It exists only so
|
|
26
|
+
MALDI users can write ``SpeciesAwareComBat(batch=..., species=...)``
|
|
27
|
+
instead of remembering which keyword corresponds to the covariate slot.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
batch : array-like of shape (n_samples,)
|
|
32
|
+
Batch labels for each sample.
|
|
33
|
+
species : array-like of shape (n_samples,)
|
|
34
|
+
Species labels for each sample. Passed to ``combatlearn``'s Fortin
|
|
35
|
+
variant as ``discrete_covariates`` so per-species biological
|
|
36
|
+
structure is preserved during correction.
|
|
37
|
+
continuous_covariates : array-like, optional
|
|
38
|
+
Additional continuous covariates to protect.
|
|
39
|
+
parametric : bool, default=True
|
|
40
|
+
Use parametric empirical Bayes.
|
|
41
|
+
mean_only : bool, default=False
|
|
42
|
+
Adjust only the mean (ignore variance).
|
|
43
|
+
reference_batch : str, optional
|
|
44
|
+
Batch level to leave unchanged.
|
|
45
|
+
eps : float, default=1e-8
|
|
46
|
+
Numerical jitter for stability.
|
|
47
|
+
|
|
48
|
+
Notes
|
|
49
|
+
-----
|
|
50
|
+
This class is deliberately minimal - it is **not** a new algorithm.
|
|
51
|
+
Calling ``SpeciesAwareComBat(batch=b, species=s).fit_transform(X)`` is
|
|
52
|
+
exactly equivalent to::
|
|
53
|
+
|
|
54
|
+
ComBat(batch=b, discrete_covariates=s, method='fortin').fit_transform(X)
|
|
55
|
+
|
|
56
|
+
Examples
|
|
57
|
+
--------
|
|
58
|
+
>>> from maldibatchkit import SpeciesAwareComBat
|
|
59
|
+
>>> corrector = SpeciesAwareComBat(batch=batches, species=species)
|
|
60
|
+
>>> X_corrected = corrector.fit_transform(X)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
batch: ArrayLike,
|
|
66
|
+
*,
|
|
67
|
+
species: ArrayLike,
|
|
68
|
+
continuous_covariates: ArrayLike | None = None,
|
|
69
|
+
parametric: bool = True,
|
|
70
|
+
mean_only: bool = False,
|
|
71
|
+
reference_batch: str | None = None,
|
|
72
|
+
eps: float = 1e-8,
|
|
73
|
+
) -> None:
|
|
74
|
+
super().__init__(
|
|
75
|
+
batch=batch,
|
|
76
|
+
discrete_covariates=species,
|
|
77
|
+
continuous_covariates=continuous_covariates,
|
|
78
|
+
method="fortin",
|
|
79
|
+
parametric=parametric,
|
|
80
|
+
mean_only=mean_only,
|
|
81
|
+
reference_batch=reference_batch,
|
|
82
|
+
eps=eps,
|
|
83
|
+
)
|
|
84
|
+
self.species = species
|