combatlearn 1.4.0__tar.gz → 2.0.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-1.4.0 → combatlearn-2.0.0}/PKG-INFO +1 -1
- {combatlearn-1.4.0 → combatlearn-2.0.0}/combatlearn/__init__.py +1 -1
- combatlearn-2.0.0/combatlearn/_utils.py +21 -0
- combatlearn-2.0.0/combatlearn/inspection.py +194 -0
- {combatlearn-1.4.0 → combatlearn-2.0.0}/combatlearn/metrics.py +127 -214
- {combatlearn-1.4.0 → combatlearn-2.0.0}/combatlearn/sklearn_api.py +8 -126
- combatlearn-2.0.0/combatlearn/visualization.py +636 -0
- {combatlearn-1.4.0 → combatlearn-2.0.0}/combatlearn.egg-info/PKG-INFO +1 -1
- {combatlearn-1.4.0 → combatlearn-2.0.0}/combatlearn.egg-info/SOURCES.txt +6 -1
- combatlearn-2.0.0/tests/test_combat.py +477 -0
- combatlearn-2.0.0/tests/test_inspection.py +235 -0
- combatlearn-2.0.0/tests/test_metrics.py +107 -0
- combatlearn-2.0.0/tests/test_visualization.py +180 -0
- combatlearn-1.4.0/combatlearn/visualization.py +0 -609
- combatlearn-1.4.0/tests/test_combat.py +0 -1008
- {combatlearn-1.4.0 → combatlearn-2.0.0}/LICENSE +0 -0
- {combatlearn-1.4.0 → combatlearn-2.0.0}/README.md +0 -0
- {combatlearn-1.4.0 → combatlearn-2.0.0}/combatlearn/core.py +0 -0
- {combatlearn-1.4.0 → combatlearn-2.0.0}/combatlearn/py.typed +0 -0
- {combatlearn-1.4.0 → combatlearn-2.0.0}/combatlearn.egg-info/dependency_links.txt +0 -0
- {combatlearn-1.4.0 → combatlearn-2.0.0}/combatlearn.egg-info/requires.txt +0 -0
- {combatlearn-1.4.0 → combatlearn-2.0.0}/combatlearn.egg-info/top_level.txt +0 -0
- {combatlearn-1.4.0 → combatlearn-2.0.0}/pyproject.toml +0 -0
- {combatlearn-1.4.0 → combatlearn-2.0.0}/requirements-dev.txt +0 -0
- {combatlearn-1.4.0 → combatlearn-2.0.0}/requirements-docs.txt +0 -0
- {combatlearn-1.4.0 → combatlearn-2.0.0}/requirements.txt +0 -0
- {combatlearn-1.4.0 → combatlearn-2.0.0}/setup.cfg +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Internal utilities shared across combatlearn modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from .core import ArrayLike
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _subset(obj: ArrayLike | None, idx: pd.Index) -> pd.DataFrame | pd.Series | None:
|
|
12
|
+
"""Subset array-like object by index."""
|
|
13
|
+
if obj is None:
|
|
14
|
+
return None
|
|
15
|
+
if isinstance(obj, pd.Series | pd.DataFrame):
|
|
16
|
+
return obj.loc[idx]
|
|
17
|
+
else:
|
|
18
|
+
if isinstance(obj, np.ndarray) and obj.ndim == 1:
|
|
19
|
+
return pd.Series(obj, index=idx)
|
|
20
|
+
else:
|
|
21
|
+
return pd.DataFrame(obj, index=idx)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Standalone inspection functions for fitted ComBat models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Literal
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .sklearn_api import ComBat
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def feature_batch_diagnostics(
|
|
15
|
+
combat: ComBat,
|
|
16
|
+
mode: Literal["magnitude", "distribution"] = "magnitude",
|
|
17
|
+
weighted: bool = True,
|
|
18
|
+
) -> pd.DataFrame:
|
|
19
|
+
"""Compute per-feature batch effect magnitude.
|
|
20
|
+
|
|
21
|
+
Returns a DataFrame with columns ``location``, ``scale``, and
|
|
22
|
+
``combined``. Location is the (weighted) RMS of gamma across batches
|
|
23
|
+
(standardized mean shifts). Scale is the (weighted) RMS of log-delta
|
|
24
|
+
across batches (log-fold variance change). Combined is the Euclidean
|
|
25
|
+
norm sqrt(location**2 + scale**2). Using RMS provides L2-consistent
|
|
26
|
+
aggregation; using log(delta) ensures symmetry.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
combat : ComBat
|
|
31
|
+
A fitted ``ComBat`` instance.
|
|
32
|
+
mode : {'magnitude', 'distribution'}, default='magnitude'
|
|
33
|
+
- 'magnitude': Returns L2-consistent absolute batch effect magnitudes.
|
|
34
|
+
Suitable for ranking, thresholding, and cross-dataset comparison.
|
|
35
|
+
- 'distribution': Returns column-wise normalized proportions (each column
|
|
36
|
+
sums to 1, values in range [0, 1]), representing the relative contribution
|
|
37
|
+
of each feature to the total location, scale, or combined batch effect.
|
|
38
|
+
Note: normalization is applied independently to each column, so the
|
|
39
|
+
Euclidean relationship (combined**2 = location**2 + scale**2) no longer holds.
|
|
40
|
+
weighted : bool, default=True
|
|
41
|
+
If True, compute a weighted RMS where each batch is weighted by its
|
|
42
|
+
sample size. This gives more influence to larger batches, producing
|
|
43
|
+
a more statistically representative summary. If False, all batches
|
|
44
|
+
contribute equally regardless of size.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
pd.DataFrame
|
|
49
|
+
DataFrame with index=feature names, columns=['location', 'scale', 'combined'],
|
|
50
|
+
sorted by 'combined' descending.
|
|
51
|
+
|
|
52
|
+
Raises
|
|
53
|
+
------
|
|
54
|
+
ValueError
|
|
55
|
+
If the model is not fitted or if mode is invalid.
|
|
56
|
+
"""
|
|
57
|
+
if not hasattr(combat, "_model") or not hasattr(combat._model, "_gamma_star"):
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"This ComBat instance is not fitted yet. Call 'fit' before 'feature_batch_diagnostics'."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if mode not in ["magnitude", "distribution"]:
|
|
63
|
+
raise ValueError(f"mode must be 'magnitude' or 'distribution', got '{mode}'")
|
|
64
|
+
|
|
65
|
+
feature_names = combat._model._grand_mean.index
|
|
66
|
+
gamma_star = combat._model._gamma_star
|
|
67
|
+
delta_star = combat._model._delta_star
|
|
68
|
+
|
|
69
|
+
if weighted:
|
|
70
|
+
# Batch sample sizes as weights, normalized to sum to 1
|
|
71
|
+
n_per_batch = combat._model._n_per_batch
|
|
72
|
+
weights = np.array(
|
|
73
|
+
[n_per_batch[str(lvl)] for lvl in combat._model._batch_levels], dtype=np.float64
|
|
74
|
+
)
|
|
75
|
+
weights = weights / weights.sum()
|
|
76
|
+
|
|
77
|
+
# Location effect: weighted RMS of gamma across batches
|
|
78
|
+
location = np.sqrt((weights[:, np.newaxis] * gamma_star**2).sum(axis=0))
|
|
79
|
+
|
|
80
|
+
# Scale effect: weighted RMS of log(delta) across batches
|
|
81
|
+
if not combat.mean_only:
|
|
82
|
+
scale = np.sqrt((weights[:, np.newaxis] * np.log(delta_star) ** 2).sum(axis=0))
|
|
83
|
+
else:
|
|
84
|
+
scale = np.zeros_like(location)
|
|
85
|
+
else:
|
|
86
|
+
# Location effect: unweighted RMS of gamma across batches
|
|
87
|
+
location = np.sqrt((gamma_star**2).mean(axis=0))
|
|
88
|
+
|
|
89
|
+
# Scale effect: unweighted RMS of log(delta) across batches
|
|
90
|
+
if not combat.mean_only:
|
|
91
|
+
scale = np.sqrt((np.log(delta_star) ** 2).mean(axis=0))
|
|
92
|
+
else:
|
|
93
|
+
scale = np.zeros_like(location)
|
|
94
|
+
|
|
95
|
+
# Euclidean to treat location and scale as orthogonal dimensions
|
|
96
|
+
combined = np.sqrt(location**2 + scale**2)
|
|
97
|
+
|
|
98
|
+
if mode == "distribution":
|
|
99
|
+
# Normalize each column independently to sum to 1
|
|
100
|
+
location_sum = location.sum()
|
|
101
|
+
scale_sum = scale.sum()
|
|
102
|
+
combined_sum = combined.sum()
|
|
103
|
+
|
|
104
|
+
location = location / location_sum if location_sum > 0 else location
|
|
105
|
+
scale = scale / scale_sum if scale_sum > 0 else scale
|
|
106
|
+
combined = combined / combined_sum if combined_sum > 0 else combined
|
|
107
|
+
|
|
108
|
+
return pd.DataFrame(
|
|
109
|
+
{
|
|
110
|
+
"location": location,
|
|
111
|
+
"scale": scale,
|
|
112
|
+
"combined": combined,
|
|
113
|
+
},
|
|
114
|
+
index=feature_names,
|
|
115
|
+
).sort_values("combined", ascending=False)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def summary(combat: ComBat) -> str:
|
|
119
|
+
"""Return a human-readable diagnostic report after fitting.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
combat : ComBat
|
|
124
|
+
A fitted ``ComBat`` instance.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
str
|
|
129
|
+
Multi-line summary string.
|
|
130
|
+
|
|
131
|
+
Raises
|
|
132
|
+
------
|
|
133
|
+
ValueError
|
|
134
|
+
If the model is not fitted.
|
|
135
|
+
"""
|
|
136
|
+
if not hasattr(combat, "_model") or not hasattr(combat._model, "_gamma_star"):
|
|
137
|
+
raise ValueError("This ComBat instance is not fitted yet. Call 'fit' before 'summary'.")
|
|
138
|
+
|
|
139
|
+
lines: list[str] = []
|
|
140
|
+
lines.append("ComBat Summary")
|
|
141
|
+
lines.append("=" * 40)
|
|
142
|
+
lines.append(f"Method: {combat.method}")
|
|
143
|
+
lines.append(f"Parametric: {combat.parametric}")
|
|
144
|
+
lines.append(f"Mean only: {combat.mean_only}")
|
|
145
|
+
lines.append(f"Reference batch: {combat.reference_batch or 'None'}")
|
|
146
|
+
|
|
147
|
+
batch_levels = combat._model._batch_levels
|
|
148
|
+
n_per_batch = combat._model._n_per_batch
|
|
149
|
+
lines.append(f"Number of batches: {len(batch_levels)}")
|
|
150
|
+
lines.append("Samples per batch:")
|
|
151
|
+
for lvl in batch_levels:
|
|
152
|
+
lines.append(f" {lvl}: {n_per_batch[str(lvl)]}")
|
|
153
|
+
|
|
154
|
+
n_features = len(combat._model._grand_mean)
|
|
155
|
+
lines.append(f"Number of features: {n_features}")
|
|
156
|
+
|
|
157
|
+
lines.append("")
|
|
158
|
+
lines.append("Top 5 features by batch effect (combined):")
|
|
159
|
+
importance = feature_batch_diagnostics(combat)
|
|
160
|
+
top5 = importance.head(5)
|
|
161
|
+
for feat, row in top5.iterrows():
|
|
162
|
+
lines.append(f" {feat}: {row['combined']:.4f}")
|
|
163
|
+
|
|
164
|
+
# Diagnostics table
|
|
165
|
+
lines.append("")
|
|
166
|
+
lines.append("Diagnostics")
|
|
167
|
+
lines.append("=" * 40)
|
|
168
|
+
col_w = 34
|
|
169
|
+
lines.append(f"{'Metric':<{col_w}}Value")
|
|
170
|
+
lines.append(f"{'------':<{col_w}}-----")
|
|
171
|
+
|
|
172
|
+
if hasattr(combat, "_batch_var_before_"):
|
|
173
|
+
lines.append(f"{'Batch var. explained (before)':<{col_w}}{combat._batch_var_before_:.1%}")
|
|
174
|
+
if hasattr(combat, "_batch_var_after_"):
|
|
175
|
+
lines.append(f"{'Batch var. explained (after)':<{col_w}}{combat._batch_var_after_:.1%}")
|
|
176
|
+
|
|
177
|
+
design_cond = getattr(combat._model, "_design_cond", None)
|
|
178
|
+
if design_cond is not None:
|
|
179
|
+
lines.append(f"{'Design matrix condition number':<{col_w}}{design_cond:.1f}")
|
|
180
|
+
|
|
181
|
+
conv_info = getattr(combat._model, "_convergence_info", [])
|
|
182
|
+
if conv_info:
|
|
183
|
+
eb_type = "parametric" if combat.parametric else "non-parametric"
|
|
184
|
+
lines.append(f"EB convergence ({eb_type}):")
|
|
185
|
+
for info in conv_info:
|
|
186
|
+
batch_name = info["batch"]
|
|
187
|
+
if info["converged"]:
|
|
188
|
+
status = f"converged ({info['iterations']} iter)"
|
|
189
|
+
else:
|
|
190
|
+
max_change = max(info["final_gamma_change"], info["final_delta_change"])
|
|
191
|
+
status = f"NOT CONVERGED ({info['iterations']} iter, \u0394={max_change:.2e})"
|
|
192
|
+
lines.append(f" {batch_name!s:<{col_w - 2}}{status}")
|
|
193
|
+
|
|
194
|
+
return "\n".join(lines)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import numpy.typing as npt
|
|
@@ -13,8 +13,12 @@ from sklearn.decomposition import PCA
|
|
|
13
13
|
from sklearn.metrics import davies_bouldin_score, silhouette_score
|
|
14
14
|
from sklearn.neighbors import NearestNeighbors
|
|
15
15
|
|
|
16
|
+
from ._utils import _subset
|
|
16
17
|
from .core import ArrayLike
|
|
17
18
|
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from .sklearn_api import ComBat
|
|
21
|
+
|
|
18
22
|
|
|
19
23
|
def _compute_pca_embedding(
|
|
20
24
|
X_before: npt.NDArray[Any],
|
|
@@ -623,225 +627,134 @@ def _compute_alignment_metrics(
|
|
|
623
627
|
}
|
|
624
628
|
|
|
625
629
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
def compute_batch_metrics(
|
|
641
|
-
self,
|
|
642
|
-
X: ArrayLike,
|
|
643
|
-
batch: ArrayLike | None = None,
|
|
644
|
-
*,
|
|
645
|
-
pca_components: int | None = None,
|
|
646
|
-
k_neighbors: list[int] | None = None,
|
|
647
|
-
kbet_k0: int | None = None,
|
|
648
|
-
lisi_perplexity: int = 30,
|
|
649
|
-
n_jobs: int = 1,
|
|
650
|
-
nn_algorithm: str = "auto",
|
|
651
|
-
) -> dict[str, Any]:
|
|
652
|
-
"""
|
|
653
|
-
Compute batch effect metrics before and after ComBat correction.
|
|
654
|
-
|
|
655
|
-
Parameters
|
|
656
|
-
----------
|
|
657
|
-
X : array-like of shape (n_samples, n_features)
|
|
658
|
-
Input data to evaluate.
|
|
659
|
-
batch : array-like of shape (n_samples,), optional
|
|
660
|
-
Batch labels. If None, uses the batch stored at construction.
|
|
661
|
-
pca_components : int, optional
|
|
662
|
-
Number of PCA components for dimensionality reduction before
|
|
663
|
-
computing metrics. If None (default), metrics are computed in
|
|
664
|
-
the original feature space. Must be less than min(n_samples, n_features).
|
|
665
|
-
k_neighbors : list of int, default=[5, 10, 50]
|
|
666
|
-
Values of k for k-NN preservation metric.
|
|
667
|
-
kbet_k0 : int, optional
|
|
668
|
-
Neighborhood size for kBET. Default is 10% of samples.
|
|
669
|
-
lisi_perplexity : int, default=30
|
|
670
|
-
Perplexity for LISI computation.
|
|
671
|
-
n_jobs : int, default=1
|
|
672
|
-
Number of parallel jobs for neighbor computations.
|
|
673
|
-
nn_algorithm : {'auto', 'ball_tree', 'kd_tree', 'brute'}, default='auto'
|
|
674
|
-
Algorithm used for nearest neighbor computation. Passed to
|
|
675
|
-
``sklearn.neighbors.NearestNeighbors``.
|
|
676
|
-
|
|
677
|
-
Returns
|
|
678
|
-
-------
|
|
679
|
-
dict
|
|
680
|
-
Dictionary with three main keys:
|
|
681
|
-
|
|
682
|
-
- ``batch_effect``: Silhouette, Davies-Bouldin, kBET, LISI, variance ratio
|
|
683
|
-
(each with 'before' and 'after' values)
|
|
684
|
-
- ``preservation``: k-NN preservation fractions, distance correlation
|
|
685
|
-
- ``alignment``: Centroid distance, Levene statistic (each with
|
|
686
|
-
'before' and 'after' values)
|
|
687
|
-
|
|
688
|
-
Raises
|
|
689
|
-
------
|
|
690
|
-
ValueError
|
|
691
|
-
If the model is not fitted or if pca_components is invalid.
|
|
692
|
-
"""
|
|
693
|
-
_valid_nn = {"auto", "ball_tree", "kd_tree", "brute"}
|
|
694
|
-
if nn_algorithm not in _valid_nn:
|
|
695
|
-
raise ValueError(f"nn_algorithm must be one of {_valid_nn}, got '{nn_algorithm}'")
|
|
696
|
-
|
|
697
|
-
if not hasattr(self, "_model") or not hasattr(self._model, "_gamma_star"):
|
|
698
|
-
raise ValueError(
|
|
699
|
-
"This ComBat instance is not fitted yet. Call 'fit' before 'compute_batch_metrics'."
|
|
700
|
-
)
|
|
630
|
+
def compute_batch_metrics(
|
|
631
|
+
combat: ComBat,
|
|
632
|
+
X: ArrayLike,
|
|
633
|
+
batch: ArrayLike | None = None,
|
|
634
|
+
*,
|
|
635
|
+
pca_components: int | None = None,
|
|
636
|
+
k_neighbors: list[int] | None = None,
|
|
637
|
+
kbet_k0: int | None = None,
|
|
638
|
+
lisi_perplexity: int = 30,
|
|
639
|
+
n_jobs: int = 1,
|
|
640
|
+
nn_algorithm: str = "auto",
|
|
641
|
+
) -> dict[str, Any]:
|
|
642
|
+
"""
|
|
643
|
+
Compute batch effect metrics before and after ComBat correction.
|
|
701
644
|
|
|
702
|
-
|
|
703
|
-
|
|
645
|
+
Parameters
|
|
646
|
+
----------
|
|
647
|
+
combat : ComBat
|
|
648
|
+
A fitted ``ComBat`` instance.
|
|
649
|
+
X : array-like of shape (n_samples, n_features)
|
|
650
|
+
Input data to evaluate.
|
|
651
|
+
batch : array-like of shape (n_samples,), optional
|
|
652
|
+
Batch labels. If None, uses the batch stored at construction.
|
|
653
|
+
pca_components : int, optional
|
|
654
|
+
Number of PCA components for dimensionality reduction before
|
|
655
|
+
computing metrics. If None (default), metrics are computed in
|
|
656
|
+
the original feature space. Must be less than min(n_samples, n_features).
|
|
657
|
+
k_neighbors : list of int, default=[5, 10, 50]
|
|
658
|
+
Values of k for k-NN preservation metric.
|
|
659
|
+
kbet_k0 : int, optional
|
|
660
|
+
Neighborhood size for kBET. Default is 10% of samples.
|
|
661
|
+
lisi_perplexity : int, default=30
|
|
662
|
+
Perplexity for LISI computation.
|
|
663
|
+
n_jobs : int, default=1
|
|
664
|
+
Number of parallel jobs for neighbor computations.
|
|
665
|
+
nn_algorithm : {'auto', 'ball_tree', 'kd_tree', 'brute'}, default='auto'
|
|
666
|
+
Algorithm used for nearest neighbor computation. Passed to
|
|
667
|
+
``sklearn.neighbors.NearestNeighbors``.
|
|
704
668
|
|
|
705
|
-
|
|
669
|
+
Returns
|
|
670
|
+
-------
|
|
671
|
+
dict
|
|
672
|
+
Dictionary with three main keys:
|
|
673
|
+
|
|
674
|
+
- ``batch_effect``: Silhouette, Davies-Bouldin, kBET, LISI, variance ratio
|
|
675
|
+
(each with 'before' and 'after' values)
|
|
676
|
+
- ``preservation``: k-NN preservation fractions, distance correlation
|
|
677
|
+
- ``alignment``: Centroid distance, Levene statistic (each with
|
|
678
|
+
'before' and 'after' values)
|
|
679
|
+
|
|
680
|
+
Raises
|
|
681
|
+
------
|
|
682
|
+
ValueError
|
|
683
|
+
If the model is not fitted or if pca_components is invalid.
|
|
684
|
+
"""
|
|
685
|
+
_valid_nn = {"auto", "ball_tree", "kd_tree", "brute"}
|
|
686
|
+
if nn_algorithm not in _valid_nn:
|
|
687
|
+
raise ValueError(f"nn_algorithm must be one of {_valid_nn}, got '{nn_algorithm}'")
|
|
706
688
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if isinstance(batch, pd.Series | pd.DataFrame):
|
|
711
|
-
batch_vec = batch.loc[idx] if hasattr(batch, "loc") else batch
|
|
712
|
-
elif isinstance(batch, np.ndarray):
|
|
713
|
-
batch_vec = pd.Series(batch, index=idx)
|
|
714
|
-
else:
|
|
715
|
-
batch_vec = pd.Series(batch, index=idx)
|
|
716
|
-
|
|
717
|
-
batch_labels = np.array(batch_vec)
|
|
718
|
-
|
|
719
|
-
X_before = X.values
|
|
720
|
-
X_after = self.transform(X).values # type: ignore[attr-defined]
|
|
721
|
-
|
|
722
|
-
n_samples, n_features = X_before.shape
|
|
723
|
-
if kbet_k0 is None:
|
|
724
|
-
kbet_k0 = max(10, int(0.10 * n_samples))
|
|
725
|
-
if k_neighbors is None:
|
|
726
|
-
k_neighbors = [5, 10, 50]
|
|
727
|
-
|
|
728
|
-
# Validate and apply PCA if requested
|
|
729
|
-
if pca_components is not None:
|
|
730
|
-
max_components = min(n_samples, n_features)
|
|
731
|
-
if pca_components >= max_components:
|
|
732
|
-
raise ValueError(
|
|
733
|
-
f"pca_components={pca_components} must be less than "
|
|
734
|
-
f"min(n_samples, n_features)={max_components}."
|
|
735
|
-
)
|
|
736
|
-
X_before_pca, X_after_pca, _ = _compute_pca_embedding(X_before, X_after, pca_components)
|
|
737
|
-
else:
|
|
738
|
-
X_before_pca = X_before
|
|
739
|
-
X_after_pca = X_after
|
|
740
|
-
|
|
741
|
-
batch_effect = _compute_batch_effect_metrics(
|
|
742
|
-
X_before_pca,
|
|
743
|
-
X_after_pca,
|
|
744
|
-
batch_labels,
|
|
745
|
-
kbet_k0=kbet_k0,
|
|
746
|
-
lisi_perplexity=lisi_perplexity,
|
|
747
|
-
nn_algorithm=nn_algorithm,
|
|
748
|
-
)
|
|
749
|
-
preservation = _compute_preservation_metrics(
|
|
750
|
-
X_before_pca,
|
|
751
|
-
X_after_pca,
|
|
752
|
-
k_neighbors,
|
|
753
|
-
n_jobs,
|
|
754
|
-
nn_algorithm=nn_algorithm,
|
|
755
|
-
)
|
|
756
|
-
alignment = _compute_alignment_metrics(
|
|
757
|
-
X_before,
|
|
758
|
-
X_after,
|
|
759
|
-
X_before_pca,
|
|
760
|
-
X_after_pca,
|
|
761
|
-
batch_labels,
|
|
689
|
+
if not hasattr(combat, "_model") or not hasattr(combat._model, "_gamma_star"):
|
|
690
|
+
raise ValueError(
|
|
691
|
+
"This ComBat instance is not fitted yet. Call 'fit' before 'compute_batch_metrics'."
|
|
762
692
|
)
|
|
763
693
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
"preservation": preservation,
|
|
767
|
-
"alignment": alignment,
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
def feature_batch_importance(
|
|
771
|
-
self,
|
|
772
|
-
mode: Literal["magnitude", "distribution"] = "magnitude",
|
|
773
|
-
) -> pd.DataFrame:
|
|
774
|
-
"""Compute per-feature batch effect magnitude.
|
|
775
|
-
|
|
776
|
-
Returns a DataFrame with columns ``location``, ``scale``, and
|
|
777
|
-
``combined``. Location is the RMS of gamma across batches
|
|
778
|
-
(standardized mean shifts). Scale is the RMS of log-delta across
|
|
779
|
-
batches (log-fold variance change). Combined is the Euclidean norm
|
|
780
|
-
sqrt(location**2 + scale**2). Using RMS provides L2-consistent
|
|
781
|
-
aggregation; using log(delta) ensures symmetry.
|
|
782
|
-
|
|
783
|
-
Parameters
|
|
784
|
-
----------
|
|
785
|
-
mode : {'magnitude', 'distribution'}, default='magnitude'
|
|
786
|
-
- 'magnitude': Returns L2-consistent absolute batch effect magnitudes.
|
|
787
|
-
Suitable for ranking, thresholding, and cross-dataset comparison.
|
|
788
|
-
- 'distribution': Returns column-wise normalized proportions (each column
|
|
789
|
-
sums to 1, values in range [0, 1]), representing the relative contribution
|
|
790
|
-
of each feature to the total location, scale, or combined batch effect.
|
|
791
|
-
Note: normalization is applied independently to each column, so the
|
|
792
|
-
Euclidean relationship (combined**2 = location**2 + scale**2) no longer holds.
|
|
793
|
-
|
|
794
|
-
Returns
|
|
795
|
-
-------
|
|
796
|
-
pd.DataFrame
|
|
797
|
-
DataFrame with index=feature names, columns=['location', 'scale', 'combined'],
|
|
798
|
-
sorted by 'combined' descending.
|
|
799
|
-
|
|
800
|
-
Raises
|
|
801
|
-
------
|
|
802
|
-
ValueError
|
|
803
|
-
If the model is not fitted or if mode is invalid.
|
|
804
|
-
"""
|
|
805
|
-
if not hasattr(self, "_model") or not hasattr(self._model, "_gamma_star"):
|
|
806
|
-
raise ValueError(
|
|
807
|
-
"This ComBat instance is not fitted yet. "
|
|
808
|
-
"Call 'fit' before 'feature_batch_importance'."
|
|
809
|
-
)
|
|
694
|
+
if not isinstance(X, pd.DataFrame):
|
|
695
|
+
X = pd.DataFrame(X)
|
|
810
696
|
|
|
811
|
-
|
|
812
|
-
raise ValueError(f"mode must be 'magnitude' or 'distribution', got '{mode}'")
|
|
697
|
+
idx = X.index
|
|
813
698
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
699
|
+
if batch is None:
|
|
700
|
+
batch_vec = _subset(combat.batch, idx)
|
|
701
|
+
else:
|
|
702
|
+
if isinstance(batch, pd.Series | pd.DataFrame):
|
|
703
|
+
batch_vec = batch.loc[idx] if hasattr(batch, "loc") else batch
|
|
704
|
+
elif isinstance(batch, np.ndarray):
|
|
705
|
+
batch_vec = pd.Series(batch, index=idx)
|
|
706
|
+
else:
|
|
707
|
+
batch_vec = pd.Series(batch, index=idx)
|
|
817
708
|
|
|
818
|
-
|
|
819
|
-
location = np.sqrt((gamma_star**2).mean(axis=0))
|
|
709
|
+
batch_labels = np.array(batch_vec)
|
|
820
710
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
711
|
+
X_before = X.values
|
|
712
|
+
X_after = combat.transform(X).values
|
|
713
|
+
|
|
714
|
+
n_samples, n_features = X_before.shape
|
|
715
|
+
if kbet_k0 is None:
|
|
716
|
+
kbet_k0 = max(10, int(0.10 * n_samples))
|
|
717
|
+
if k_neighbors is None:
|
|
718
|
+
k_neighbors = [5, 10, 50]
|
|
719
|
+
|
|
720
|
+
# Validate and apply PCA if requested
|
|
721
|
+
if pca_components is not None:
|
|
722
|
+
max_components = min(n_samples, n_features)
|
|
723
|
+
if pca_components >= max_components:
|
|
724
|
+
raise ValueError(
|
|
725
|
+
f"pca_components={pca_components} must be less than "
|
|
726
|
+
f"min(n_samples, n_features)={max_components}."
|
|
727
|
+
)
|
|
728
|
+
X_before_pca, X_after_pca, _ = _compute_pca_embedding(X_before, X_after, pca_components)
|
|
729
|
+
else:
|
|
730
|
+
X_before_pca = X_before
|
|
731
|
+
X_after_pca = X_after
|
|
732
|
+
|
|
733
|
+
batch_effect = _compute_batch_effect_metrics(
|
|
734
|
+
X_before_pca,
|
|
735
|
+
X_after_pca,
|
|
736
|
+
batch_labels,
|
|
737
|
+
kbet_k0=kbet_k0,
|
|
738
|
+
lisi_perplexity=lisi_perplexity,
|
|
739
|
+
nn_algorithm=nn_algorithm,
|
|
740
|
+
)
|
|
741
|
+
preservation = _compute_preservation_metrics(
|
|
742
|
+
X_before_pca,
|
|
743
|
+
X_after_pca,
|
|
744
|
+
k_neighbors,
|
|
745
|
+
n_jobs,
|
|
746
|
+
nn_algorithm=nn_algorithm,
|
|
747
|
+
)
|
|
748
|
+
alignment = _compute_alignment_metrics(
|
|
749
|
+
X_before,
|
|
750
|
+
X_after,
|
|
751
|
+
X_before_pca,
|
|
752
|
+
X_after_pca,
|
|
753
|
+
batch_labels,
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
"batch_effect": batch_effect,
|
|
758
|
+
"preservation": preservation,
|
|
759
|
+
"alignment": alignment,
|
|
760
|
+
}
|