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.
@@ -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