MaldiAMRKit 0.22.0__tar.gz → 0.22.1__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.
Files changed (108) hide show
  1. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1/MaldiAMRKit.egg-info}/PKG-INFO +5 -4
  2. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/MaldiAMRKit.egg-info/SOURCES.txt +3 -0
  3. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/MaldiAMRKit.egg-info/requires.txt +1 -0
  4. {maldiamrkit-0.22.0/MaldiAMRKit.egg-info → maldiamrkit-0.22.1}/PKG-INFO +5 -4
  5. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/README.md +3 -3
  6. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/__init__.py +1 -1
  7. maldiamrkit-0.22.1/maldiamrkit/alignment/_dtw.py +82 -0
  8. maldiamrkit-0.22.1/maldiamrkit/alignment/_versioning.py +87 -0
  9. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/alignment/peak_align.py +9 -4
  10. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/alignment/raw_warping.py +117 -13
  11. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/alignment/strategies.py +56 -23
  12. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/alignment/warping.py +23 -3
  13. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/cli.py +19 -1
  14. maldiamrkit-0.22.1/maldiamrkit/data/_metadata.py +33 -0
  15. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/builder.py +1 -1
  16. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/dataset_layouts.py +168 -53
  17. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/duplicates.py +35 -21
  18. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/input_layouts.py +6 -4
  19. maldiamrkit-0.22.1/maldiamrkit/data/loader.py +405 -0
  20. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/dataset.py +164 -46
  21. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/detection/peak_detector.py +21 -8
  22. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/detection/peaklist.py +74 -18
  23. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/differential/analysis.py +14 -0
  24. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/drift/monitor.py +27 -5
  25. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/drift/peak_drift.py +9 -2
  26. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/evaluation/metrics.py +103 -29
  27. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/evaluation/mic_regression.py +18 -7
  28. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/evaluation/splitting.py +149 -25
  29. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/filters.py +15 -8
  30. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/io/readers.py +31 -4
  31. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/preprocessing/binning.py +16 -9
  32. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/preprocessing/merging.py +11 -8
  33. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/preprocessing/quality.py +24 -20
  34. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/preprocessing/transformers.py +7 -5
  35. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/susceptibility/breakpoint.py +22 -6
  36. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/susceptibility/label_encoder.py +15 -1
  37. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/susceptibility/mic_encoder.py +21 -6
  38. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/requirements.txt +3 -0
  39. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/tests/test_peak_detector.py +44 -0
  40. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/tests/test_peaklist.py +119 -0
  41. maldiamrkit-0.22.0/maldiamrkit/data/loader.py +0 -250
  42. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/LICENSE +0 -0
  43. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/MaldiAMRKit.egg-info/dependency_links.txt +0 -0
  44. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/MaldiAMRKit.egg-info/entry_points.txt +0 -0
  45. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/MaldiAMRKit.egg-info/top_level.txt +0 -0
  46. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/_compat.py +0 -0
  47. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/_registry.py +0 -0
  48. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/alignment/__init__.py +0 -0
  49. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/__init__.py +0 -0
  50. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/README.md +0 -0
  51. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v1.0.yaml +0 -0
  52. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v1.1.yaml +0 -0
  53. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v1.2.yaml +0 -0
  54. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v1.3.yaml +0 -0
  55. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v10.0.yaml +0 -0
  56. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v11.0.yaml +0 -0
  57. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v12.0.yaml +0 -0
  58. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v13.1.yaml +0 -0
  59. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v14.0.yaml +0 -0
  60. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v15.0.yaml +0 -0
  61. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v16.0.yaml +0 -0
  62. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v2.0.yaml +0 -0
  63. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v4.0.yaml +0 -0
  64. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v5.0.yaml +0 -0
  65. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v6.0.yaml +0 -0
  66. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v7.1.yaml +0 -0
  67. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v8.0.yaml +0 -0
  68. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v8.1.yaml +0 -0
  69. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/eucast/eucast_v9.0.yaml +0 -0
  70. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/breakpoints/example.yaml +0 -0
  71. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/data/site_info.py +0 -0
  72. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/detection/__init__.py +0 -0
  73. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/differential/__init__.py +0 -0
  74. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/differential/plots.py +0 -0
  75. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/differential/stats.py +0 -0
  76. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/drift/__init__.py +0 -0
  77. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/drift/pca_drift.py +0 -0
  78. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/drift/plots.py +0 -0
  79. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/drift/reference.py +0 -0
  80. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/evaluation/__init__.py +0 -0
  81. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/io/__init__.py +0 -0
  82. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/io/mic.py +0 -0
  83. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/io/writers.py +0 -0
  84. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/preprocessing/__init__.py +0 -0
  85. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/preprocessing/pipeline.py +0 -0
  86. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/preprocessing/preprocessing_pipeline.py +0 -0
  87. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/py.typed +0 -0
  88. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/similarity/__init__.py +0 -0
  89. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/similarity/clustering.py +0 -0
  90. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/similarity/metrics.py +0 -0
  91. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/similarity/pairwise.py +0 -0
  92. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/similarity/plots.py +0 -0
  93. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/spectrum.py +0 -0
  94. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/susceptibility/__init__.py +0 -0
  95. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/visualization/__init__.py +0 -0
  96. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/visualization/_common.py +0 -0
  97. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/visualization/alignment_plots.py +0 -0
  98. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/visualization/exploratory_plots.py +0 -0
  99. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/visualization/peak_plots.py +0 -0
  100. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/maldiamrkit/visualization/spectrum_plots.py +0 -0
  101. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/pyproject.toml +0 -0
  102. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/requirements-batch.txt +0 -0
  103. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/requirements-dev.txt +0 -0
  104. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/requirements-docs.txt +0 -0
  105. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/requirements-formats.txt +0 -0
  106. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/setup.cfg +0 -0
  107. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/tests/test_compat.py +0 -0
  108. {maldiamrkit-0.22.0 → maldiamrkit-0.22.1}/tests/test_numerical_edge_cases.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MaldiAMRKit
