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
maldibatchkit/cli.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
"""Command-line interface for MaldiBatchKit.
|
|
2
|
+
|
|
3
|
+
The CLI is organised as::
|
|
4
|
+
|
|
5
|
+
maldibatchkit
|
|
6
|
+
+-- correct
|
|
7
|
+
| +-- combat (Johnson 2007)
|
|
8
|
+
| +-- combat-fortin (Fortin 2018, covariate-aware)
|
|
9
|
+
| +-- combat-chen (Chen 2022, CovBat)
|
|
10
|
+
| +-- species-combat (Fortin preset with species)
|
|
11
|
+
| +-- quality-combat (weighted EB extension)
|
|
12
|
+
| +-- limma (Ritchie 2015)
|
|
13
|
+
| +-- harmony (Korsunsky 2019)
|
|
14
|
+
| +-- median-center
|
|
15
|
+
| +-- zscore-per-batch
|
|
16
|
+
| +-- reference-scaling
|
|
17
|
+
| +-- warping (BatchAwareWarping)
|
|
18
|
+
+-- diagnose
|
|
19
|
+
|
|
20
|
+
Every ``correct`` subcommand shares the same ``--input / --output /
|
|
21
|
+
--batch-csv`` contract and adds the flags that are meaningful for its
|
|
22
|
+
method (covariates, reference batch, parametric toggles, ...). Use
|
|
23
|
+
``maldibatchkit correct <method> --help`` to see the exact options for
|
|
24
|
+
a given corrector.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Annotated, Optional
|
|
31
|
+
|
|
32
|
+
import numpy as np
|
|
33
|
+
import pandas as pd
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
import typer
|
|
37
|
+
except ImportError as _typer_err: # pragma: no cover - typer is a hard dep
|
|
38
|
+
raise ImportError(
|
|
39
|
+
"The MaldiBatchKit CLI depends on `typer`. "
|
|
40
|
+
"Install it with `pip install typer[all]`."
|
|
41
|
+
) from _typer_err
|
|
42
|
+
|
|
43
|
+
from . import (
|
|
44
|
+
BatchAwareWarping,
|
|
45
|
+
ComBat,
|
|
46
|
+
Harmony,
|
|
47
|
+
Limma,
|
|
48
|
+
MedianCentering,
|
|
49
|
+
QualityWeightedComBat,
|
|
50
|
+
ReferenceScaling,
|
|
51
|
+
SpeciesAwareComBat,
|
|
52
|
+
ZScorePerBatch,
|
|
53
|
+
)
|
|
54
|
+
from ._cli_io import (
|
|
55
|
+
load_dataframe_csv,
|
|
56
|
+
load_labels_csv,
|
|
57
|
+
load_matrix,
|
|
58
|
+
load_mz_csv,
|
|
59
|
+
save_matrix,
|
|
60
|
+
)
|
|
61
|
+
from .diagnostics import diagnostic_report
|
|
62
|
+
|
|
63
|
+
app = typer.Typer(
|
|
64
|
+
name="maldibatchkit",
|
|
65
|
+
help="Batch-effect correction for MALDI-TOF AMR feature matrices.",
|
|
66
|
+
add_completion=False,
|
|
67
|
+
no_args_is_help=True,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
correct_app = typer.Typer(
|
|
71
|
+
name="correct",
|
|
72
|
+
help="Apply a batch-correction method to a feature matrix.",
|
|
73
|
+
add_completion=False,
|
|
74
|
+
no_args_is_help=True,
|
|
75
|
+
)
|
|
76
|
+
app.add_typer(correct_app)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
InputOpt = Annotated[
|
|
80
|
+
Path, typer.Option("-i", "--input", help="Feature matrix (CSV or NPZ).")
|
|
81
|
+
]
|
|
82
|
+
OutputOpt = Annotated[
|
|
83
|
+
Path, typer.Option("-o", "--output", help="Output path (CSV or NPZ).")
|
|
84
|
+
]
|
|
85
|
+
BatchOpt = Annotated[
|
|
86
|
+
Optional[Path],
|
|
87
|
+
typer.Option(
|
|
88
|
+
"--batch-csv",
|
|
89
|
+
help="Single-column CSV of batch labels (index = sample IDs).",
|
|
90
|
+
),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _run_and_save(
|
|
95
|
+
transformer, X: pd.DataFrame, output: Path, batch: pd.Series, extras: dict
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Fit-transform + save, preserving the input container shape."""
|
|
98
|
+
X_corrected = transformer.fit_transform(X)
|
|
99
|
+
if not isinstance(X_corrected, pd.DataFrame):
|
|
100
|
+
X_corrected = pd.DataFrame(
|
|
101
|
+
np.asarray(X_corrected), index=X.index, columns=X.columns
|
|
102
|
+
)
|
|
103
|
+
save_matrix(X_corrected, output, batch=batch, extras=extras)
|
|
104
|
+
typer.echo(f"wrote {output} ({X_corrected.shape[0]} x {X_corrected.shape[1]})")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _load_covariates(
|
|
108
|
+
discrete_csv: Path | None,
|
|
109
|
+
continuous_csv: Path | None,
|
|
110
|
+
index: pd.Index,
|
|
111
|
+
) -> tuple[pd.DataFrame | None, pd.DataFrame | None]:
|
|
112
|
+
discrete = load_dataframe_csv(discrete_csv, index) if discrete_csv else None
|
|
113
|
+
continuous = load_dataframe_csv(continuous_csv, index) if continuous_csv else None
|
|
114
|
+
return discrete, continuous
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@correct_app.command("combat")
|
|
118
|
+
def correct_combat(
|
|
119
|
+
input: InputOpt,
|
|
120
|
+
output: OutputOpt,
|
|
121
|
+
batch_csv: BatchOpt = None,
|
|
122
|
+
parametric: Annotated[
|
|
123
|
+
bool, typer.Option(help="Use parametric empirical Bayes.")
|
|
124
|
+
] = True,
|
|
125
|
+
mean_only: Annotated[
|
|
126
|
+
bool, typer.Option(help="Adjust only the mean, not the variance.")
|
|
127
|
+
] = False,
|
|
128
|
+
reference_batch: Annotated[
|
|
129
|
+
Optional[str],
|
|
130
|
+
typer.Option(help="Batch level to leave unchanged."),
|
|
131
|
+
] = None,
|
|
132
|
+
eps: Annotated[float, typer.Option(help="Numerical jitter.")] = 1e-8,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Vanilla Johnson (2007) ComBat. No covariates."""
|
|
135
|
+
X, batch, extras = load_matrix(input, batch_csv)
|
|
136
|
+
transformer = ComBat(
|
|
137
|
+
batch=batch,
|
|
138
|
+
method="johnson",
|
|
139
|
+
parametric=parametric,
|
|
140
|
+
mean_only=mean_only,
|
|
141
|
+
reference_batch=reference_batch,
|
|
142
|
+
eps=eps,
|
|
143
|
+
)
|
|
144
|
+
_run_and_save(transformer, X, output, batch, extras)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@correct_app.command("combat-fortin")
|
|
148
|
+
def correct_combat_fortin(
|
|
149
|
+
input: InputOpt,
|
|
150
|
+
output: OutputOpt,
|
|
151
|
+
batch_csv: BatchOpt = None,
|
|
152
|
+
discrete_covariates_csv: Annotated[
|
|
153
|
+
Optional[Path],
|
|
154
|
+
typer.Option(
|
|
155
|
+
"--discrete-covariates-csv",
|
|
156
|
+
help="CSV of categorical covariates to protect "
|
|
157
|
+
"(index = sample IDs, any number of columns).",
|
|
158
|
+
),
|
|
159
|
+
] = None,
|
|
160
|
+
continuous_covariates_csv: Annotated[
|
|
161
|
+
Optional[Path],
|
|
162
|
+
typer.Option(
|
|
163
|
+
"--continuous-covariates-csv",
|
|
164
|
+
help="CSV of continuous covariates to protect.",
|
|
165
|
+
),
|
|
166
|
+
] = None,
|
|
167
|
+
parametric: Annotated[bool, typer.Option()] = True,
|
|
168
|
+
mean_only: Annotated[bool, typer.Option()] = False,
|
|
169
|
+
reference_batch: Annotated[Optional[str], typer.Option()] = None,
|
|
170
|
+
eps: Annotated[float, typer.Option()] = 1e-8,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Fortin (2018) covariate-aware ComBat."""
|
|
173
|
+
X, batch, extras = load_matrix(input, batch_csv)
|
|
174
|
+
discrete, continuous = _load_covariates(
|
|
175
|
+
discrete_covariates_csv, continuous_covariates_csv, X.index
|
|
176
|
+
)
|
|
177
|
+
if discrete is None and continuous is None:
|
|
178
|
+
raise typer.BadParameter(
|
|
179
|
+
"combat-fortin requires at least one of "
|
|
180
|
+
"--discrete-covariates-csv / --continuous-covariates-csv. "
|
|
181
|
+
"Without covariates this reduces to Johnson; use "
|
|
182
|
+
"`maldibatchkit correct combat` instead."
|
|
183
|
+
)
|
|
184
|
+
transformer = ComBat(
|
|
185
|
+
batch=batch,
|
|
186
|
+
discrete_covariates=discrete,
|
|
187
|
+
continuous_covariates=continuous,
|
|
188
|
+
method="fortin",
|
|
189
|
+
parametric=parametric,
|
|
190
|
+
mean_only=mean_only,
|
|
191
|
+
reference_batch=reference_batch,
|
|
192
|
+
eps=eps,
|
|
193
|
+
)
|
|
194
|
+
_run_and_save(transformer, X, output, batch, extras)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@correct_app.command("combat-chen")
|
|
198
|
+
def correct_combat_chen(
|
|
199
|
+
input: InputOpt,
|
|
200
|
+
output: OutputOpt,
|
|
201
|
+
batch_csv: BatchOpt = None,
|
|
202
|
+
discrete_covariates_csv: Annotated[Optional[Path], typer.Option()] = None,
|
|
203
|
+
continuous_covariates_csv: Annotated[Optional[Path], typer.Option()] = None,
|
|
204
|
+
covbat_cov_thresh: Annotated[
|
|
205
|
+
float,
|
|
206
|
+
typer.Option(
|
|
207
|
+
help="Variance threshold in (0, 1] for PCs, or int >= 1 for a "
|
|
208
|
+
"fixed number of components."
|
|
209
|
+
),
|
|
210
|
+
] = 0.9,
|
|
211
|
+
parametric: Annotated[bool, typer.Option()] = True,
|
|
212
|
+
mean_only: Annotated[bool, typer.Option()] = False,
|
|
213
|
+
reference_batch: Annotated[Optional[str], typer.Option()] = None,
|
|
214
|
+
eps: Annotated[float, typer.Option()] = 1e-8,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""Chen (2022) CovBat: ComBat on PCA-decomposed batch covariance."""
|
|
217
|
+
X, batch, extras = load_matrix(input, batch_csv)
|
|
218
|
+
discrete, continuous = _load_covariates(
|
|
219
|
+
discrete_covariates_csv, continuous_covariates_csv, X.index
|
|
220
|
+
)
|
|
221
|
+
transformer = ComBat(
|
|
222
|
+
batch=batch,
|
|
223
|
+
discrete_covariates=discrete,
|
|
224
|
+
continuous_covariates=continuous,
|
|
225
|
+
method="chen",
|
|
226
|
+
parametric=parametric,
|
|
227
|
+
mean_only=mean_only,
|
|
228
|
+
reference_batch=reference_batch,
|
|
229
|
+
eps=eps,
|
|
230
|
+
covbat_cov_thresh=covbat_cov_thresh,
|
|
231
|
+
)
|
|
232
|
+
_run_and_save(transformer, X, output, batch, extras)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@correct_app.command("species-combat")
|
|
236
|
+
def correct_species_combat(
|
|
237
|
+
input: InputOpt,
|
|
238
|
+
output: OutputOpt,
|
|
239
|
+
batch_csv: BatchOpt = None,
|
|
240
|
+
species_csv: Annotated[
|
|
241
|
+
Optional[Path],
|
|
242
|
+
typer.Option(
|
|
243
|
+
"--species-csv",
|
|
244
|
+
help="Single-column CSV with species labels "
|
|
245
|
+
"(index = sample IDs). Required.",
|
|
246
|
+
),
|
|
247
|
+
] = None,
|
|
248
|
+
continuous_covariates_csv: Annotated[Optional[Path], typer.Option()] = None,
|
|
249
|
+
parametric: Annotated[bool, typer.Option()] = True,
|
|
250
|
+
mean_only: Annotated[bool, typer.Option()] = False,
|
|
251
|
+
reference_batch: Annotated[Optional[str], typer.Option()] = None,
|
|
252
|
+
eps: Annotated[float, typer.Option()] = 1e-8,
|
|
253
|
+
) -> None:
|
|
254
|
+
"""Run ComBat-Fortin with species as the protected covariate."""
|
|
255
|
+
X, batch, extras = load_matrix(input, batch_csv)
|
|
256
|
+
if species_csv is None:
|
|
257
|
+
raise typer.BadParameter("species-combat requires --species-csv.")
|
|
258
|
+
species = load_labels_csv(species_csv, X.index)
|
|
259
|
+
continuous = (
|
|
260
|
+
load_dataframe_csv(continuous_covariates_csv, X.index)
|
|
261
|
+
if continuous_covariates_csv
|
|
262
|
+
else None
|
|
263
|
+
)
|
|
264
|
+
transformer = SpeciesAwareComBat(
|
|
265
|
+
batch=batch,
|
|
266
|
+
species=species,
|
|
267
|
+
continuous_covariates=continuous,
|
|
268
|
+
parametric=parametric,
|
|
269
|
+
mean_only=mean_only,
|
|
270
|
+
reference_batch=reference_batch,
|
|
271
|
+
eps=eps,
|
|
272
|
+
)
|
|
273
|
+
_run_and_save(transformer, X, output, batch, extras)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@correct_app.command("quality-combat")
|
|
277
|
+
def correct_quality_combat(
|
|
278
|
+
input: InputOpt,
|
|
279
|
+
output: OutputOpt,
|
|
280
|
+
batch_csv: BatchOpt = None,
|
|
281
|
+
quality_csv: Annotated[
|
|
282
|
+
Optional[Path],
|
|
283
|
+
typer.Option(
|
|
284
|
+
"--quality-csv",
|
|
285
|
+
help="Single-column CSV with non-negative per-sample weights "
|
|
286
|
+
"(typically SNR). Required.",
|
|
287
|
+
),
|
|
288
|
+
] = None,
|
|
289
|
+
parametric: Annotated[bool, typer.Option()] = True,
|
|
290
|
+
reference_batch: Annotated[Optional[str], typer.Option()] = None,
|
|
291
|
+
max_iter: Annotated[int, typer.Option(help="Parametric EB iteration cap.")] = 50,
|
|
292
|
+
tol: Annotated[float, typer.Option(help="Convergence tolerance.")] = 1e-4,
|
|
293
|
+
eps: Annotated[float, typer.Option()] = 1e-8,
|
|
294
|
+
) -> None:
|
|
295
|
+
"""Quality-weighted empirical-Bayes ComBat."""
|
|
296
|
+
X, batch, extras = load_matrix(input, batch_csv)
|
|
297
|
+
if quality_csv is None:
|
|
298
|
+
raise typer.BadParameter("quality-combat requires --quality-csv.")
|
|
299
|
+
quality = load_labels_csv(quality_csv, X.index)
|
|
300
|
+
transformer = QualityWeightedComBat(
|
|
301
|
+
batch=batch,
|
|
302
|
+
quality=quality,
|
|
303
|
+
parametric=parametric,
|
|
304
|
+
reference_batch=reference_batch,
|
|
305
|
+
eps=eps,
|
|
306
|
+
max_iter=max_iter,
|
|
307
|
+
tol=tol,
|
|
308
|
+
)
|
|
309
|
+
_run_and_save(transformer, X, output, batch, extras)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@correct_app.command("limma")
|
|
313
|
+
def correct_limma(
|
|
314
|
+
input: InputOpt,
|
|
315
|
+
output: OutputOpt,
|
|
316
|
+
batch_csv: BatchOpt = None,
|
|
317
|
+
design_csv: Annotated[
|
|
318
|
+
Optional[Path],
|
|
319
|
+
typer.Option(
|
|
320
|
+
"--design-csv",
|
|
321
|
+
help="CSV with covariates to protect (index = sample IDs, "
|
|
322
|
+
"any number of numeric columns).",
|
|
323
|
+
),
|
|
324
|
+
] = None,
|
|
325
|
+
eps: Annotated[float, typer.Option(help="Ridge regularisation.")] = 1e-8,
|
|
326
|
+
) -> None:
|
|
327
|
+
"""limma::removeBatchEffect (Ritchie 2015)."""
|
|
328
|
+
X, batch, extras = load_matrix(input, batch_csv)
|
|
329
|
+
design = load_dataframe_csv(design_csv, X.index) if design_csv else None
|
|
330
|
+
transformer = Limma(batch=batch, design=design, eps=eps)
|
|
331
|
+
_run_and_save(transformer, X, output, batch, extras)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@correct_app.command("harmony")
|
|
335
|
+
def correct_harmony(
|
|
336
|
+
input: InputOpt,
|
|
337
|
+
output: OutputOpt,
|
|
338
|
+
batch_csv: BatchOpt = None,
|
|
339
|
+
covariates_csv: Annotated[
|
|
340
|
+
Optional[Path],
|
|
341
|
+
typer.Option(
|
|
342
|
+
"--covariates-csv",
|
|
343
|
+
help="Extra categorical covariates forwarded to harmonypy "
|
|
344
|
+
"vars_use (index = sample IDs).",
|
|
345
|
+
),
|
|
346
|
+
] = None,
|
|
347
|
+
theta: Annotated[float, typer.Option(help="Diversity clustering penalty.")] = 2.0,
|
|
348
|
+
max_iter: Annotated[int, typer.Option(help="Harmony iterations.")] = 20,
|
|
349
|
+
random_state: Annotated[Optional[int], typer.Option()] = None,
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Harmony (Korsunsky 2019) via harmonypy. Train/test safe via closed-form transform."""
|
|
352
|
+
X, batch, extras = load_matrix(input, batch_csv)
|
|
353
|
+
covariates = load_dataframe_csv(covariates_csv, X.index) if covariates_csv else None
|
|
354
|
+
transformer = Harmony(
|
|
355
|
+
batch=batch,
|
|
356
|
+
covariates=covariates,
|
|
357
|
+
theta=theta,
|
|
358
|
+
max_iter=max_iter,
|
|
359
|
+
random_state=random_state,
|
|
360
|
+
)
|
|
361
|
+
_run_and_save(transformer, X, output, batch, extras)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@correct_app.command("median-center")
|
|
365
|
+
def correct_median_center(
|
|
366
|
+
input: InputOpt,
|
|
367
|
+
output: OutputOpt,
|
|
368
|
+
batch_csv: BatchOpt = None,
|
|
369
|
+
) -> None:
|
|
370
|
+
"""Per-batch median centering."""
|
|
371
|
+
X, batch, extras = load_matrix(input, batch_csv)
|
|
372
|
+
transformer = MedianCentering(batch=batch)
|
|
373
|
+
_run_and_save(transformer, X, output, batch, extras)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@correct_app.command("zscore-per-batch")
|
|
377
|
+
def correct_zscore_per_batch(
|
|
378
|
+
input: InputOpt,
|
|
379
|
+
output: OutputOpt,
|
|
380
|
+
batch_csv: BatchOpt = None,
|
|
381
|
+
eps: Annotated[float, typer.Option()] = 1e-8,
|
|
382
|
+
) -> None:
|
|
383
|
+
"""Per-batch z-score normalisation."""
|
|
384
|
+
X, batch, extras = load_matrix(input, batch_csv)
|
|
385
|
+
transformer = ZScorePerBatch(batch=batch, eps=eps)
|
|
386
|
+
_run_and_save(transformer, X, output, batch, extras)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@correct_app.command("reference-scaling")
|
|
390
|
+
def correct_reference_scaling(
|
|
391
|
+
input: InputOpt,
|
|
392
|
+
output: OutputOpt,
|
|
393
|
+
batch_csv: BatchOpt = None,
|
|
394
|
+
reference_batch: Annotated[
|
|
395
|
+
Optional[str],
|
|
396
|
+
typer.Option(
|
|
397
|
+
help="Batch level to use as reference. Defaults to the "
|
|
398
|
+
"largest training batch."
|
|
399
|
+
),
|
|
400
|
+
] = None,
|
|
401
|
+
eps: Annotated[float, typer.Option()] = 1e-8,
|
|
402
|
+
) -> None:
|
|
403
|
+
"""Multiplicative per-batch scaling to a reference."""
|
|
404
|
+
X, batch, extras = load_matrix(input, batch_csv)
|
|
405
|
+
transformer = ReferenceScaling(
|
|
406
|
+
batch=batch, reference_batch=reference_batch, eps=eps
|
|
407
|
+
)
|
|
408
|
+
_run_and_save(transformer, X, output, batch, extras)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@correct_app.command("warping")
|
|
412
|
+
def correct_warping(
|
|
413
|
+
input: InputOpt,
|
|
414
|
+
output: OutputOpt,
|
|
415
|
+
batch_csv: BatchOpt = None,
|
|
416
|
+
reference: Annotated[
|
|
417
|
+
str,
|
|
418
|
+
typer.Option(help="Global reference: 'median' or 'batch_mean'."),
|
|
419
|
+
] = "median",
|
|
420
|
+
method: Annotated[
|
|
421
|
+
str,
|
|
422
|
+
typer.Option(help="Warping strategy: shift / linear / piecewise / dtw."),
|
|
423
|
+
] = "shift",
|
|
424
|
+
n_segments: Annotated[int, typer.Option()] = 5,
|
|
425
|
+
max_shift: Annotated[int, typer.Option()] = 50,
|
|
426
|
+
dtw_radius: Annotated[int, typer.Option()] = 10,
|
|
427
|
+
smooth_sigma: Annotated[float, typer.Option()] = 2.0,
|
|
428
|
+
n_jobs: Annotated[int, typer.Option()] = 1,
|
|
429
|
+
) -> None:
|
|
430
|
+
"""Batch-aware m/z warping onto a shared global reference."""
|
|
431
|
+
X, batch, extras = load_matrix(input, batch_csv)
|
|
432
|
+
transformer = BatchAwareWarping(
|
|
433
|
+
batch=batch,
|
|
434
|
+
reference=reference,
|
|
435
|
+
method=method,
|
|
436
|
+
n_segments=n_segments,
|
|
437
|
+
max_shift=max_shift,
|
|
438
|
+
dtw_radius=dtw_radius,
|
|
439
|
+
smooth_sigma=smooth_sigma,
|
|
440
|
+
n_jobs=n_jobs,
|
|
441
|
+
)
|
|
442
|
+
_run_and_save(transformer, X, output, batch, extras)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@app.command("diagnose")
|
|
446
|
+
def diagnose(
|
|
447
|
+
input: InputOpt,
|
|
448
|
+
output: OutputOpt,
|
|
449
|
+
batch_csv: BatchOpt = None,
|
|
450
|
+
corrected: Annotated[
|
|
451
|
+
Optional[Path],
|
|
452
|
+
typer.Option(
|
|
453
|
+
"--corrected",
|
|
454
|
+
help="Corrected matrix (CSV or NPZ). If omitted, the "
|
|
455
|
+
"report compares the input to itself.",
|
|
456
|
+
),
|
|
457
|
+
] = None,
|
|
458
|
+
mz_csv: Annotated[
|
|
459
|
+
Optional[Path],
|
|
460
|
+
typer.Option(
|
|
461
|
+
"--mz-csv",
|
|
462
|
+
help="m/z values (single-column CSV of length n_features) "
|
|
463
|
+
"used by the peak-drift metric.",
|
|
464
|
+
),
|
|
465
|
+
] = None,
|
|
466
|
+
top_k_peaks: Annotated[
|
|
467
|
+
int, typer.Option(help="Number of peaks tracked for drift.")
|
|
468
|
+
] = 50,
|
|
469
|
+
lisi_perplexity: Annotated[float, typer.Option(help="Perplexity for LISI.")] = 30.0,
|
|
470
|
+
kbet_k: Annotated[
|
|
471
|
+
Optional[int],
|
|
472
|
+
typer.Option("--kbet-k", help="Neighbourhood size for kBET."),
|
|
473
|
+
] = None,
|
|
474
|
+
) -> None:
|
|
475
|
+
"""Run the full diagnostic report and write a tidy CSV."""
|
|
476
|
+
before, batch, _ = load_matrix(input, batch_csv)
|
|
477
|
+
if corrected is not None:
|
|
478
|
+
after, _, _ = load_matrix(corrected, batch_csv)
|
|
479
|
+
else:
|
|
480
|
+
after = before.copy()
|
|
481
|
+
mz_values = load_mz_csv(mz_csv, before.shape[1]) if mz_csv else None
|
|
482
|
+
report = diagnostic_report(
|
|
483
|
+
before,
|
|
484
|
+
after,
|
|
485
|
+
batch,
|
|
486
|
+
mz_values=mz_values,
|
|
487
|
+
k=kbet_k,
|
|
488
|
+
lisi_perplexity=lisi_perplexity,
|
|
489
|
+
top_k_peaks=top_k_peaks,
|
|
490
|
+
)
|
|
491
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
492
|
+
report.to_csv(output, index=False)
|
|
493
|
+
typer.echo(f"wrote {output} ({len(report)} rows)")
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
if __name__ == "__main__": # pragma: no cover
|
|
497
|
+
app()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Batch-effect correction transformers (all train/test safe)."""
|
|
2
|
+
|
|
3
|
+
from .baselines import MedianCentering, ReferenceScaling, ZScorePerBatch
|
|
4
|
+
from .combat import ComBat, SpeciesAwareComBat
|
|
5
|
+
from .harmony import Harmony
|
|
6
|
+
from .limma import Limma
|
|
7
|
+
from .maldi import BatchAwareWarping
|
|
8
|
+
from .quality_weighted import QualityWeightedComBat
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BatchAwareWarping",
|
|
12
|
+
"ComBat",
|
|
13
|
+
"Harmony",
|
|
14
|
+
"Limma",
|
|
15
|
+
"MedianCentering",
|
|
16
|
+
"QualityWeightedComBat",
|
|
17
|
+
"ReferenceScaling",
|
|
18
|
+
"SpeciesAwareComBat",
|
|
19
|
+
"ZScorePerBatch",
|
|
20
|
+
]
|