carve-validate 1.0.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.
- carve/__init__.py +8 -0
- carve/_accuracy.py +42 -0
- carve/_consensus.py +213 -0
- carve/_grids.py +153 -0
- carve/_output.py +132 -0
- carve/_pipeline.py +182 -0
- carve/_plotting.py +1568 -0
- carve/_runner.py +561 -0
- carve/_selection.py +401 -0
- carve/_types.py +130 -0
- carve/_utils.py +306 -0
- carve/api.py +1763 -0
- carve/cluster.py +332 -0
- carve_validate-1.0.0.dist-info/METADATA +176 -0
- carve_validate-1.0.0.dist-info/RECORD +18 -0
- carve_validate-1.0.0.dist-info/WHEEL +5 -0
- carve_validate-1.0.0.dist-info/licenses/LICENSE +21 -0
- carve_validate-1.0.0.dist-info/top_level.txt +1 -0
carve/__init__.py
ADDED
carve/_accuracy.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Accuracy-based generalizability metrics."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from ._utils import align_cluster_labels
|
|
6
|
+
|
|
7
|
+
# (sample_indices, true_labels, predicted_labels) triple from a prediction run
|
|
8
|
+
PredictionRun = tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def compute_generalizability_scores(
|
|
12
|
+
n_samples: int,
|
|
13
|
+
runs: list[PredictionRun],
|
|
14
|
+
) -> np.ndarray:
|
|
15
|
+
"""Compute per-sample generalizability from prediction runs.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
n_samples : int
|
|
20
|
+
Total number of samples in the original dataset.
|
|
21
|
+
runs : list of tuple
|
|
22
|
+
Each run is (sample_indices, true_labels, predicted_labels).
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
scores : ndarray of shape (n_samples,)
|
|
27
|
+
Per-sample accuracy across available runs. Samples never evaluated
|
|
28
|
+
receive score 0.
|
|
29
|
+
"""
|
|
30
|
+
correct = np.zeros(n_samples, int)
|
|
31
|
+
total = np.zeros(n_samples, int)
|
|
32
|
+
|
|
33
|
+
for sample_idx, true_labels, predicted_labels in runs:
|
|
34
|
+
aligned_predictions = align_cluster_labels(true_labels, predicted_labels)
|
|
35
|
+
correct_mask = (true_labels == aligned_predictions).astype(int)
|
|
36
|
+
np.add.at(correct, sample_idx, correct_mask)
|
|
37
|
+
np.add.at(total, sample_idx, 1)
|
|
38
|
+
|
|
39
|
+
out = np.zeros(n_samples, float)
|
|
40
|
+
valid = total > 0
|
|
41
|
+
out[valid] = correct[valid] / total[valid]
|
|
42
|
+
return out
|
carve/_consensus.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Consensus matrix utilities and stability metrics.
|
|
2
|
+
|
|
3
|
+
This module builds consensus matrices from repeated clustering runs and
|
|
4
|
+
derives stability statistics such as Gini, cross-entropy, and PAC.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy.cluster.hierarchy import linkage, leaves_list
|
|
9
|
+
from scipy.spatial.distance import squareform
|
|
10
|
+
|
|
11
|
+
# (sample_indices, labels) pair from a single clustering run
|
|
12
|
+
SampledLabels = tuple[np.ndarray, np.ndarray]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compute_consensus_matrix(
|
|
16
|
+
n_samples: int,
|
|
17
|
+
runs: list[SampledLabels],
|
|
18
|
+
*,
|
|
19
|
+
return_counts: bool = False,
|
|
20
|
+
) -> np.ndarray | tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
21
|
+
"""Compute a consensus matrix from resampled clustering runs.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
n_samples : int
|
|
26
|
+
Total number of samples in the original dataset.
|
|
27
|
+
runs : list of tuple of (ndarray, ndarray)
|
|
28
|
+
Each run is (sample_indices, labels), where ``sample_indices`` are
|
|
29
|
+
integer indices into the original dataset and ``labels`` are cluster
|
|
30
|
+
assignments for those indices.
|
|
31
|
+
return_counts : bool, default=False
|
|
32
|
+
If True, also return raw co-cluster and co-sample counts.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
consensus_matrix : ndarray of shape (n_samples, n_samples)
|
|
37
|
+
Fraction of times each sample pair co-clustered when co-sampled.
|
|
38
|
+
Pairs never co-sampled are set to NaN.
|
|
39
|
+
co_cluster_counts : ndarray of shape (n_samples, n_samples)
|
|
40
|
+
Raw counts of co-clustering across runs (only if ``return_counts``).
|
|
41
|
+
co_sample_counts : ndarray of shape (n_samples, n_samples)
|
|
42
|
+
Raw counts of co-sampling across runs (only if ``return_counts``).
|
|
43
|
+
"""
|
|
44
|
+
# co_cluster_counts = np.zeros((n_samples, n_samples), dtype=float)
|
|
45
|
+
# co_sample_counts = np.zeros((n_samples, n_samples), dtype=float)
|
|
46
|
+
|
|
47
|
+
# for sample_idx, labels in runs:
|
|
48
|
+
# co_sample_counts[np.ix_(sample_idx, sample_idx)] += 1
|
|
49
|
+
|
|
50
|
+
# for label in np.unique(labels):
|
|
51
|
+
# label_idx = sample_idx[labels == label]
|
|
52
|
+
# co_cluster_counts[np.ix_(label_idx, label_idx)] += 1
|
|
53
|
+
|
|
54
|
+
N = sum(len(np.unique(labels)) for _, labels in runs)
|
|
55
|
+
|
|
56
|
+
S = np.zeros((n_samples, len(runs)), dtype=np.float32)
|
|
57
|
+
B = np.zeros((n_samples, N), dtype=np.float32)
|
|
58
|
+
|
|
59
|
+
col = 0
|
|
60
|
+
for r, (sample_idx, labels) in enumerate(runs):
|
|
61
|
+
S[sample_idx, r] = 1
|
|
62
|
+
for label in np.unique(labels):
|
|
63
|
+
members = sample_idx[labels == label]
|
|
64
|
+
B[members, col] = 1
|
|
65
|
+
col += 1
|
|
66
|
+
|
|
67
|
+
co_sample_counts = S @ S.T
|
|
68
|
+
co_cluster_counts = B @ B.T
|
|
69
|
+
|
|
70
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
71
|
+
consensus_matrix = co_cluster_counts / co_sample_counts
|
|
72
|
+
consensus_matrix[co_sample_counts == 0] = np.nan
|
|
73
|
+
|
|
74
|
+
if return_counts:
|
|
75
|
+
return consensus_matrix, co_cluster_counts, co_sample_counts
|
|
76
|
+
else:
|
|
77
|
+
return consensus_matrix
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def reorder_consensus_matrix(
|
|
81
|
+
consensus_matrix: np.ndarray,
|
|
82
|
+
*,
|
|
83
|
+
fill_nan_for_order: float = 0.0,
|
|
84
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
85
|
+
"""Reorder a consensus matrix by hierarchical clustering of distances.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
consensus_matrix : ndarray of shape (n_samples, n_samples)
|
|
90
|
+
Consensus matrix with values in [0, 1] and NaNs for never-sampled pairs.
|
|
91
|
+
fill_nan_for_order : float, default=0.0
|
|
92
|
+
Value used to replace NaNs when computing the clustering order only.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
reordered : ndarray of shape (n_samples, n_samples)
|
|
97
|
+
Consensus matrix reordered by dendrogram leaves.
|
|
98
|
+
order : ndarray of shape (n_samples,)
|
|
99
|
+
The permutation of indices applied to rows and columns.
|
|
100
|
+
"""
|
|
101
|
+
matrix_for_order = np.nan_to_num(consensus_matrix, nan=fill_nan_for_order)
|
|
102
|
+
distances = squareform(1.0 - matrix_for_order, checks=False)
|
|
103
|
+
linkage_matrix = linkage(distances, method="average")
|
|
104
|
+
order = leaves_list(linkage_matrix)
|
|
105
|
+
|
|
106
|
+
return consensus_matrix[np.ix_(order, order)], order
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def compute_consensus_metrics(
|
|
110
|
+
consensus_matrices: list[np.ndarray],
|
|
111
|
+
) -> tuple[list[np.ndarray], list[np.ndarray], list[float]]:
|
|
112
|
+
"""Compute stability metrics for multiple consensus matrices.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
consensus_matrices : list of ndarray
|
|
117
|
+
Consensus matrices to evaluate.
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
gini_list : list of ndarray
|
|
122
|
+
Per-sample stability scores based on the Gini-style statistic.
|
|
123
|
+
ce_list : list of ndarray
|
|
124
|
+
Per-sample stability scores based on cross-entropy.
|
|
125
|
+
pac_list : list of float
|
|
126
|
+
Global PAC-based stability scores (1 - PAC).
|
|
127
|
+
"""
|
|
128
|
+
gini_list = []
|
|
129
|
+
ce_list = []
|
|
130
|
+
pac_list = []
|
|
131
|
+
|
|
132
|
+
for consensus_matrix in consensus_matrices:
|
|
133
|
+
s_gini, s_ce = stability_from_consensus(consensus_matrix)
|
|
134
|
+
pac = compute_consensus_pac(consensus_matrix)
|
|
135
|
+
|
|
136
|
+
gini_list.append(s_gini)
|
|
137
|
+
ce_list.append(s_ce)
|
|
138
|
+
pac_list.append(pac)
|
|
139
|
+
|
|
140
|
+
return gini_list, ce_list, pac_list
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def stability_from_consensus(
|
|
144
|
+
consensus_matrix: np.ndarray,
|
|
145
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
146
|
+
"""Compute per-sample stability scores from a consensus matrix.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
consensus_matrix : ndarray of shape (n_samples, n_samples)
|
|
151
|
+
Consensus matrix with values in [0, 1] and NaNs for never-sampled pairs.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
stability_gini : ndarray of shape (n_samples,)
|
|
156
|
+
Gini-based stability score per sample in [0, 1].
|
|
157
|
+
stability_ce : ndarray of shape (n_samples,)
|
|
158
|
+
Cross-entropy-based stability score per sample in [0, 1].
|
|
159
|
+
"""
|
|
160
|
+
consensus_probs = np.array(consensus_matrix, dtype=float, copy=True)
|
|
161
|
+
np.fill_diagonal(consensus_probs, np.nan)
|
|
162
|
+
|
|
163
|
+
term = consensus_probs * (1.0 - consensus_probs)
|
|
164
|
+
|
|
165
|
+
clipped = np.clip(consensus_probs, 1e-12, 1.0 - 1e-12)
|
|
166
|
+
entropy = -(clipped * np.log(clipped) + (1.0 - clipped) * np.log(1.0 - clipped))
|
|
167
|
+
|
|
168
|
+
# Gini uncertainty in [0, 0.5]; CE uncertainty in [0, log2]
|
|
169
|
+
uncertainty_gini = 2.0 * np.nanmean(term, axis=1)
|
|
170
|
+
uncertainty_ce = np.nanmean(entropy, axis=1)
|
|
171
|
+
|
|
172
|
+
# Rescale to [0, 1] stability scores
|
|
173
|
+
stability_gini = 1.0 - np.clip(2.0 * uncertainty_gini, 0.0, 1.0)
|
|
174
|
+
stability_ce = 1.0 - np.clip(uncertainty_ce / np.log(2.0), 0.0, 1.0)
|
|
175
|
+
|
|
176
|
+
return stability_gini, stability_ce
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def compute_consensus_pac(
|
|
180
|
+
consensus_matrix: np.ndarray,
|
|
181
|
+
*,
|
|
182
|
+
tau: float = 0.05,
|
|
183
|
+
) -> float:
|
|
184
|
+
"""Compute PAC-based stability score from a consensus matrix.
|
|
185
|
+
|
|
186
|
+
PAC measures the proportion of ambiguous consensus values in
|
|
187
|
+
(tau, 1-tau). This function returns 1 - PAC so that larger values
|
|
188
|
+
indicate greater stability.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
consensus_matrix : ndarray of shape (n_samples, n_samples)
|
|
193
|
+
Consensus matrix with values in [0, 1] and NaNs for never-sampled pairs.
|
|
194
|
+
tau : float, default=0.05
|
|
195
|
+
Ambiguity threshold defining the PAC interval.
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
score : float
|
|
200
|
+
Stability score in [0, 1], or NaN if no valid pairs exist.
|
|
201
|
+
"""
|
|
202
|
+
consensus_probs = np.array(consensus_matrix, dtype=float, copy=True)
|
|
203
|
+
n_samples = consensus_probs.shape[0]
|
|
204
|
+
mask = ~np.eye(n_samples, dtype=bool)
|
|
205
|
+
values = consensus_probs[mask]
|
|
206
|
+
values = values[~np.isnan(values)]
|
|
207
|
+
|
|
208
|
+
if values.size == 0:
|
|
209
|
+
return np.nan
|
|
210
|
+
|
|
211
|
+
ambiguous = ((values > tau) & (values < (1 - tau))).sum()
|
|
212
|
+
pac = ambiguous / values.size
|
|
213
|
+
return 1.0 - pac
|
carve/_grids.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Default estimator and preprocessing grids for CARVE."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from sklearn.cluster import KMeans, AgglomerativeClustering
|
|
7
|
+
from sklearn.decomposition import PCA
|
|
8
|
+
from sklearn.manifold import TSNE
|
|
9
|
+
from sklearn.neighbors import NearestNeighbors
|
|
10
|
+
from sklearn.preprocessing import FunctionTransformer, StandardScaler
|
|
11
|
+
from umap import UMAP
|
|
12
|
+
|
|
13
|
+
from .cluster import SpectralClusteringCARVE
|
|
14
|
+
from ._types import GridSpec, PreprocSpec
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def estimate_knn_gamma(
|
|
18
|
+
X: np.ndarray,
|
|
19
|
+
n_neighbors: int = 7,
|
|
20
|
+
multipliers: tuple[float, ...] = (0.5, 1.0, 2.0),
|
|
21
|
+
) -> list[float]:
|
|
22
|
+
"""Estimate RBF gamma values using a k-NN median heuristic.
|
|
23
|
+
|
|
24
|
+
Fits a NearestNeighbors model, takes the k-th neighbor distance for
|
|
25
|
+
each point, computes sigma = median(kth_distances), and returns
|
|
26
|
+
gamma = 1 / (2 * (m * sigma)^2) for each multiplier m.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
X : ndarray of shape (n_samples, n_features)
|
|
31
|
+
Input data.
|
|
32
|
+
n_neighbors : int, default=7
|
|
33
|
+
Number of neighbors.
|
|
34
|
+
multipliers : tuple of float, default=(0.5, 1.0, 2.0)
|
|
35
|
+
Scale multipliers applied to sigma.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
gammas : list of float
|
|
40
|
+
Gamma values spanning a range around the local scale.
|
|
41
|
+
"""
|
|
42
|
+
nn = NearestNeighbors(n_neighbors=n_neighbors)
|
|
43
|
+
nn.fit(X)
|
|
44
|
+
dists, _ = nn.kneighbors(X)
|
|
45
|
+
kth_dists = dists[:, -1]
|
|
46
|
+
sigma = float(np.median(kth_dists))
|
|
47
|
+
if sigma <= 0:
|
|
48
|
+
sigma = (
|
|
49
|
+
float(np.mean(kth_dists[kth_dists > 0])) if np.any(kth_dists > 0) else 1.0
|
|
50
|
+
)
|
|
51
|
+
return [float(1.0 / (2.0 * (m * sigma) ** 2)) for m in multipliers]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def default_estimator_grids(
|
|
55
|
+
X: np.ndarray,
|
|
56
|
+
n_clusters: int | np.ndarray = 10,
|
|
57
|
+
preset: Literal["light", "full"] = "light",
|
|
58
|
+
) -> list[GridSpec]:
|
|
59
|
+
"""Return default clustering estimator grids.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
X : ndarray of shape (n_samples, n_features)
|
|
64
|
+
Input data used to derive data-driven hyperparameters (e.g., gamma).
|
|
65
|
+
n_clusters : int or ndarray, default=10
|
|
66
|
+
Number(s) of clusters to evaluate.
|
|
67
|
+
preset : {"light", "full"}, default="light"
|
|
68
|
+
``"light"`` includes KMeans, Ward-linkage agglomerative, and
|
|
69
|
+
self-tuning spectral clustering. ``"full"`` adds average- and
|
|
70
|
+
single-linkage agglomerative clustering and RBF-kernel spectral
|
|
71
|
+
clustering with data-driven gamma.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
grids : list of tuple
|
|
76
|
+
List of (EstimatorClass, param_grid) tuples suitable for
|
|
77
|
+
``sklearn.model_selection.ParameterGrid``.
|
|
78
|
+
"""
|
|
79
|
+
ks = list(np.asarray(n_clusters).tolist())
|
|
80
|
+
|
|
81
|
+
grids: list[GridSpec] = [
|
|
82
|
+
(KMeans, {"n_clusters": ks}),
|
|
83
|
+
(AgglomerativeClustering, {"n_clusters": ks, "linkage": ["ward"]}),
|
|
84
|
+
(SpectralClusteringCARVE, {"n_clusters": ks, "affinity": ["self_tuning"]}),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
if preset == "full":
|
|
88
|
+
grids.append(
|
|
89
|
+
(
|
|
90
|
+
AgglomerativeClustering,
|
|
91
|
+
{"n_clusters": ks, "linkage": ["average", "single", "complete"]},
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
grids.append(
|
|
96
|
+
(
|
|
97
|
+
SpectralClusteringCARVE,
|
|
98
|
+
{"n_clusters": ks, "affinity": ["rbf"], "gamma": estimate_knn_gamma(X)},
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return grids
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def default_normalization_options() -> list[PreprocSpec]:
|
|
106
|
+
"""Return default normalization preprocessing options.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
options : list of tuple
|
|
111
|
+
List of (TransformerClass, param_grid) pairs.
|
|
112
|
+
"""
|
|
113
|
+
return [
|
|
114
|
+
(FunctionTransformer, {}),
|
|
115
|
+
(StandardScaler, {}),
|
|
116
|
+
(FunctionTransformer, {"func": [np.log1p]}),
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def default_dim_reduction_options(
|
|
121
|
+
X: np.ndarray,
|
|
122
|
+
subsample_ratio: float = 0.6,
|
|
123
|
+
) -> list[PreprocSpec]:
|
|
124
|
+
"""Return default dimensionality reduction options.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
X : ndarray of shape (n_samples, n_features)
|
|
129
|
+
Input data used to determine feasible component counts.
|
|
130
|
+
subsample_ratio : float, default=0.6
|
|
131
|
+
Subsample ratio used in validation; constrains DR hyperparameters.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
options : list of tuple
|
|
136
|
+
List of (TransformerClass, param_grid) pairs.
|
|
137
|
+
"""
|
|
138
|
+
n_samples, p = X.shape
|
|
139
|
+
min_n = int(round(n_samples * (1 - subsample_ratio))) - 1
|
|
140
|
+
|
|
141
|
+
return [
|
|
142
|
+
(FunctionTransformer, {}),
|
|
143
|
+
(PCA, {"n_components": list(range(2, min(min_n, p)))}),
|
|
144
|
+
(TSNE, {"n_components": [2], "perplexity": list(range(5, min(min_n, 51)))}),
|
|
145
|
+
(
|
|
146
|
+
UMAP,
|
|
147
|
+
{
|
|
148
|
+
"n_components": list(range(2, min(min_n, p))),
|
|
149
|
+
"n_neighbors": list(range(5, 51)),
|
|
150
|
+
"min_dist": [0.1],
|
|
151
|
+
},
|
|
152
|
+
),
|
|
153
|
+
]
|
carve/_output.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Console output helpers for CARVE runs."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from typing import Any
|
|
6
|
+
from sklearn.base import ClusterMixin
|
|
7
|
+
from sklearn.model_selection import ParameterGrid
|
|
8
|
+
from tqdm.auto import tqdm
|
|
9
|
+
|
|
10
|
+
from ._types import EstimatorRecord, GridSpec
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _print_run_header(
|
|
14
|
+
X: np.ndarray,
|
|
15
|
+
n_clusters: int | np.ndarray,
|
|
16
|
+
n_resamples: int,
|
|
17
|
+
subsample_ratio: float,
|
|
18
|
+
estimator_grids: list[GridSpec],
|
|
19
|
+
n_jobs: int,
|
|
20
|
+
randomize_preprocessing: bool,
|
|
21
|
+
random_state: int | None,
|
|
22
|
+
verbose: int,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""Print a standard header describing the validation configuration.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
X : ndarray of shape (n_samples, n_features)
|
|
29
|
+
Input data.
|
|
30
|
+
n_clusters : int or ndarray
|
|
31
|
+
Number(s) of clusters evaluated.
|
|
32
|
+
n_resamples : int
|
|
33
|
+
Number of resampling iterations.
|
|
34
|
+
subsample_ratio : float
|
|
35
|
+
Proportion of samples in each subsample.
|
|
36
|
+
estimator_grids : list of tuple
|
|
37
|
+
Estimator classes and parameter grids.
|
|
38
|
+
n_jobs : int
|
|
39
|
+
Parallelism used for runs.
|
|
40
|
+
randomize_preprocessing : bool
|
|
41
|
+
Whether preprocessing is randomized.
|
|
42
|
+
random_state : int or None
|
|
43
|
+
Random seed.
|
|
44
|
+
verbose : int
|
|
45
|
+
Verbosity level; prints only if >= 2.
|
|
46
|
+
"""
|
|
47
|
+
if verbose < 2:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
total_configs = sum(len(ParameterGrid(g)) for _, g in estimator_grids)
|
|
51
|
+
line = "=" * 60
|
|
52
|
+
|
|
53
|
+
print(f"[CARVE] {line}")
|
|
54
|
+
print("[CARVE] Validation Settings:")
|
|
55
|
+
print(f"[CARVE] n_samples : {X.shape[0]}")
|
|
56
|
+
print(f"[CARVE] n_features : {X.shape[1]}")
|
|
57
|
+
print(f"[CARVE] n_clusters : {n_clusters}")
|
|
58
|
+
print(f"[CARVE] n_resamples : {n_resamples}")
|
|
59
|
+
print(f"[CARVE] subsample_ratio : {subsample_ratio}")
|
|
60
|
+
print(f"[CARVE] n_jobs : {n_jobs}")
|
|
61
|
+
print(f"[CARVE] total configs : {total_configs}")
|
|
62
|
+
print(f"[CARVE] randomize_preproc : {randomize_preprocessing}")
|
|
63
|
+
print(f"[CARVE] random_state : {random_state}")
|
|
64
|
+
print(f"[CARVE] {line}")
|
|
65
|
+
print("\n[CARVE] Starting validation ...\n")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _print_run_footer(
|
|
69
|
+
estimator_df: pd.DataFrame,
|
|
70
|
+
verbose: int,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Print a standard footer after validation completes.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
estimator_df : pandas.DataFrame
|
|
77
|
+
Results table with one row per estimator configuration.
|
|
78
|
+
verbose : int
|
|
79
|
+
Verbosity level; prints only if >= 2.
|
|
80
|
+
"""
|
|
81
|
+
if verbose < 2:
|
|
82
|
+
return
|
|
83
|
+
print(
|
|
84
|
+
f"\n[CARVE] finished. evaluated {len(estimator_df)} estimator configurations."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _log_config_progress(
|
|
89
|
+
config_idx: int,
|
|
90
|
+
total_configs: int,
|
|
91
|
+
est_class: type[ClusterMixin],
|
|
92
|
+
params: dict[str, Any],
|
|
93
|
+
record: EstimatorRecord,
|
|
94
|
+
pbar_obj: tqdm | None,
|
|
95
|
+
verbose: int = 0,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Log per-configuration progress during grid evaluation.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
config_idx : int
|
|
102
|
+
1-based index of the current configuration.
|
|
103
|
+
total_configs : int
|
|
104
|
+
Total number of configurations in the grid.
|
|
105
|
+
est_class : type
|
|
106
|
+
Estimator class being evaluated.
|
|
107
|
+
params : dict
|
|
108
|
+
Hyperparameters for the current configuration.
|
|
109
|
+
record : dict
|
|
110
|
+
Metrics for the current configuration.
|
|
111
|
+
pbar_obj : tqdm or None
|
|
112
|
+
Optional progress-bar object used to display messages.
|
|
113
|
+
verbose : int, default=0
|
|
114
|
+
Verbosity level; logs only if >= 1.
|
|
115
|
+
"""
|
|
116
|
+
if verbose <= 0:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
n_clusters = params.get("n_clusters", "?")
|
|
120
|
+
msg = (
|
|
121
|
+
f"[CARVE] [{config_idx}/{total_configs}] "
|
|
122
|
+
f"est={est_class.__name__} "
|
|
123
|
+
f"n_clusters={n_clusters} | "
|
|
124
|
+
f"ARI_stab={record['ari_stability']:.3f}±{record['ari_stability_se']:.3f} "
|
|
125
|
+
f"ARI_gen={record['ari_generalizability']:.3f}±{record['ari_generalizability_se']:.3f} "
|
|
126
|
+
# f"ARI_avg={record['ari_average']:.3f}±{record['ari_average_se']:.3f}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if pbar_obj is not None:
|
|
130
|
+
pbar_obj.write(msg)
|
|
131
|
+
else:
|
|
132
|
+
print(msg)
|