3
- Version: 0.22.0
3
+ Version: 0.22.1
4
4
  Summary: A comprehensive toolkit for MALDI-TOF mass spectrometry data preprocessing for antimicrobial resistance (AMR) prediction purposes
5
5
  Author-email: Ettore Rocchi <ettore.rocchi3@unibo.it>
6
6
  License-Expression: MIT
@@ -31,6 +31,7 @@ Requires-Dist: seaborn>=0.11.0
31
31
  Requires-Dist: pybaselines>=1.0.0
32
32
  Requires-Dist: gudhi>=3.5.0
33
33
  Requires-Dist: tslearn>=0.6.0
34
+ Requires-Dist: numba>=0.55
34
35
  Requires-Dist: pyyaml>=6.0
35
36
  Requires-Dist: pyarrow>=14.0.0
36
37
  Requires-Dist: typer>=0.9.0
@@ -143,7 +144,7 @@ pip install -e .[dev]
143
144
  - **Spectral Alignment**: Shift, linear, piecewise, DTW, quadratic, cubic, and LOWESS warping for both binned and raw full-resolution spectra. Spectra are first shifted and then aligned to the reference of the shifted spectra, a non-rigid warp is kept only when the matched peaks support it, and resampling preserves the total intensity; spectra of several species are best aligned one species at a time
144
145
  - **Peak Detection**: Local maxima and persistent homology methods
145
146
  - **Peak Sets**: `PeakSet` / `PeakList` represent each spectrum as a variable-length `(m/z, intensity)` set for peak-set models, via `MaldiPeakDetector.transform_peaklist()` on binned spectra or `create_peakset_input()` at full m/z resolution; extraction is stateless per spectrum, so results are cacheable without leakage
146
- - **Fit-Free Peak Alignment**: `align_peaks()` warps a peak set onto caller-supplied reference peaks, so the reference stays fitted on the training split
147
+ - **Fit-Free Peak Alignment**: `align_peaks()` warps a peak set onto caller-supplied reference peaks, which the caller estimates on the training split (for example `RawWarping(...).fit(X_train).ref_peaks_mz_`; a reference built with test spectra leaks)
147
148
 
148
149
  ### Extensibility
149
150
  - **Component Registries**: Plug your own components into the name-dispatched machinery - `register_spectral_metric()` for distance metrics (usable in `spectral_distance`, `pairwise_distances`, and `DriftMonitor`), `register_transformer()` to make `PreprocessingPipeline` configs holding custom transformers round-trip through JSON/YAML, and `register_binning_method()` for custom bin edges
@@ -162,7 +163,7 @@ pip install -e .[dev]
162
163
 
163
164
  ### Differential Analysis
