aetherscan 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.
- aetherscan/__init__.py +45 -0
- aetherscan/benchmark.py +179 -0
- aetherscan/candidate_figures.py +328 -0
- aetherscan/cli.py +3032 -0
- aetherscan/config.py +1002 -0
- aetherscan/dashboard.py +709 -0
- aetherscan/dashboard_cli.py +51 -0
- aetherscan/dashboard_launcher.py +194 -0
- aetherscan/data_generation.py +1448 -0
- aetherscan/db/__init__.py +21 -0
- aetherscan/db/db.py +2789 -0
- aetherscan/hf_hub.py +547 -0
- aetherscan/inference.py +912 -0
- aetherscan/inference_viz.py +1494 -0
- aetherscan/latent_gif.py +512 -0
- aetherscan/latent_variants.py +352 -0
- aetherscan/logger/__init__.py +21 -0
- aetherscan/logger/logger.py +467 -0
- aetherscan/logger/slack_handler.py +760 -0
- aetherscan/main.py +1269 -0
- aetherscan/manager/__init__.py +21 -0
- aetherscan/manager/manager.py +781 -0
- aetherscan/models/__init__.py +21 -0
- aetherscan/models/random_forest.py +171 -0
- aetherscan/models/vae.py +849 -0
- aetherscan/monitor/__init__.py +19 -0
- aetherscan/monitor/monitor.py +935 -0
- aetherscan/pfb.py +161 -0
- aetherscan/preprocessing.py +2718 -0
- aetherscan/rf_metrics.py +100 -0
- aetherscan/round_data.py +932 -0
- aetherscan/run_state.py +277 -0
- aetherscan/seeding.py +160 -0
- aetherscan/shap_parallel.py +238 -0
- aetherscan/tag_guards.py +206 -0
- aetherscan/train.py +7681 -0
- aetherscan-1.0.0.dist-info/METADATA +1187 -0
- aetherscan-1.0.0.dist-info/RECORD +41 -0
- aetherscan-1.0.0.dist-info/WHEEL +4 -0
- aetherscan-1.0.0.dist-info/entry_points.txt +2 -0
- aetherscan-1.0.0.dist-info/licenses/LICENSE +13 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Latent-representation variants, selection metrics, and probability calibration (#282).
|
|
3
|
+
|
|
4
|
+
The RF historically consumed the stochastic sampled `z` — nobody ever tested whether that is
|
|
5
|
+
the best representation, and the encoder's `z_mean`/`z_log_var` were computed and thrown away
|
|
6
|
+
at the RF stage. This module defines the 8-variant catalogue trained in one sweep on the same
|
|
7
|
+
generated data and split, the feature builders that inference reuses (driven by the saved
|
|
8
|
+
config's rf.latent_variant — never hardcoded), the selection metrics (recall at a fixed low
|
|
9
|
+
FPR is primary; AUC averages over operating points the pipeline never uses), the bootstrap
|
|
10
|
+
minimum-margin tie-break toward simpler variants, and the ECE-gated probability calibrator.
|
|
11
|
+
|
|
12
|
+
Everything operates on FLATTENED per-cadence feature blocks of shape
|
|
13
|
+
(n_cadences, num_observations * latent_dim) — the exact output of
|
|
14
|
+
models.random_forest.prepare_latent_features — so builders compose by hstack and the RF
|
|
15
|
+
feature layout stays `[z_mean block | extras]`, documented for SHAP readability.
|
|
16
|
+
|
|
17
|
+
Deliberately TF-free (mirrors rf_metrics.py): imported by both train.py and inference.py, and
|
|
18
|
+
unit-testable without the scientific stack.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
from sklearn.isotonic import IsotonicRegression
|
|
27
|
+
from sklearn.linear_model import LogisticRegression
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Variant catalogue, ordered SIMPLE -> COMPLEX (by feature count, deterministic-first among
|
|
32
|
+
# equals). The minimum-margin selection walks this order, so ties break toward the earlier
|
|
33
|
+
# (simpler / cheaper-SHAP) variant. Names are persisted in config_{tag}.json (rf.latent_variant)
|
|
34
|
+
# and in artifact filenames — treat them as a stable contract.
|
|
35
|
+
VARIANT_ORDER = [
|
|
36
|
+
"z_mean", # deterministic baseline: per-obs posterior means (F = 48 at defaults)
|
|
37
|
+
"z", # legacy baseline: one stochastic sample (F = 48)
|
|
38
|
+
"z_aug", # trained on z_mean + K sampled draws as extra ROWS, evaluated on z_mean (F = 48)
|
|
39
|
+
"z_mean_total_kl", # z_mean + total KL per cadence (F = 49)
|
|
40
|
+
"z_mean_obs_logvar", # z_mean + per-observation mean log_var (F = 48 + num_obs = 54)
|
|
41
|
+
"z_mean_dim_logvar", # z_mean + per-dim mean log_var over obs (F = 48 + latent_dim = 56)
|
|
42
|
+
"z_mean_logvar_active", # z_mean + log_var restricted to ACTIVE dims (F = 48..96)
|
|
43
|
+
"z_mean_logvar", # full per-dimension uncertainty (F = 96)
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _reshape_blocks(flat: np.ndarray, num_observations: int, latent_dim: int) -> np.ndarray:
|
|
48
|
+
return np.asarray(flat).reshape(-1, num_observations, latent_dim)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def per_cadence_kl(z_mean_flat: np.ndarray, z_log_var_flat: np.ndarray) -> np.ndarray:
|
|
52
|
+
"""Total KL divergence per cadence: -0.5*(1 + lv - mu^2 - e^lv) summed over every
|
|
53
|
+
(observation, dim) — the same closed form as models/vae.py's loss term."""
|
|
54
|
+
z_mean = np.asarray(z_mean_flat, dtype=np.float64)
|
|
55
|
+
z_log_var = np.asarray(z_log_var_flat, dtype=np.float64)
|
|
56
|
+
kl = -0.5 * (1.0 + z_log_var - np.square(z_mean) - np.exp(z_log_var))
|
|
57
|
+
return kl.sum(axis=1).astype(np.float32)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def latent_dim_variances(
|
|
61
|
+
z_mean_flat: np.ndarray, num_observations: int, latent_dim: int
|
|
62
|
+
) -> np.ndarray:
|
|
63
|
+
"""Per-dim z_mean variance across samples (pooling all observations) — the raw quantity
|
|
64
|
+
the Active Units threshold cuts on. Exposed so callers can log the margins: a dim a hair
|
|
65
|
+
below the cutoff and a dim that went dark read identically in the AU count but mean
|
|
66
|
+
opposite things for an A/B parity decision (#288's fp16 verdict hinged on exactly this)."""
|
|
67
|
+
per_obs = _reshape_blocks(z_mean_flat, num_observations, latent_dim)
|
|
68
|
+
pooled = per_obs.reshape(-1, latent_dim)
|
|
69
|
+
return pooled.var(axis=0)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def active_latent_dims(
|
|
73
|
+
z_mean_flat: np.ndarray, num_observations: int, latent_dim: int, threshold: float
|
|
74
|
+
) -> list[int]:
|
|
75
|
+
"""
|
|
76
|
+
Active Units (Burda et al.): dims whose z_mean variance across samples (pooling all
|
|
77
|
+
observations) exceeds `threshold`. Collapsed dims carry near-constant posteriors and
|
|
78
|
+
contribute dead-weight log_var features — this gates the z_mean_logvar_active variant
|
|
79
|
+
and feeds check_posterior_collapse.
|
|
80
|
+
"""
|
|
81
|
+
variances = latent_dim_variances(z_mean_flat, num_observations, latent_dim)
|
|
82
|
+
return [int(d) for d in np.nonzero(variances > threshold)[0]]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _active_logvar_columns(
|
|
86
|
+
active_dims: list[int], num_observations: int, latent_dim: int
|
|
87
|
+
) -> list[int]:
|
|
88
|
+
"""Column indices of the active dims inside a flattened per-cadence block (obs-major)."""
|
|
89
|
+
return [
|
|
90
|
+
obs * latent_dim + dim for obs in range(num_observations) for dim in sorted(active_dims)
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def variant_feature_count(
|
|
95
|
+
variant: str, num_observations: int, latent_dim: int, active_dims: list[int] | None = None
|
|
96
|
+
) -> int:
|
|
97
|
+
base = num_observations * latent_dim
|
|
98
|
+
if variant in ("z", "z_mean", "z_aug"):
|
|
99
|
+
return base
|
|
100
|
+
if variant == "z_mean_total_kl":
|
|
101
|
+
return base + 1
|
|
102
|
+
if variant == "z_mean_obs_logvar":
|
|
103
|
+
return base + num_observations
|
|
104
|
+
if variant == "z_mean_dim_logvar":
|
|
105
|
+
return base + latent_dim
|
|
106
|
+
if variant == "z_mean_logvar_active":
|
|
107
|
+
return base + num_observations * len(active_dims or [])
|
|
108
|
+
if variant == "z_mean_logvar":
|
|
109
|
+
return 2 * base
|
|
110
|
+
raise ValueError(f"Unknown latent variant {variant!r}; expected one of {VARIANT_ORDER}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def build_variant_features(
|
|
114
|
+
variant: str,
|
|
115
|
+
lead_flat: np.ndarray,
|
|
116
|
+
z_log_var_flat: np.ndarray,
|
|
117
|
+
num_observations: int,
|
|
118
|
+
latent_dim: int,
|
|
119
|
+
active_dims: list[int] | None = None,
|
|
120
|
+
) -> np.ndarray:
|
|
121
|
+
"""
|
|
122
|
+
Assemble one variant's feature matrix from a LEAD block plus the log_var block.
|
|
123
|
+
|
|
124
|
+
`lead_flat` fills the first num_observations*latent_dim columns: z_mean for the
|
|
125
|
+
deterministic (pass-1 / evaluation) form, a sampled draw for MC scoring, or the sampled
|
|
126
|
+
z for the legacy "z" variant's training rows. The uncertainty extras always come from
|
|
127
|
+
the DETERMINISTIC z_log_var block. Layout is [lead | extras], obs-major within blocks —
|
|
128
|
+
the RF is permutation-invariant across features, so ordering only affects SHAP/feature
|
|
129
|
+
naming readability.
|
|
130
|
+
"""
|
|
131
|
+
lead = np.asarray(lead_flat, dtype=np.float32)
|
|
132
|
+
if variant in ("z", "z_mean", "z_aug"):
|
|
133
|
+
return lead
|
|
134
|
+
|
|
135
|
+
z_log_var = np.asarray(z_log_var_flat, dtype=np.float32)
|
|
136
|
+
if variant == "z_mean_logvar":
|
|
137
|
+
return np.hstack([lead, z_log_var])
|
|
138
|
+
if variant == "z_mean_total_kl":
|
|
139
|
+
kl = per_cadence_kl(lead, z_log_var)
|
|
140
|
+
return np.hstack([lead, kl[:, None]])
|
|
141
|
+
if variant == "z_mean_obs_logvar":
|
|
142
|
+
per_obs_mean = _reshape_blocks(z_log_var, num_observations, latent_dim).mean(axis=2)
|
|
143
|
+
return np.hstack([lead, per_obs_mean.astype(np.float32)])
|
|
144
|
+
if variant == "z_mean_dim_logvar":
|
|
145
|
+
per_dim_mean = _reshape_blocks(z_log_var, num_observations, latent_dim).mean(axis=1)
|
|
146
|
+
return np.hstack([lead, per_dim_mean.astype(np.float32)])
|
|
147
|
+
if variant == "z_mean_logvar_active":
|
|
148
|
+
columns = _active_logvar_columns(active_dims or [], num_observations, latent_dim)
|
|
149
|
+
if not columns:
|
|
150
|
+
# Every dim collapsed (or none measured active) — degenerate to plain z_mean
|
|
151
|
+
return lead
|
|
152
|
+
return np.hstack([lead, z_log_var[:, columns]])
|
|
153
|
+
raise ValueError(f"Unknown latent variant {variant!r}; expected one of {VARIANT_ORDER}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def variant_feature_names(
|
|
157
|
+
variant: str,
|
|
158
|
+
num_observations: int,
|
|
159
|
+
latent_dim: int,
|
|
160
|
+
active_dims: list[int] | None = None,
|
|
161
|
+
) -> list[str]:
|
|
162
|
+
"""
|
|
163
|
+
Human-readable column names mirroring build_variant_features' exact layout —
|
|
164
|
+
[lead | extras], obs-major within blocks. Even-indexed observations are ON, odd are OFF,
|
|
165
|
+
pairs numbered 1..3 (the data_generation cadence convention). MUST stay column-for-column
|
|
166
|
+
in lockstep with build_variant_features: the SHAP plots pair these names with the feature
|
|
167
|
+
matrix, and a length mismatch is an IndexError inside shap (a 54-feature
|
|
168
|
+
z_mean_obs_logvar winner against the old hardcoded 48-name list is exactly how this
|
|
169
|
+
function came to exist).
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def obs_label(o: int) -> str:
|
|
173
|
+
return f"{'ON' if o % 2 == 0 else 'OFF'}-{o // 2 + 1}"
|
|
174
|
+
|
|
175
|
+
lead = [f"{obs_label(o)}_dim-{d}" for o in range(num_observations) for d in range(latent_dim)]
|
|
176
|
+
if variant in ("z", "z_mean", "z_aug"):
|
|
177
|
+
return lead
|
|
178
|
+
if variant == "z_mean_logvar":
|
|
179
|
+
return lead + [
|
|
180
|
+
f"logvar_{obs_label(o)}_dim-{d}"
|
|
181
|
+
for o in range(num_observations)
|
|
182
|
+
for d in range(latent_dim)
|
|
183
|
+
]
|
|
184
|
+
if variant == "z_mean_total_kl":
|
|
185
|
+
return lead + ["total_kl"]
|
|
186
|
+
if variant == "z_mean_obs_logvar":
|
|
187
|
+
return lead + [f"logvar_mean_{obs_label(o)}" for o in range(num_observations)]
|
|
188
|
+
if variant == "z_mean_dim_logvar":
|
|
189
|
+
return lead + [f"logvar_mean_dim-{d}" for d in range(latent_dim)]
|
|
190
|
+
if variant == "z_mean_logvar_active":
|
|
191
|
+
columns = _active_logvar_columns(active_dims or [], num_observations, latent_dim)
|
|
192
|
+
return lead + [f"logvar_{obs_label(c // latent_dim)}_dim-{c % latent_dim}" for c in columns]
|
|
193
|
+
raise ValueError(f"Unknown latent variant {variant!r}; expected one of {VARIANT_ORDER}")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def sample_z_flat(
|
|
197
|
+
z_mean_flat: np.ndarray, z_log_var_flat: np.ndarray, rng: np.random.Generator
|
|
198
|
+
) -> np.ndarray:
|
|
199
|
+
"""One reparameterized draw z = mu + exp(lv/2) * eps on the flattened blocks (numpy —
|
|
200
|
+
seeded via aetherscan.seeding, independent of TF's RNG)."""
|
|
201
|
+
z_mean = np.asarray(z_mean_flat, dtype=np.float32)
|
|
202
|
+
z_log_var = np.asarray(z_log_var_flat, dtype=np.float32)
|
|
203
|
+
epsilon = rng.standard_normal(size=z_mean.shape).astype(np.float32)
|
|
204
|
+
return z_mean + np.exp(0.5 * z_log_var) * epsilon
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def build_z_aug_training_set(
|
|
208
|
+
z_mean_flat: np.ndarray,
|
|
209
|
+
z_log_var_flat: np.ndarray,
|
|
210
|
+
labels: np.ndarray,
|
|
211
|
+
draws: int,
|
|
212
|
+
rng: np.random.Generator,
|
|
213
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
214
|
+
"""The z_aug variant's training rows: the deterministic z_mean row plus `draws` sampled
|
|
215
|
+
rows per cadence (sampling-as-augmentation), labels replicated to match. Evaluation
|
|
216
|
+
stays on plain z_mean — that is the whole point of the variant."""
|
|
217
|
+
blocks = [np.asarray(z_mean_flat, dtype=np.float32)]
|
|
218
|
+
blocks += [sample_z_flat(z_mean_flat, z_log_var_flat, rng) for _ in range(draws)]
|
|
219
|
+
features = np.vstack(blocks)
|
|
220
|
+
stacked_labels = np.concatenate([np.asarray(labels)] * (draws + 1))
|
|
221
|
+
return features, stacked_labels
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ------------------------------------------------------------------ selection metrics
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def recall_at_fpr(labels: np.ndarray, scores: np.ndarray, max_fpr: float) -> float:
|
|
228
|
+
"""
|
|
229
|
+
Recall of the positive class at the score threshold whose false-positive rate is
|
|
230
|
+
`max_fpr`: the primary #282 selection metric (don't miss signals while keeping the
|
|
231
|
+
candidate list reviewable). Returns NaN for single-class labels.
|
|
232
|
+
"""
|
|
233
|
+
labels = np.asarray(labels).astype(bool)
|
|
234
|
+
scores = np.asarray(scores)
|
|
235
|
+
negatives = scores[~labels]
|
|
236
|
+
positives = scores[labels]
|
|
237
|
+
if len(negatives) == 0 or len(positives) == 0:
|
|
238
|
+
return float("nan")
|
|
239
|
+
threshold = np.quantile(negatives, 1.0 - max_fpr)
|
|
240
|
+
return float(np.mean(positives > threshold))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def expected_calibration_error(labels: np.ndarray, probas: np.ndarray, n_bins: int = 10) -> float:
|
|
244
|
+
"""ECE over quantile bins — the same binning strategy as plot_rf_calibration_curve, so
|
|
245
|
+
the gate and the plotted number agree."""
|
|
246
|
+
labels = np.asarray(labels, dtype=np.float64)
|
|
247
|
+
probas = np.asarray(probas, dtype=np.float64)
|
|
248
|
+
edges = np.quantile(probas, np.linspace(0.0, 1.0, n_bins + 1))
|
|
249
|
+
edges[0], edges[-1] = -np.inf, np.inf
|
|
250
|
+
ece = 0.0
|
|
251
|
+
for low, high in zip(edges[:-1], edges[1:], strict=False):
|
|
252
|
+
mask = (probas > low) & (probas <= high)
|
|
253
|
+
if not mask.any():
|
|
254
|
+
continue
|
|
255
|
+
ece += (mask.mean()) * abs(labels[mask].mean() - probas[mask].mean())
|
|
256
|
+
return float(ece)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def recall_margin_lower_bound(
|
|
260
|
+
labels: np.ndarray,
|
|
261
|
+
scores_best: np.ndarray,
|
|
262
|
+
scores_simpler: np.ndarray,
|
|
263
|
+
max_fpr: float,
|
|
264
|
+
rounds: int,
|
|
265
|
+
rng: np.random.Generator,
|
|
266
|
+
alpha: float = 0.05,
|
|
267
|
+
) -> float:
|
|
268
|
+
"""Lower bound of the bootstrap CI on recall@FPR(best) - recall@FPR(simpler). A bound
|
|
269
|
+
<= 0 means the best variant does not beat the simpler one by more than noise."""
|
|
270
|
+
labels = np.asarray(labels)
|
|
271
|
+
n = len(labels)
|
|
272
|
+
diffs = np.empty(rounds)
|
|
273
|
+
for i in range(rounds):
|
|
274
|
+
idx = rng.integers(0, n, size=n)
|
|
275
|
+
diffs[i] = recall_at_fpr(labels[idx], scores_best[idx], max_fpr) - recall_at_fpr(
|
|
276
|
+
labels[idx], scores_simpler[idx], max_fpr
|
|
277
|
+
)
|
|
278
|
+
return float(np.nanquantile(diffs, alpha))
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def select_winner(
|
|
282
|
+
labels: np.ndarray,
|
|
283
|
+
scores_by_variant: dict[str, np.ndarray],
|
|
284
|
+
max_fpr: float,
|
|
285
|
+
bootstrap_rounds: int,
|
|
286
|
+
rng: np.random.Generator,
|
|
287
|
+
) -> tuple[str, dict[str, float]]:
|
|
288
|
+
"""
|
|
289
|
+
Minimum-margin selection (#282): compute recall@FPR for every variant, find the best,
|
|
290
|
+
then walk VARIANT_ORDER (simple -> complex) and return the FIRST variant that is
|
|
291
|
+
statistically tied with the best (bootstrap lower bound of the best-minus-variant
|
|
292
|
+
margin <= 0). Only a variant the best beats beyond noise is passed over.
|
|
293
|
+
"""
|
|
294
|
+
recalls = {
|
|
295
|
+
name: recall_at_fpr(labels, scores, max_fpr) for name, scores in scores_by_variant.items()
|
|
296
|
+
}
|
|
297
|
+
ordered = [name for name in VARIANT_ORDER if name in scores_by_variant]
|
|
298
|
+
best = max(ordered, key=lambda name: (recalls[name], -ordered.index(name)))
|
|
299
|
+
for name in ordered:
|
|
300
|
+
if name == best:
|
|
301
|
+
break
|
|
302
|
+
margin_bound = recall_margin_lower_bound(
|
|
303
|
+
labels,
|
|
304
|
+
scores_by_variant[best],
|
|
305
|
+
scores_by_variant[name],
|
|
306
|
+
max_fpr,
|
|
307
|
+
bootstrap_rounds,
|
|
308
|
+
rng,
|
|
309
|
+
)
|
|
310
|
+
if margin_bound <= 0:
|
|
311
|
+
logger.info(
|
|
312
|
+
f"Variant selection: '{best}' (recall {recalls[best]:.4f}) does not beat "
|
|
313
|
+
f"simpler '{name}' (recall {recalls[name]:.4f}) beyond noise "
|
|
314
|
+
f"(bootstrap lower margin {margin_bound:+.4f}) — tie broken toward '{name}'"
|
|
315
|
+
)
|
|
316
|
+
return name, recalls
|
|
317
|
+
return best, recalls
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ------------------------------------------------------------------ probability calibration
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def fit_probability_calibrator(probas: np.ndarray, labels: np.ndarray, min_isotonic: int) -> dict:
|
|
324
|
+
"""
|
|
325
|
+
Fit a probability calibrator on HELD-OUT calibration rows (#282): isotonic when the set
|
|
326
|
+
is large enough (>= min_isotonic rows), else sigmoid/Platt (isotonic overfits small
|
|
327
|
+
sets). Returned dict {method, model} is joblib-persistable as rf_calibrator_{tag}.joblib
|
|
328
|
+
and applied identically at inference — an unapplied calibrator would be a silent
|
|
329
|
+
train/serve mismatch.
|
|
330
|
+
"""
|
|
331
|
+
probas = np.asarray(probas, dtype=np.float64)
|
|
332
|
+
labels = np.asarray(labels, dtype=np.int64)
|
|
333
|
+
if len(probas) >= min_isotonic:
|
|
334
|
+
model = IsotonicRegression(y_min=0.0, y_max=1.0, out_of_bounds="clip")
|
|
335
|
+
model.fit(probas, labels)
|
|
336
|
+
method = "isotonic"
|
|
337
|
+
else:
|
|
338
|
+
model = LogisticRegression(solver="lbfgs")
|
|
339
|
+
model.fit(probas.reshape(-1, 1), labels)
|
|
340
|
+
method = "sigmoid"
|
|
341
|
+
return {"method": method, "model": model}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def apply_probability_calibrator(calibrator: dict | None, probas: np.ndarray) -> np.ndarray:
|
|
345
|
+
"""Map raw RF probabilities through the calibrator (identity when None). Monotonic, so
|
|
346
|
+
rank metrics (AUC, recall@FPR) are unchanged — only probability VALUES move."""
|
|
347
|
+
if calibrator is None:
|
|
348
|
+
return np.asarray(probas)
|
|
349
|
+
probas = np.asarray(probas, dtype=np.float64)
|
|
350
|
+
if calibrator["method"] == "isotonic":
|
|
351
|
+
return calibrator["model"].predict(probas)
|
|
352
|
+
return calibrator["model"].predict_proba(probas.reshape(-1, 1))[:, 1]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logger package for Aetherscan pipeline
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from .logger import (
|
|
8
|
+
get_logger,
|
|
9
|
+
init_logger,
|
|
10
|
+
init_worker_logging,
|
|
11
|
+
shutdown_logger,
|
|
12
|
+
)
|
|
13
|
+
from .slack_handler import SlackHandler
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"get_logger",
|
|
17
|
+
"init_logger",
|
|
18
|
+
"init_worker_logging",
|
|
19
|
+
"shutdown_logger",
|
|
20
|
+
"SlackHandler",
|
|
21
|
+
]
|