164
165
  - **`DifferentialAnalysis`**: Per-bin statistical testing (Mann-Whitney U, Welch's t-test) between resistant and susceptible groups, with multiple-testing correction, log2 fold change, and Cohen's d effect size
165
- - **Peak Selection**: `top_peaks()` by adjusted p-value, `significant_peaks()` with fold-change and p-value thresholds, `compare_drugs()` for multi-drug boolean significance matrices
166
+ - **Peak Ranking**: `top_peaks()` by adjusted p-value, `significant_peaks()` with fold-change and p-value thresholds, `compare_drugs()` for multi-drug boolean significance matrices (to select features for a classifier, rank peaks inside each training fold)
166
167
  - **AMR-Aware Plots**: `plot_volcano()`, `plot_manhattan()` along the m/z axis, and `plot_drug_comparison()` with binary heatmap or UpSet-style intersection view
167
168
 
168
169
  ### Drift Monitoring
@@ -246,7 +247,7 @@ pipe = Pipeline([
246
247
  ("clf", RandomForestClassifier(n_estimators=100, random_state=42))
247
248
  ])
248
249
 
249
- # Cross-validation
250
+ # Cross-validation (with replicates, use StratifiedGroupKFold and groups=data.groups)
250
251
  scores = cross_val_score(pipe, X, y, cv=5, scoring="accuracy")
251
252
  print(f"CV Accuracy: {scores.mean():.3f} +/- {scores.std():.3f}")
252
253
  ```
@@ -21,11 +21,14 @@ maldiamrkit/filters.py
21
21
  maldiamrkit/py.typed
22
22
  maldiamrkit/spectrum.py
23
23
  maldiamrkit/alignment/__init__.py
24
+ maldiamrkit/alignment/_dtw.py
25
+ maldiamrkit/alignment/_versioning.py
24
26
  maldiamrkit/alignment/peak_align.py
25
27
  maldiamrkit/alignment/raw_warping.py
26
28
  maldiamrkit/alignment/strategies.py
27
29
  maldiamrkit/alignment/warping.py
28
30
  maldiamrkit/data/__init__.py
31
+ maldiamrkit/data/_metadata.py
29
32
  maldiamrkit/data/builder.py
30
33
  maldiamrkit/data/dataset_layouts.py
31
34
  maldiamrkit/data/duplicates.py
@@ -7,6 +7,7 @@ seaborn>=0.11.0
7
7
  pybaselines>=1.0.0
8
8
  gudhi>=3.5.0
9
9
  tslearn>=0.6.0
10
+ numba>=0.55
10
11
  pyyaml>=6.0
11
12
  pyarrow>=14.0.0
12
13
  typer>=0.9.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MaldiAMRKit
3
- Version: 0.22.0
3
+ Version: 0.22.1
4
4
  Summary: A comprehensive toolkit for MALDI-TOF mass spectrometry data preprocessing for antimicrobial resistance (AMR) prediction purposes
5
5
  Author-email: Ettore Rocchi <ettore.rocchi3@unibo.it>
6
6
  License-Expression: MIT
@@ -31,6 +31,7 @@ Requires-Dist: seaborn>=0.11.0
31
31
  Requires-Dist: pybaselines>=1.0.0
32
32
  Requires-Dist: gudhi>=3.5.0
33
33
  Requires-Dist: tslearn>=0.6.0
34
+ Requires-Dist: numba>=0.55
34
35
  Requires-Dist: pyyaml>=6.0
35
36
  Requires-Dist: pyarrow>=14.0.0
36
37
  Requires-Dist: typer>=0.9.0
@@ -143,7 +144,7 @@ pip install -e .[dev]
143
144
  - **Spectral Alignment**: Shift, linear, piecewise, DTW, quadratic, cubic, and LOWESS warping for both binned and raw full-resolution spectra. Spectra are first shifted and then aligned to the reference of the shifted spectra, a non-rigid warp is kept only when the matched peaks support it, and resampling preserves the total intensity; spectra of several species are best aligned one species at a time
144
145
  - **Peak Detection**: Local maxima and persistent homology methods
145
146
  - **Peak Sets**: `PeakSet` / `PeakList` represent each spectrum as a variable-length `(m/z, intensity)` set for peak-set models, via `MaldiPeakDetector.transform_peaklist()` on binned spectra or `create_peakset_input()` at full m/z resolution; extraction is stateless per spectrum, so results are cacheable without leakage
146
- - **Fit-Free Peak Alignment**: `align_peaks()` warps a peak set onto caller-supplied reference peaks, so the reference stays fitted on the training split
147
+ - **Fit-Free Peak Alignment**: `align_peaks()` warps a peak set onto caller-supplied reference peaks, which the caller estimates on the training split (for example `RawWarping(...).fit(X_train).ref_peaks_mz_`; a reference built with test spectra leaks)
147
148
 
148
149
  ### Extensibility
149
150
  - **Component Registries**: Plug your own components into the name-dispatched machinery - `register_spectral_metric()` for distance metrics (usable in `spectral_distance`, `pairwise_distances`, and `DriftMonitor`), `register_transformer()` to make `PreprocessingPipeline` configs holding custom transformers round-trip through JSON/YAML, and `register_binning_method()` for custom bin edges
@@ -162,7 +163,7 @@ pip install -e .[dev]
162
163
 
163
164
  ### Differential Analysis
164
165
  - **`DifferentialAnalysis`**: Per-bin statistical testing (Mann-Whitney U, Welch's t-test) between resistant and susceptible groups, with multiple-testing correction, log2 fold change, and Cohen's d effect size
165
- - **Peak Selection**: `top_peaks()` by adjusted p-value, `significant_peaks()` with fold-change and p-value thresholds, `compare_drugs()` for multi-drug boolean significance matrices
166
+ - **Peak Ranking**: `top_peaks()` by adjusted p-value, `significant_peaks()` with fold-change and p-value thresholds, `compare_drugs()` for multi-drug boolean significance matrices (to select features for a classifier, rank peaks inside each training fold)
166
167
  - **AMR-Aware Plots**: `plot_volcano()`, `plot_manhattan()` along the m/z axis, and `plot_drug_comparison()` with binary heatmap or UpSet-style intersection view
167
168
 
168
169
  ### Drift Monitoring
@@ -246,7 +247,7 @@ pipe = Pipeline([
246
247
  ("clf", RandomForestClassifier(n_estimators=100, random_state=42))
247
248
  ])
248
249
 
249
- # Cross-validation
250
+ # Cross-validation (with replicates, use StratifiedGroupKFold and groups=data.groups)
250
251
  scores = cross_val_score(pipe, X, y, cv=5, scoring="accuracy")
251
252
  print(f"CV Accuracy: {scores.mean():.3f} +/- {scores.std():.3f}")
252
253
  ```
@@ -82,7 +82,7 @@ pip install -e .[dev]
82
82
  - **Spectral Alignment**: Shift, linear, piecewise, DTW, quadratic, cubic, and LOWESS warping for both binned and raw full-resolution spectra. Spectra are first shifted and then aligned to the reference of the shifted spectra, a non-rigid warp is kept only when the matched peaks support it, and resampling preserves the total intensity; spectra of several species are best aligned one species at a time
83
83
  - **Peak Detection**: Local maxima and persistent homology methods
84
84
  - **Peak Sets**: `PeakSet` / `PeakList` represent each spectrum as a variable-length `(m/z, intensity)` set for peak-set models, via `MaldiPeakDetector.transform_peaklist()` on binned spectra or `create_peakset_input()` at full m/z resolution; extraction is stateless per spectrum, so results are cacheable without leakage
85
- - **Fit-Free Peak Alignment**: `align_peaks()` warps a peak set onto caller-supplied reference peaks, so the reference stays fitted on the training split
85
+ - **Fit-Free Peak Alignment**: `align_peaks()` warps a peak set onto caller-supplied reference peaks, which the caller estimates on the training split (for example `RawWarping(...).fit(X_train).ref_peaks_mz_`; a reference built with test spectra leaks)
86
86
 
87
87
  ### Extensibility
88
88
  - **Component Registries**: Plug your own components into the name-dispatched machinery - `register_spectral_metric()` for distance metrics (usable in `spectral_distance`, `pairwise_distances`, and `DriftMonitor`), `register_transformer()` to make `PreprocessingPipeline` configs holding custom transformers round-trip through JSON/YAML, and `register_binning_method()` for custom bin edges
@@ -101,7 +101,7 @@ pip install -e .[dev]
101
101
 
102
102
  ### Differential Analysis
103
103
  - **`DifferentialAnalysis`**: Per-bin statistical testing (Mann-Whitney U, Welch's t-test) between resistant and susceptible groups, with multiple-testing correction, log2 fold change, and Cohen's d effect size
104
- - **Peak Selection**: `top_peaks()` by adjusted p-value, `significant_peaks()` with fold-change and p-value thresholds, `compare_drugs()` for multi-drug boolean significance matrices
104
+ - **Peak Ranking**: `top_peaks()` by adjusted p-value, `significant_peaks()` with fold-change and p-value thresholds, `compare_drugs()` for multi-drug boolean significance matrices (to select features for a classifier, rank peaks inside each training fold)
105
105
  - **AMR-Aware Plots**: `plot_volcano()`, `plot_manhattan()` along the m/z axis, and `plot_drug_comparison()` with binary heatmap or UpSet-style intersection view
106
106
 
107
107
  ### Drift Monitoring
@@ -185,7 +185,7 @@ pipe = Pipeline([
185
185
  ("clf", RandomForestClassifier(n_estimators=100, random_state=42))
186
186
  ])
187
187
 
188
- # Cross-validation
188
+ # Cross-validation (with replicates, use StratifiedGroupKFold and groups=data.groups)
189
189
  scores = cross_val_score(pipe, X, y, cv=5, scoring="accuracy")
190
190
  print(f"CV Accuracy: {scores.mean():.3f} +/- {scores.std():.3f}")
191
191
  ```
@@ -27,7 +27,7 @@ Examples
27
27
  from .dataset import MaldiSet
28
28
  from .spectrum import MaldiSpectrum
29
29
 
30
- __version__ = "0.22.0"
30
+ __version__ = "0.22.1"
31
31
  __author__ = "Ettore Rocchi"
32
32
 
33
33
  __all__ = [
@@ -0,0 +1,82 @@
1
+ """Banded Sakoe-Chiba DTW path, computed on the band only.
2
+
3
+ The path is the one :func:`tslearn.metrics.dtw_path` returns for two series
4
+ of equal length with ``global_constraint="sakoe_chiba"``: same float64 cost,
5
+ same tie order when walking back. Memory is ``O(n * (2 * radius + 1))``
6
+ instead of tslearn's ``O(n ** 2)``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import numpy as np
12
+ from numba import njit
13
+
14
+
15
+ @njit(cache=True, nogil=True)
16
+ def _banded_path(s1, s2, radius):
17
+ """Forward DTW path ``(path_i, path_j)`` of two float64 series of length n."""
18
+ n = s1.shape[0]
19
+ r = min(radius, n - 1)
20
+ w = 2 * r + 1
21
+ # cost[i, k] is the accumulated cost of cell (i, j) with j = i - r + k.
22
+ cost = np.full((n, w), np.inf)
23
+ for i in range(n):
24
+ lo = max(0, i - r)
25
+ hi = min(n - 1, i + r)
26
+ for j in range(lo, hi + 1):
27
+ k = j - i + r
28
+ if i == 0 and j == 0:
29
+ best = 0.0
30
+ else:
31
+ up = cost[i - 1, k + 1] if i > 0 and k + 1 < w else np.inf
32
+ left = cost[i, k - 1] if j > 0 and k > 0 else np.inf
33
+ diag = cost[i - 1, k] if i > 0 and j > 0 else np.inf
34
+ best = min(up, left, diag)
35
+ diff = s1[i] - s2[j]
36
+ dist = diff * diff
37
+ cost[i, k] = dist + best
38
+
39
+ path_i = np.empty(2 * n, dtype=np.int64)
40
+ path_j = np.empty(2 * n, dtype=np.int64)
41
+ i = n - 1
42
+ j = n - 1
43
+ m = 0
44
+ path_i[0] = i
45
+ path_j[0] = j
46
+ while i != 0 or j != 0:
47
+ if i == 0:
48
+ j -= 1
49
+ elif j == 0:
50
+ i -= 1
51
+ else:
52
+ # Same order as tslearn: diagonal, then up, then left.
53
+ k = j - i + r
54
+ diag = cost[i - 1, k] if 0 <= k < w else np.inf
55
+ up = cost[i - 1, k + 1] if 0 <= k + 1 < w else np.inf
56
+ left = cost[i, k - 1] if 0 <= k - 1 < w else np.inf
57
+ if diag <= up and diag <= left:
58
+ i -= 1
59
+ j -= 1
60
+ elif up <= left:
61
+ i -= 1
62
+ else:
63
+ j -= 1
64
+ m += 1
65
+ path_i[m] = i
66
+ path_j[m] = j
67
+ return path_i[: m + 1][::-1].copy(), path_j[: m + 1][::-1].copy()
68
+
69
+
70
+ def _dtw_path(
71
+ query: np.ndarray, reference: np.ndarray, radius: int
72
+ ) -> tuple[np.ndarray, np.ndarray]:
73
+ """Banded DTW path of two series of equal length.
74
+
75
+ Both series are cast to contiguous float64 (as tslearn does), so the
76
+ kernel is compiled once whatever the input dtype.
77
+ """
78
+ return _banded_path(
79
+ np.ascontiguousarray(query, dtype=np.float64),
80
+ np.ascontiguousarray(reference, dtype=np.float64),
81
+ int(radius),
82
+ )
@@ -0,0 +1,87 @@
1
+ """Version stamp for pickled aligners.
2
+
3
+ A pickled :class:`~maldiamrkit.alignment.Warping` or
4
+ :class:`~maldiamrkit.alignment.RawWarping` records the maldiamrkit version it
5
+ was saved with. An aligner fitted with maldiamrkit 0.21 or earlier lacks the
6
+ parameters added in 0.22 and aligns differently, so it is refused instead of
7
+ being completed with defaults (which would align new spectra differently
8
+ from the training data).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ import warnings
15
+
16
+ _PARAMS_SINCE_0_22 = (
17
+ "shift_estimator",
18
+ "min_gain",
19
+ "min_support",
20
+ "n_peaks",
21
+ "prealign",
22
+ "renormalize",
23
+ )
24
+
25
+
26
+ def _current_version() -> str:
27
+ import maldiamrkit
28
+
29
+ return maldiamrkit.__version__
30
+
31
+
32
+ def _version_tuple(text: object) -> tuple[int, int, int] | None:
33
+ """Integers of the first three dotted fields of a version, or None."""
34
+ if not isinstance(text, str):
35
+ return None
36
+ match = re.match(r"\s*(\d+)\.(\d+)(?:\.(\d+))?", text)
37
+ if match is None:
38
+ return None
39
+ return tuple(int(part or 0) for part in match.groups())
40
+
41
+
42
+ def _versioned_state(state: dict) -> dict:
43
+ """Stamp the pickled state with the running maldiamrkit version."""
44
+ return dict(state, _maldiamrkit_version=_current_version())
45
+
46
+
47
+ def _checked_state(name: str, state: dict) -> dict:
48
+ """Return the state to restore, warning when it comes from an older release."""
49
+ state = dict(state)
50
+ version = state.pop("_maldiamrkit_version", None)
51
+ current = _current_version()
52
+ if any(param not in state for param in _PARAMS_SINCE_0_22):
53
+ message = (
54
+ f"This {name} was fitted with maldiamrkit 0.21 or earlier, whose "
55
+ f"alignment differs from maldiamrkit {current}: it must be refitted "
56
+ "(fit, transform and get_alignment_quality raise), or used with "
57
+ "maldiamrkit < 0.22."
58
+ )
59
+ elif version is None:
60
+ message = (
61
+ f"This {name} was pickled with maldiamrkit 0.22.0 or earlier, which "
62
+ f"did not record its version; it is loaded with maldiamrkit "
63
+ f"{current} and can still be used."
64
+ )
65
+ else:
66
+ saved, running = _version_tuple(version), _version_tuple(current)
67
+ if saved is not None and (running is None or saved >= running):
68
+ return state
69
+ message = (
70
+ f"This {name} was pickled with maldiamrkit {version} and is loaded "
71
+ f"with maldiamrkit {current}."
72
+ )
73
+ warnings.warn(message, UserWarning, stacklevel=3)
74
+ return state
75
+
76
+
77
+ def _check_supported(estimator: object) -> None:
78
+ """Raise when ``estimator`` holds the state of maldiamrkit 0.21 or earlier."""
79
+ if all(hasattr(estimator, param) for param in _PARAMS_SINCE_0_22):
80
+ return
81
+ name = type(estimator).__name__
82
+ raise RuntimeError(
83
+ f"This {name} was fitted with maldiamrkit 0.21 or earlier, whose "
84
+ "alignment differs from maldiamrkit 0.22: create and fit a new "
85
+ f"{name} with the current version (or keep maldiamrkit < 0.22 to use "
86
+ "this one)."
87
+ )
@@ -62,7 +62,10 @@ def align_peaks(
62
62
  peaks : PeakSet
63
63
  Peak set to align.
64
64
  ref_peaks_mz : array-like
65
- Reference peak m/z positions to align to.
65
+ Reference peak m/z positions to align to. Estimate them on the
66
+ training spectra only (for example ``RawWarping(...).fit(X_train)``
67
+ and its ``ref_peaks_mz_``), so that test peak sets do not shape
68
+ their own reference.
66
69
  method : {"shift", "linear", "piecewise", "quadratic", "cubic", "lowess"}, default="shift"
67
70
  Warping strategy. ``"dtw"`` is unsupported for peak sets because it
68
71
  resamples onto a dense grid and would destroy the set structure.
@@ -82,7 +85,8 @@ def align_peaks(
82
85
  Returns
83
86
  -------
84
87
  PeakSet
85
- A new peak set with warped m/z and unchanged intensities.
88
+ A new peak set with warped m/z and unchanged intensities and
89
+ scores.
86
90
 
87
91
  Raises
88
92
  ------
@@ -110,9 +114,10 @@ def align_peaks(
110
114
  "'cubic', or 'lowess'."
111
115
  )
112
116
 
117
+ score = None if peaks.score is None else peaks.score.copy()
113
118
  ref_peaks_mz = np.asarray(ref_peaks_mz, dtype=float).ravel()
114
119
  if peaks.n_peaks == 0 or ref_peaks_mz.size == 0:
115
- return PeakSet(peaks.mz.copy(), peaks.intensity.copy())
120
+ return PeakSet(peaks.mz.copy(), peaks.intensity.copy(), score=score)
116
121
 
117
122
  strategy = _build_strategy(
118
123
  method, max_shift_da, n_segments, smooth_sigma, lowess_frac, lowess_it
@@ -125,4 +130,4 @@ def align_peaks(
125
130
  ref_peaks_mz,
126
131
  np.ones_like(ref_peaks_mz),
127
132
  )
128
- return PeakSet(np.asarray(new_mz, dtype=float), peaks.intensity.copy())
133
+ return PeakSet(np.asarray(new_mz, dtype=float), peaks.intensity.copy(), score=score)
@@ -13,9 +13,20 @@ from sklearn.base import BaseEstimator, TransformerMixin
13
13
  from ..data.duplicates import DuplicateStrategy, apply_index_strategy
14
14
  from ..detection.peak_detector import MaldiPeakDetector
15
15
  from ..io.readers import read_spectrum
16
- from ..preprocessing.binning import BinningMethod, bin_spectrum
16
+ from ..preprocessing.binning import (
17
+ _BINNING_REGISTRY,
18
+ BinningMethod,
19
+ _bin_on_edges,
20
+ bin_spectrum,
21
+ )
17
22
  from ..preprocessing.pipeline import preprocess
18
23
  from ..preprocessing.preprocessing_pipeline import PreprocessingPipeline
24
+ from ._versioning import (
25
+ _PARAMS_SINCE_0_22,
26
+ _check_supported,
27
+ _checked_state,
28
+ _versioned_state,
29
+ )
19
30
  from .strategies import (
20
31
  _ALIGNMENT_REGISTRY,
21
32
  _SHIFT_ESTIMATORS,
@@ -56,10 +67,15 @@ def create_raw_input(
56
67
  * ``"first"``: keep the first occurrence (default).
57
68
  * ``"last"``: keep the last occurrence.
58
69
  * ``"drop"``: remove all duplicates.
59
- * ``"keep_all"``: keep every replicate with ``_repN`` suffixes.
70
+ * ``"keep_all"``: keep every replicate, adding ``_repN`` to the
71
+ duplicated IDs (unique IDs stay unchanged).
60
72
  * ``"average"``: keep all replicates and add an
61
73
  ``_original_id`` column so that
62
- :meth:`RawWarping.transform` can group and average them.
74
+ :meth:`RawWarping.transform` can group and average them (the
75
+ output then has one row per sample, so it cannot be used inside
76
+ ``Pipeline.fit``, which needs one row per input row; when
77
+ splitting rows, keep the replicates of a sample together with
78
+ ``groups=``).
63
79
 
64
80
  Returns
65
81
  -------
@@ -136,6 +152,8 @@ class RawWarping(BaseEstimator, TransformerMixin):
136
152
  Width of output bins in Daltons.
137
153
  bin_method : str, default="uniform"
138
154
  Binning method. One of 'uniform', 'proportional', 'adaptive', 'custom'.
155
+ With 'adaptive', the edges are learned from the reference spectrum in
156
+ :meth:`fit` and used for every transformed spectrum.
139
157
  bin_kwargs : dict, optional
140
158
  Additional keyword arguments for binning.
141
159
  max_shift_da : float, default=10.0
@@ -145,10 +163,8 @@ class RawWarping(BaseEstimator, TransformerMixin):
145
163
  n_segments : int, default=5
146
164
  Number of segments for piecewise warping.
147
165
  dtw_radius : int, default=10
148
- Radius constraint for DTW. DTW memory grows with the square of the
149
- number of reference points (about 10 GB per spectrum for a 0.5 Da
150
- grid over 2-20 kDa), so a coarser ``interp_step`` is needed to run
151
- it on full-resolution spectra.
166
+ Radius constraint for DTW (Sakoe-Chiba band, in points of the
167
+ reference grid).
152
168
  smooth_sigma : float, default=2.0
153
169
  Gaussian smoothing for piecewise transitions.
154
170
  lowess_frac : float, default=0.3
@@ -263,6 +279,10 @@ class RawWarping(BaseEstimator, TransformerMixin):
263
279
  aligns each spectrum on its own (replicates of one sample are averaged
264
280
  after alignment), so its result does not depend on the other spectra
265
281
  transformed with it.
282
+
283
+ A pickled aligner records the maldiamrkit version it was saved with; one
284
+ fitted with maldiamrkit 0.21 or earlier cannot be used and must be
285
+ refitted.
266
286
  """
267
287
 
268
288
  def __init__(
@@ -407,6 +427,32 @@ class RawWarping(BaseEstimator, TransformerMixin):
407
427
 
408
428
  return ref_mz, ref_intensity, ref_peaks_mz
409
429
 
430
+ def __getstate__(self):
431
+ return _versioned_state(super().__getstate__())
432
+
433
+ def __setstate__(self, state):
434
+ super().__setstate__(_checked_state(type(self).__name__, state))
435
+ # Aligners pickled with 0.22.0 binned adaptively spectrum by spectrum:
436
+ # learn the edges from their reference, as fit does now.
437
+ if (
438
+ "_adaptive_edges_" not in self.__dict__
439
+ and hasattr(self, "ref_mz_")
440
+ and all(param in self.__dict__ for param in _PARAMS_SINCE_0_22)
441
+ ):
442
+ self._adaptive_edges_ = self._learn_adaptive_edges()
443
+ if self._adaptive_edges_ is not None and not self._output_columns().equals(
444
+ self.output_columns_
445
+ ):
446
+ self._adaptive_edges_ = None
447
+ self._stale_bins_ = True
448
+ warnings.warn(
449
+ "This RawWarping was pickled with maldiamrkit 0.22.0 and its "
450
+ "adaptive bins cannot be rebuilt exactly from its reference: "
451
+ "refit it before transforming.",
452
+ UserWarning,
453
+ stacklevel=2,
454
+ )
455
+
410
456
  def fit(self, X: pd.DataFrame, y=None):
411
457
  """
412
458
  Fit by computing the reference spectrum from raw data.
@@ -427,10 +473,14 @@ class RawWarping(BaseEstimator, TransformerMixin):
427
473
 
428
474
  Raises
429
475
  ------
476
+ RuntimeError
477
+ If the transformer was fitted with maldiamrkit 0.21 or earlier
478
+ (loaded from an old pickle).
430
479
  ValueError
431
480
  If the input DataFrame is empty, lacks a 'path' column,
432
481
  or uses an unknown warping method.
433
482
  """
483
+ _check_supported(self)
434
484
  if X.empty:
435
485
  raise ValueError("Input DataFrame X is empty")
436
486
 
@@ -454,6 +504,8 @@ class RawWarping(BaseEstimator, TransformerMixin):
454
504
 
455
505
  # Store preprocessing config
456
506
  self.pipeline_ = self.pipeline or PreprocessingPipeline.default()
507
+ self._adaptive_edges_ = None
508
+ self.__dict__.pop("_stale_bins_", None)
457
509
 
458
510
  # Get paths from DataFrame
459
511
  paths = X["path"].tolist()
@@ -484,12 +536,50 @@ class RawWarping(BaseEstimator, TransformerMixin):
484
536
  stacklevel=2,
485
537
  )
486
538
 
539
+ # Adaptive bins are learned once, from the reference, so that every
540
+ # transformed spectrum gets the same columns.
541
+ self._adaptive_edges_ = self._learn_adaptive_edges()
542
+
487
543
  # Determine output columns by binning a sample spectrum
488
- sample_binned = self._bin_warped(self.ref_mz_, self.ref_intensity_)
489
- self.output_columns_ = sample_binned.set_index("mass").index.astype(str)
544
+ self.output_columns_ = self._output_columns()
490
545
 
491
546
  return self
492
547
 
548
+ def _learn_adaptive_edges(self) -> np.ndarray | None:
549
+ """Adaptive bin edges of the reference, or None for other methods.
550
+
551
+ The reference is binned with :func:`bin_spectrum`, which also checks
552
+ ``bin_kwargs``.
553
+ """
554
+ if _BINNING_REGISTRY.resolve_key(self.bin_method, BinningMethod) != "adaptive":
555
+ return None
556
+ mz_min, mz_max = self.pipeline_.mz_range
557
+ _, metadata = bin_spectrum(
558
+ pd.DataFrame({"mass": self.ref_mz_, "intensity": self.ref_intensity_}),
559
+ mz_min=mz_min,
560
+ mz_max=mz_max,
561
+ bin_width=self.bin_width,
562
+ method=self.bin_method,
563
+ **(self.bin_kwargs or {}),
564
+ )
565
+ return np.append(
566
+ metadata["bin_start"].to_numpy(dtype=float),
567
+ float(metadata["bin_end"].iloc[-1]),
568
+ )
569
+
570
+ def _output_columns(self) -> pd.Index:
571
+ """Column labels of the binned output, from the reference."""
572
+ binned = self._bin_warped(self.ref_mz_, self.ref_intensity_)
573
+ return binned.set_index("mass").index.astype(str)
574
+
575
+ def _check_bins(self) -> None:
576
+ """Raise when the adaptive bins of a 0.22.0 pickle could not be rebuilt."""
577
+ if getattr(self, "_stale_bins_", False):
578
+ raise RuntimeError(
579
+ "This RawWarping was pickled with maldiamrkit 0.22.0 and its "
580
+ "adaptive bins cannot be rebuilt exactly: refit it."
581
+ )
582
+
493
583
  def _get_strategy(self):
494
584
  """Build strategy instance from current parameters."""
495
585
  cls = _ALIGNMENT_REGISTRY[self.method]
@@ -554,6 +644,10 @@ class RawWarping(BaseEstimator, TransformerMixin):
554
644
  def _bin_warped(self, mz: np.ndarray, intensity: np.ndarray) -> pd.DataFrame:
555
645
  """Bin warped raw spectrum to output grid."""
556
646
  warped_df = pd.DataFrame({"mass": mz, "intensity": intensity})
647
+ adaptive_edges = getattr(self, "_adaptive_edges_", None)
648
+ if adaptive_edges is not None:
649
+ binned, _ = _bin_on_edges(warped_df, adaptive_edges)
650
+ return binned
557
651
  bin_kwargs = self.bin_kwargs or {}
558
652
  mz_min, mz_max = self.pipeline_.mz_range
559
653
  binned, _ = bin_spectrum(
@@ -612,15 +706,21 @@ class RawWarping(BaseEstimator, TransformerMixin):
612
706
  -------
613
707
  X_aligned : pd.DataFrame
614
708
  Aligned and binned spectra with sample IDs as index and
615
- m/z bin centers as columns.
709
+ m/z bin centers as columns. With an ``_original_id`` column
710
+ (see :func:`create_raw_input`), replicates are averaged into
711
+ one row per sample, in order of first appearance.
616
712
 
617
713
  Raises
618
714
  ------
619
715
  RuntimeError
620
- If the transformer has not been fitted.
716
+ If the transformer has not been fitted, was fitted with
717
+ maldiamrkit 0.21 or earlier, or was pickled with 0.22.0 with
718
+ adaptive bins that cannot be rebuilt.
621
719
  ValueError
622
720
  If the input DataFrame lacks a 'path' column.
623
721
  """
722
+ _check_supported(self)
723
+ self._check_bins()
624
724
  if not hasattr(self, "ref_mz_"):
625
725
  raise RuntimeError("RawWarping must be fitted before transform")
626
726
 
@@ -648,7 +748,7 @@ class RawWarping(BaseEstimator, TransformerMixin):
648
748
  if "_original_id" in X.columns:
649
749
  original_ids = X["_original_id"]
650
750
  result["_original_id"] = original_ids.values
651
- averaged = result.groupby("_original_id").mean()
751
+ averaged = result.groupby("_original_id", sort=False).mean()
652
752
  averaged.index.name = None
653
753
  result = averaged
654
754
 
@@ -678,8 +778,12 @@ class RawWarping(BaseEstimator, TransformerMixin):
678
778
  Raises
679
779
  ------
680
780
  RuntimeError
681
- If the transformer has not been fitted.
781
+ If the transformer has not been fitted, was fitted with
782
+ maldiamrkit 0.21 or earlier, or was pickled with 0.22.0 with
783
+ adaptive bins that cannot be rebuilt.
682
784
  """
785
+ _check_supported(self)
786
+ self._check_bins()
683
787
  if not hasattr(self, "ref_mz_"):
684
788
  raise RuntimeError("RawWarping must be fitted first")
685
789