transport-runtime 0.6.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.
- transport_runtime/__init__.py +85 -0
- transport_runtime/audit.py +210 -0
- transport_runtime/certificate_strategy.py +115 -0
- transport_runtime/certify.py +236 -0
- transport_runtime/channels.py +84 -0
- transport_runtime/cli.py +87 -0
- transport_runtime/clinical.py +123 -0
- transport_runtime/compiler.py +257 -0
- transport_runtime/core.py +256 -0
- transport_runtime/diff.py +72 -0
- transport_runtime/fetch_data.py +105 -0
- transport_runtime/identify.py +346 -0
- transport_runtime/ot_gap.py +129 -0
- transport_runtime/pipeline.py +114 -0
- transport_runtime/robust.py +223 -0
- transport_runtime/selection.py +186 -0
- transport_runtime/slope.py +403 -0
- transport_runtime/theory.py +122 -0
- transport_runtime/verify.py +76 -0
- transport_runtime-0.6.0.dist-info/METADATA +109 -0
- transport_runtime-0.6.0.dist-info/RECORD +25 -0
- transport_runtime-0.6.0.dist-info/WHEEL +5 -0
- transport_runtime-0.6.0.dist-info/entry_points.txt +3 -0
- transport_runtime-0.6.0.dist-info/licenses/LICENSE +25 -0
- transport_runtime-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Transport Runtime: zero-label deployment for any fitted classifier."""
|
|
2
|
+
from .core import TransportLayer, DeployedModel, DeployedConformal, moment_delta
|
|
3
|
+
from .ot_gap import (cdf_l1_gap, label_free_gap, aux_minmax, aux_uniform,
|
|
4
|
+
aux_model, total_coverage_gap)
|
|
5
|
+
from .channels import discover_score_channels, quantile_channel
|
|
6
|
+
from .clinical import channel_clinical_audit, clinical_threshold_audit, compare_decision_curves, decision_curve
|
|
7
|
+
from .selection import select_transport_channel, select_transport_channel_stability
|
|
8
|
+
from .slope import (
|
|
9
|
+
allocate_labels_for_slope,
|
|
10
|
+
apply_channel_affine,
|
|
11
|
+
channel_affine_repair,
|
|
12
|
+
channel_affine_margin_interval,
|
|
13
|
+
bootstrap_channel_affine,
|
|
14
|
+
ece_from_p,
|
|
15
|
+
evaluate_affine_repair,
|
|
16
|
+
fit_channel_affine,
|
|
17
|
+
margins_from_model,
|
|
18
|
+
slope_threshold_sensitivity,
|
|
19
|
+
summarize_slope_risk,
|
|
20
|
+
within_channel_score_shift,
|
|
21
|
+
)
|
|
22
|
+
from .certify import CertificateRecommendation, evaluate_certificate, recommend_certificate, recommend_certificate_from_audit_pack, summarize_channel_clinical_risk, summarize_slope_uncertainty
|
|
23
|
+
from .audit import audit_pack, save_audit_pack
|
|
24
|
+
from .diff import contract_diff
|
|
25
|
+
from .verify import CheckResult, VerificationReport, verify_contract
|
|
26
|
+
from .compiler import (
|
|
27
|
+
CertificateSpec,
|
|
28
|
+
ChannelSpec,
|
|
29
|
+
DeploymentContract,
|
|
30
|
+
PrevalenceSpec,
|
|
31
|
+
TransportCompiler,
|
|
32
|
+
)
|
|
33
|
+
from .robust import (
|
|
34
|
+
PrevalenceIntervalSet,
|
|
35
|
+
choose_repair,
|
|
36
|
+
deploy_eb,
|
|
37
|
+
deploy_interval,
|
|
38
|
+
empirical_bayes_target,
|
|
39
|
+
estimate_repair_gain,
|
|
40
|
+
interval_regret_bound,
|
|
41
|
+
interval_uniform_variance,
|
|
42
|
+
minimax_target,
|
|
43
|
+
worst_case_channel_bias,
|
|
44
|
+
)
|
|
45
|
+
from .identify import (
|
|
46
|
+
IdentificationResult,
|
|
47
|
+
STATUS_INCONCLUSIVE,
|
|
48
|
+
STATUS_NOT_TRANSPORTABLE_BY_ADJUSTMENT,
|
|
49
|
+
STATUS_TRANSPORTABLE,
|
|
50
|
+
ancestors,
|
|
51
|
+
d_connected,
|
|
52
|
+
descendants,
|
|
53
|
+
intervened_graph,
|
|
54
|
+
is_dag,
|
|
55
|
+
s_admissible,
|
|
56
|
+
transportability_id,
|
|
57
|
+
valid_backdoor_set,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
'TransportLayer', 'DeployedModel', 'DeployedConformal', 'moment_delta',
|
|
62
|
+
'ChannelSpec', 'PrevalenceSpec', 'CertificateSpec', 'TransportCompiler', 'DeploymentContract',
|
|
63
|
+
'quantile_channel', 'discover_score_channels', 'select_transport_channel', 'select_transport_channel_stability',
|
|
64
|
+
'decision_curve', 'compare_decision_curves', 'clinical_threshold_audit', 'channel_clinical_audit',
|
|
65
|
+
'margins_from_model', 'within_channel_score_shift', 'summarize_slope_risk', 'slope_threshold_sensitivity',
|
|
66
|
+
'fit_channel_affine', 'apply_channel_affine', 'channel_affine_repair',
|
|
67
|
+
'bootstrap_channel_affine', 'channel_affine_margin_interval',
|
|
68
|
+
'allocate_labels_for_slope',
|
|
69
|
+
'ece_from_p', 'evaluate_affine_repair',
|
|
70
|
+
'verify_contract', 'VerificationReport', 'CheckResult',
|
|
71
|
+
'recommend_certificate', 'recommend_certificate_from_audit_pack', 'CertificateRecommendation', 'evaluate_certificate', 'summarize_slope_uncertainty', 'summarize_channel_clinical_risk',
|
|
72
|
+
'audit_pack', 'save_audit_pack', 'contract_diff',
|
|
73
|
+
'cdf_l1_gap', 'label_free_gap', 'aux_minmax', 'aux_uniform',
|
|
74
|
+
'aux_model', 'total_coverage_gap',
|
|
75
|
+
'PrevalenceIntervalSet', 'deploy_interval', 'interval_regret_bound',
|
|
76
|
+
'deploy_eb', 'empirical_bayes_target', 'estimate_repair_gain', 'choose_repair', 'interval_uniform_variance',
|
|
77
|
+
'minimax_target', 'worst_case_channel_bias',
|
|
78
|
+
'IdentificationResult', 'transportability_id', 'valid_backdoor_set',
|
|
79
|
+
's_admissible', 'd_connected', 'intervened_graph', 'is_dag',
|
|
80
|
+
'ancestors', 'descendants', 'STATUS_TRANSPORTABLE', 'STATUS_INCONCLUSIVE',
|
|
81
|
+
'STATUS_NOT_TRANSPORTABLE_BY_ADJUSTMENT',
|
|
82
|
+
]
|
|
83
|
+
__version__ = '0.6.0'
|
|
84
|
+
|
|
85
|
+
from transport_runtime.pipeline import TransportPipeline # noqa: E402,F401
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Standardized audit pack for deployment contracts.
|
|
2
|
+
|
|
3
|
+
An audit pack is the single JSON artifact that combines:
|
|
4
|
+
- contract manifest;
|
|
5
|
+
- slope-risk summary;
|
|
6
|
+
- contract verification;
|
|
7
|
+
- labeled-target certificate replay, when labels are available.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from scipy.special import expit
|
|
15
|
+
|
|
16
|
+
from .certify import evaluate_certificate
|
|
17
|
+
from .clinical import clinical_threshold_audit, decision_curve
|
|
18
|
+
from .compiler import DeploymentContract
|
|
19
|
+
from .slope import bootstrap_channel_affine, channel_affine_margin_interval, margins_from_model
|
|
20
|
+
from .verify import verify_contract
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _tolist(value):
|
|
24
|
+
return np.asarray(value, dtype=float).tolist()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _slope_repair_certificate(contract, X, channels, y, boot_kwargs):
|
|
28
|
+
levels = getattr(contract.deployed, "levels_", None)
|
|
29
|
+
if levels is None:
|
|
30
|
+
return None
|
|
31
|
+
f_repaired = margins_from_model(contract.layer.model, X) + np.asarray(levels, dtype=float)[np.asarray(channels, dtype=int)]
|
|
32
|
+
boot = bootstrap_channel_affine(f_repaired, y, channels, **boot_kwargs)
|
|
33
|
+
interval = channel_affine_margin_interval(f_repaired, channels, boot)
|
|
34
|
+
width = interval["upper"] - interval["lower"]
|
|
35
|
+
return {
|
|
36
|
+
"note": "finite-label uncertainty for affine slope repair fitted after level repair; "
|
|
37
|
+
"this is fitting uncertainty, not proof of target calibration",
|
|
38
|
+
"alpha": float(boot["alpha"]),
|
|
39
|
+
"ci_level": float(boot["ci_level"]),
|
|
40
|
+
"n_boot": int(boot["n_boot"]),
|
|
41
|
+
"channel_n": _tolist(boot["channel_n"]),
|
|
42
|
+
"a": _tolist(boot["a"]),
|
|
43
|
+
"b": _tolist(boot["b"]),
|
|
44
|
+
"a_mean": _tolist(boot["a_mean"]),
|
|
45
|
+
"b_mean": _tolist(boot["b_mean"]),
|
|
46
|
+
"a_std": _tolist(boot["a_std"]),
|
|
47
|
+
"b_std": _tolist(boot["b_std"]),
|
|
48
|
+
"a_ci_lower": _tolist(boot["a_ci_lower"]),
|
|
49
|
+
"a_ci_upper": _tolist(boot["a_ci_upper"]),
|
|
50
|
+
"b_ci_lower": _tolist(boot["b_ci_lower"]),
|
|
51
|
+
"b_ci_upper": _tolist(boot["b_ci_upper"]),
|
|
52
|
+
"bootstrap_success_rate": _tolist(boot["bootstrap_success_rate"]),
|
|
53
|
+
"margin_interval_summary": {
|
|
54
|
+
"n": int(len(width)),
|
|
55
|
+
"mean_width": float(np.mean(width)),
|
|
56
|
+
"median_width": float(np.median(width)),
|
|
57
|
+
"q95_width": float(np.quantile(width, 0.95)),
|
|
58
|
+
"max_width": float(np.max(width)),
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _clinical_threshold_audit(contract, X, channels, y, thresholds):
|
|
64
|
+
base_p = expit(margins_from_model(contract.layer.model, X))
|
|
65
|
+
repaired_p = contract.predict_proba(X, channels)
|
|
66
|
+
audit = clinical_threshold_audit(y, base_p, repaired_p, thresholds=thresholds)
|
|
67
|
+
return {
|
|
68
|
+
"note": "threshold-specific net-benefit audit comparing repaired contract output with unrepaired model output; "
|
|
69
|
+
"synthetic or labeled-target audit evidence does not by itself prove clinical effectiveness",
|
|
70
|
+
"thresholds": audit["thresholds"],
|
|
71
|
+
"base_net_benefit": audit["base_net_benefit"],
|
|
72
|
+
"repaired_net_benefit": audit["repaired_net_benefit"],
|
|
73
|
+
"repaired_minus_base": audit["repaired_minus_base"],
|
|
74
|
+
"summary": audit["summary"],
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _channel_clinical_summary(contract, X, channels, y, thresholds):
|
|
79
|
+
thresholds = np.asarray(
|
|
80
|
+
thresholds if thresholds is not None else np.linspace(0.01, 0.99, 99),
|
|
81
|
+
dtype=float,
|
|
82
|
+
)
|
|
83
|
+
base_p = expit(margins_from_model(contract.layer.model, X))
|
|
84
|
+
repaired_p = contract.predict_proba(X, channels)
|
|
85
|
+
y = np.asarray(y, dtype=float)
|
|
86
|
+
channels = np.asarray(channels, dtype=int)
|
|
87
|
+
summaries = []
|
|
88
|
+
for g in np.unique(channels):
|
|
89
|
+
sel = channels == g
|
|
90
|
+
base = decision_curve(y[sel], base_p[sel], thresholds=thresholds)
|
|
91
|
+
repaired = decision_curve(y[sel], repaired_p[sel], thresholds=thresholds)
|
|
92
|
+
gain = np.asarray(repaired["net_benefit"], dtype=float) - np.asarray(base["net_benefit"], dtype=float)
|
|
93
|
+
j = int(np.argmax(gain))
|
|
94
|
+
summaries.append(
|
|
95
|
+
{
|
|
96
|
+
"channel": int(g),
|
|
97
|
+
"channel_n": int(sel.sum()),
|
|
98
|
+
"channel_prevalence": float(np.mean(y[sel])),
|
|
99
|
+
"integrated_gain": float(np.trapz(gain, thresholds)),
|
|
100
|
+
"max_gain": float(gain[j]),
|
|
101
|
+
"threshold_at_max_gain": float(thresholds[j]),
|
|
102
|
+
"worst_gain": float(np.min(gain)),
|
|
103
|
+
"fraction_improved": float(np.mean(gain > 0.0)),
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
integrated = np.array([row["integrated_gain"] for row in summaries], dtype=float)
|
|
107
|
+
fragile = summaries[int(np.argmin(integrated))]
|
|
108
|
+
return {
|
|
109
|
+
"note": "channel-level threshold-specific net-benefit summary; negative channel gains can be masked by aggregate averages",
|
|
110
|
+
"thresholds": thresholds.tolist(),
|
|
111
|
+
"channel_summaries": summaries,
|
|
112
|
+
"overall": {
|
|
113
|
+
"n_channels": int(len(summaries)),
|
|
114
|
+
"mean_integrated_gain": float(np.mean(integrated)),
|
|
115
|
+
"min_integrated_gain": float(np.min(integrated)),
|
|
116
|
+
"n_channels_negative_integrated_gain": int(np.sum(integrated < 0.0)),
|
|
117
|
+
"most_fragile_channel": fragile["channel"],
|
|
118
|
+
"most_fragile_integrated_gain": fragile["integrated_gain"],
|
|
119
|
+
"most_fragile_worst_gain": fragile["worst_gain"],
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def audit_pack(
|
|
125
|
+
contract: DeploymentContract,
|
|
126
|
+
X=None,
|
|
127
|
+
channels=None,
|
|
128
|
+
y=None,
|
|
129
|
+
slope_boot=False,
|
|
130
|
+
slope_boot_kwargs=None,
|
|
131
|
+
clinical=False,
|
|
132
|
+
clinical_thresholds=None,
|
|
133
|
+
channel_clinical=False,
|
|
134
|
+
channel_clinical_thresholds=None,
|
|
135
|
+
) -> dict:
|
|
136
|
+
"""Build a standardized audit pack for a deployment contract.
|
|
137
|
+
|
|
138
|
+
Set `slope_boot=True` to add a finite-label affine slope-repair uncertainty
|
|
139
|
+
certificate when labeled target data are supplied. The option is disabled by
|
|
140
|
+
default to keep routine audit packs lightweight.
|
|
141
|
+
"""
|
|
142
|
+
pack = contract.manifest()
|
|
143
|
+
pack["slope_risk"] = contract.slope_risk
|
|
144
|
+
|
|
145
|
+
if X is not None and channels is not None:
|
|
146
|
+
pack["verification"] = verify_contract(contract, X=X, channels=channels).to_dict()
|
|
147
|
+
else:
|
|
148
|
+
pack["verification"] = None
|
|
149
|
+
|
|
150
|
+
if contract.certificate is not None and X is not None and y is not None and channels is not None:
|
|
151
|
+
pack["certificate_evaluation"] = evaluate_certificate(contract, X, y, channels)
|
|
152
|
+
else:
|
|
153
|
+
pack["certificate_evaluation"] = None
|
|
154
|
+
|
|
155
|
+
if slope_boot:
|
|
156
|
+
if X is None or y is None or channels is None:
|
|
157
|
+
raise ValueError("slope_boot=True requires X, channels, and y")
|
|
158
|
+
kwargs = dict(slope_boot_kwargs or {})
|
|
159
|
+
pack["slope_repair_certificate"] = _slope_repair_certificate(contract, X, channels, y, kwargs)
|
|
160
|
+
else:
|
|
161
|
+
pack["slope_repair_certificate"] = None
|
|
162
|
+
|
|
163
|
+
if clinical:
|
|
164
|
+
if X is None or y is None or channels is None:
|
|
165
|
+
raise ValueError("clinical=True requires X, channels, and y")
|
|
166
|
+
pack["clinical_threshold_audit"] = _clinical_threshold_audit(contract, X, channels, y, clinical_thresholds)
|
|
167
|
+
else:
|
|
168
|
+
pack["clinical_threshold_audit"] = None
|
|
169
|
+
|
|
170
|
+
if channel_clinical:
|
|
171
|
+
if X is None or y is None or channels is None:
|
|
172
|
+
raise ValueError("channel_clinical=True requires X, channels, and y")
|
|
173
|
+
pack["channel_clinical_summary"] = _channel_clinical_summary(
|
|
174
|
+
contract, X, channels, y, channel_clinical_thresholds
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
pack["channel_clinical_summary"] = None
|
|
178
|
+
|
|
179
|
+
return pack
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def save_audit_pack(
|
|
183
|
+
contract: DeploymentContract,
|
|
184
|
+
path,
|
|
185
|
+
X=None,
|
|
186
|
+
channels=None,
|
|
187
|
+
y=None,
|
|
188
|
+
slope_boot=False,
|
|
189
|
+
slope_boot_kwargs=None,
|
|
190
|
+
clinical=False,
|
|
191
|
+
clinical_thresholds=None,
|
|
192
|
+
channel_clinical=False,
|
|
193
|
+
channel_clinical_thresholds=None,
|
|
194
|
+
) -> dict:
|
|
195
|
+
"""Save an audit pack JSON and return it."""
|
|
196
|
+
pack = audit_pack(
|
|
197
|
+
contract,
|
|
198
|
+
X=X,
|
|
199
|
+
channels=channels,
|
|
200
|
+
y=y,
|
|
201
|
+
slope_boot=slope_boot,
|
|
202
|
+
slope_boot_kwargs=slope_boot_kwargs,
|
|
203
|
+
clinical=clinical,
|
|
204
|
+
clinical_thresholds=clinical_thresholds,
|
|
205
|
+
channel_clinical=channel_clinical,
|
|
206
|
+
channel_clinical_thresholds=channel_clinical_thresholds,
|
|
207
|
+
)
|
|
208
|
+
with open(str(path), "w", encoding="utf-8") as f:
|
|
209
|
+
json.dump(pack, f, indent=2, sort_keys=True)
|
|
210
|
+
return pack
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Three-tier certificate strategy (B2 closure, v0.6.84).
|
|
2
|
+
|
|
3
|
+
Empirical basis: docs/theory_impossibility.md rounds 66-68 and
|
|
4
|
+
scripts/ncs_conformal_transport_v01.py - under transport, no single conformal
|
|
5
|
+
certificate is universally safe (plain split-conformal collapses
|
|
6
|
+
conditionally; Mondrian trades marginal for conditional coverage; label-free
|
|
7
|
+
regime features only partially separate regimes). This module operationalises
|
|
8
|
+
the resulting three-tier architecture:
|
|
9
|
+
|
|
10
|
+
Tier 1 - label_free_default: conservative certificate choice from deployment
|
|
11
|
+
quantities only (target logit-spread ratio + public prevalence).
|
|
12
|
+
Always deployable; matches 2/4 regimes in validation (the two
|
|
13
|
+
extremes), conservative elsewhere.
|
|
14
|
+
Tier 2 - label_pool_audit: definitive regime classification + certificate
|
|
15
|
+
selection from a small label pool (n~50-100, the runtime's existing
|
|
16
|
+
DCA protocol). Estimates class-conditional coverage of BOTH
|
|
17
|
+
candidate certificates on the pool.
|
|
18
|
+
Tier 3 - audit output: regardless of tier, both certificates' marginal and
|
|
19
|
+
conditional coverage are emitted - 'ship the audit that chooses',
|
|
20
|
+
not a bare certificate.
|
|
21
|
+
|
|
22
|
+
Reproduce the validation: PYTHONPATH=. python scripts/ncs_conformal_transport_v01.py
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def logit(p):
|
|
30
|
+
p = np.clip(np.asarray(p, dtype=float), 1e-6, 1 - 1e-6)
|
|
31
|
+
return np.log(p / (1 - p))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def spread_ratio(p_source_calibrated, p_target_naive):
|
|
35
|
+
"""logit-spread ratio (label-free regime feature; see module docstring for
|
|
36
|
+
the documented limitation - binomial variance depends on the mean, so this
|
|
37
|
+
does NOT fully separate regimes)."""
|
|
38
|
+
ls, lt = logit(p_source_calibrated), logit(p_target_naive)
|
|
39
|
+
return float(np.std(lt) / max(np.std(ls), 1e-9))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def label_free_default(ratio, pi_public, tau=0.10, pi_lo=0.05, pi_hi=0.50):
|
|
43
|
+
"""Tier 1: conservative label-free certificate choice.
|
|
44
|
+
|
|
45
|
+
Returns 'mondrian+repair' for extreme-prevalence or spread-deviation
|
|
46
|
+
regimes, else 'plain+repair'."""
|
|
47
|
+
if pi_public < pi_lo or pi_public > pi_hi:
|
|
48
|
+
return "mondrian+repair"
|
|
49
|
+
if abs(ratio - 1.0) > tau:
|
|
50
|
+
return "mondrian+repair"
|
|
51
|
+
return "plain+repair"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _class_coverage(p, y, c, q):
|
|
55
|
+
p = np.asarray(p, dtype=float)
|
|
56
|
+
y = np.asarray(y, dtype=int)
|
|
57
|
+
m = y == c
|
|
58
|
+
if m.sum() == 0:
|
|
59
|
+
return np.nan
|
|
60
|
+
return float(np.mean(p[m] >= q))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def label_pool_audit(p_pool, y_pool, p_deploy_pool, q_plain, qs_mondrian,
|
|
64
|
+
pi_public, tau=0.10, pi_lo=0.05, pi_hi=0.50):
|
|
65
|
+
"""Tier 2: definitive selection from a small label pool.
|
|
66
|
+
|
|
67
|
+
Estimates class-conditional coverage of both candidate certificates on the
|
|
68
|
+
label pool and selects the one with the better worst-class coverage
|
|
69
|
+
(maximin). Falls back to the label-free default when the pool has too few
|
|
70
|
+
positives to estimate conditional coverage.
|
|
71
|
+
"""
|
|
72
|
+
p_pool = np.asarray(p_pool, dtype=float)
|
|
73
|
+
y_pool = np.asarray(y_pool, dtype=int)
|
|
74
|
+
ratio = spread_ratio(p_deploy_pool, p_pool)
|
|
75
|
+
default = label_free_default(ratio, pi_public, tau, pi_lo, pi_hi)
|
|
76
|
+
n_pos = int((y_pool == 1).sum())
|
|
77
|
+
if n_pos < 10:
|
|
78
|
+
return {"certificate": default, "tier": 1, "reason": "insufficient pool positives",
|
|
79
|
+
"n_pool": len(y_pool), "n_pos": n_pos}
|
|
80
|
+
|
|
81
|
+
cov_p = {"overall": float(np.mean(p_pool >= 1 - q_plain)),
|
|
82
|
+
"y1": _class_coverage(p_pool, y_pool, 1, 1 - q_plain),
|
|
83
|
+
"y0": _class_coverage(p_pool, y_pool, 0, q_plain)}
|
|
84
|
+
# Mondrian on 2-class with deploy probabilities: case threshold from qs[1],
|
|
85
|
+
# control threshold from qs[0]; coverage uses the appropriate threshold.
|
|
86
|
+
cov_m = {"overall": float(np.mean(
|
|
87
|
+
(p_pool >= 1 - qs_mondrian[1]) | (1 - p_pool >= 1 - qs_mondrian[0]))),
|
|
88
|
+
"y1": _class_coverage(p_pool, y_pool, 1, 1 - qs_mondrian[1]),
|
|
89
|
+
"y0": _class_coverage(p_pool, y_pool, 0, 1 - qs_mondrian[0])}
|
|
90
|
+
worst = {"plain+repair": min(cov_p["y0"], cov_p["y1"]),
|
|
91
|
+
"mondrian+repair": min(cov_m["y0"], cov_m["y1"])}
|
|
92
|
+
chosen = max(worst, key=worst.get)
|
|
93
|
+
return {"certificate": chosen, "tier": 2, "reason": "maximin on label pool",
|
|
94
|
+
"n_pool": len(y_pool), "n_pos": n_pos,
|
|
95
|
+
"coverage_plain": cov_p, "coverage_mondrian": cov_m,
|
|
96
|
+
"worst_class_coverage": worst}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def three_tier_strategy(p_source_calibrated, p_target_naive, pi_public,
|
|
100
|
+
p_pool=None, y_pool=None, q_plain=None, qs_mondrian=None):
|
|
101
|
+
"""Tier 3 orchestrator: emits the audit that chooses.
|
|
102
|
+
|
|
103
|
+
Returns a dict with the selected certificate, the tier used, the label-free
|
|
104
|
+
default (always reported), and - when a pool is provided - both
|
|
105
|
+
certificates' conditional coverage estimates."""
|
|
106
|
+
ratio = spread_ratio(p_source_calibrated, p_target_naive)
|
|
107
|
+
default = label_free_default(ratio, pi_public)
|
|
108
|
+
out = {"spread_ratio": ratio, "label_free_default": default,
|
|
109
|
+
"tier": 1, "certificate": default}
|
|
110
|
+
if p_pool is not None and y_pool is not None:
|
|
111
|
+
audit = label_pool_audit(p_pool, y_pool, p_target_naive[:len(p_pool)],
|
|
112
|
+
q_plain, qs_mondrian, pi_public)
|
|
113
|
+
out.update(audit)
|
|
114
|
+
out["audit"] = {k: v for k, v in audit.items() if k.startswith("coverage")}
|
|
115
|
+
return out
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Certificate recommendation for Transport Runtime deployment contracts.
|
|
2
|
+
|
|
3
|
+
Innovation layer: a certificate is not always equally appropriate. Small channels,
|
|
4
|
+
post-repair slope-risk flags, and high public-prevalence uncertainty should change
|
|
5
|
+
the certificate policy.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from .compiler import CertificateSpec, DeploymentContract, _certificate_to_dict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class CertificateRecommendation:
|
|
18
|
+
spec: CertificateSpec
|
|
19
|
+
reasons: tuple[str, ...]
|
|
20
|
+
|
|
21
|
+
def to_dict(self) -> dict:
|
|
22
|
+
return {
|
|
23
|
+
"spec": {
|
|
24
|
+
"alpha": self.spec.alpha,
|
|
25
|
+
"per_channel": self.spec.per_channel,
|
|
26
|
+
"weighted": self.spec.weighted,
|
|
27
|
+
},
|
|
28
|
+
"reasons": list(self.reasons),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def summarize_slope_uncertainty(slope_uncertainty) -> dict | None:
|
|
33
|
+
"""Summarize optional slope-repair uncertainty for certificate policy.
|
|
34
|
+
|
|
35
|
+
Accepts either a scalar width or an audit-pack `slope_repair_certificate`
|
|
36
|
+
dictionary containing `a_ci_lower`, `a_ci_upper`, `b_ci_lower`, and
|
|
37
|
+
`b_ci_upper`. Returns `None` when no uncertainty evidence is supplied.
|
|
38
|
+
"""
|
|
39
|
+
if slope_uncertainty is None:
|
|
40
|
+
return None
|
|
41
|
+
if isinstance(slope_uncertainty, dict):
|
|
42
|
+
required = ("a_ci_lower", "a_ci_upper", "b_ci_lower", "b_ci_upper")
|
|
43
|
+
if all(k in slope_uncertainty for k in required):
|
|
44
|
+
a_width = np.asarray(slope_uncertainty["a_ci_upper"], dtype=float) - np.asarray(slope_uncertainty["a_ci_lower"], dtype=float)
|
|
45
|
+
b_width = np.asarray(slope_uncertainty["b_ci_upper"], dtype=float) - np.asarray(slope_uncertainty["b_ci_lower"], dtype=float)
|
|
46
|
+
width = a_width + b_width
|
|
47
|
+
if np.any(~np.isfinite(width)):
|
|
48
|
+
raise ValueError("slope uncertainty intervals must be finite")
|
|
49
|
+
return {
|
|
50
|
+
"max_width": float(np.max(width)),
|
|
51
|
+
"mean_width": float(np.mean(width)),
|
|
52
|
+
"n_channels": int(len(width)),
|
|
53
|
+
"source": "slope_repair_certificate",
|
|
54
|
+
}
|
|
55
|
+
if "margin_interval_summary" in slope_uncertainty:
|
|
56
|
+
q95 = float(slope_uncertainty["margin_interval_summary"].get("q95_width", np.nan))
|
|
57
|
+
if not np.isfinite(q95):
|
|
58
|
+
raise ValueError("margin interval summary must contain a finite q95_width")
|
|
59
|
+
return {
|
|
60
|
+
"max_width": q95,
|
|
61
|
+
"mean_width": q95,
|
|
62
|
+
"n_channels": len(np.asarray(slope_uncertainty.get("channel_n", [0]))),
|
|
63
|
+
"source": "margin_interval_summary",
|
|
64
|
+
}
|
|
65
|
+
raise ValueError("unsupported slope_uncertainty dictionary")
|
|
66
|
+
width = float(slope_uncertainty)
|
|
67
|
+
if not np.isfinite(width):
|
|
68
|
+
raise ValueError("slope uncertainty width must be finite")
|
|
69
|
+
return {"max_width": width, "mean_width": width, "n_channels": None, "source": "scalar"}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def summarize_channel_clinical_risk(channel_clinical, threshold: float = 0.0) -> dict | None:
|
|
73
|
+
"""Summarize channel-level clinical evidence for certificate policy."""
|
|
74
|
+
if channel_clinical is None:
|
|
75
|
+
return None
|
|
76
|
+
if not isinstance(channel_clinical, dict) or "overall" not in channel_clinical:
|
|
77
|
+
raise ValueError("channel_clinical must be an audit-pack channel_clinical_summary dictionary")
|
|
78
|
+
overall = channel_clinical["overall"]
|
|
79
|
+
return {
|
|
80
|
+
"n_channels": int(overall.get("n_channels", 0)),
|
|
81
|
+
"n_negative_channels": int(overall.get("n_channels_negative_integrated_gain", 0)),
|
|
82
|
+
"most_fragile_channel": int(overall.get("most_fragile_channel", -1)),
|
|
83
|
+
"most_fragile_integrated_gain": float(overall.get("most_fragile_integrated_gain", np.nan)),
|
|
84
|
+
"most_fragile_worst_gain": float(overall.get("most_fragile_worst_gain", np.nan)),
|
|
85
|
+
"threshold": float(threshold),
|
|
86
|
+
"source": "channel_clinical_summary",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def recommend_certificate(
|
|
91
|
+
channel_counts,
|
|
92
|
+
slope_risk: dict | None = None,
|
|
93
|
+
prevalence_variance=None,
|
|
94
|
+
slope_uncertainty=None,
|
|
95
|
+
channel_clinical=None,
|
|
96
|
+
alpha: float = 0.1,
|
|
97
|
+
min_channel_n: int = 200,
|
|
98
|
+
high_variance_threshold: float = 0.02**2,
|
|
99
|
+
slope_uncertainty_threshold: float = 0.20,
|
|
100
|
+
channel_clinical_threshold: float = 0.0,
|
|
101
|
+
) -> CertificateRecommendation:
|
|
102
|
+
"""Recommend a certificate policy from deployment risk features.
|
|
103
|
+
|
|
104
|
+
Rules
|
|
105
|
+
-----
|
|
106
|
+
- small channels, slope-risk flags, high finite-label slope uncertainty, or
|
|
107
|
+
channel-level clinical losses -> weighted per-channel certificate;
|
|
108
|
+
- otherwise high prevalence uncertainty -> weighted marginal certificate;
|
|
109
|
+
- otherwise -> plain split-conformal certificate.
|
|
110
|
+
"""
|
|
111
|
+
counts = np.asarray(channel_counts, dtype=float)
|
|
112
|
+
if counts.ndim != 1 or len(counts) == 0:
|
|
113
|
+
raise ValueError("channel_counts must be a non-empty vector")
|
|
114
|
+
reasons: list[str] = []
|
|
115
|
+
|
|
116
|
+
small = bool(np.any(counts < min_channel_n))
|
|
117
|
+
if small:
|
|
118
|
+
reasons.append(f"at least one channel has n < {min_channel_n}")
|
|
119
|
+
|
|
120
|
+
flagged = False
|
|
121
|
+
if slope_risk is not None:
|
|
122
|
+
flagged = int(slope_risk.get("n_flagged", 0)) > 0
|
|
123
|
+
if flagged:
|
|
124
|
+
reasons.append(f"slope-risk flagged channels: {slope_risk.get('flagged_channels', [])}")
|
|
125
|
+
|
|
126
|
+
slope_summary = summarize_slope_uncertainty(slope_uncertainty)
|
|
127
|
+
high_slope_uncertainty = False
|
|
128
|
+
if slope_summary is not None:
|
|
129
|
+
high_slope_uncertainty = bool(slope_summary["max_width"] > slope_uncertainty_threshold)
|
|
130
|
+
if high_slope_uncertainty:
|
|
131
|
+
reasons.append(
|
|
132
|
+
"high finite-label slope uncertainty: "
|
|
133
|
+
f"max width {slope_summary['max_width']:.4f} > {slope_uncertainty_threshold:.4f}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
channel_summary = summarize_channel_clinical_risk(channel_clinical, threshold=channel_clinical_threshold)
|
|
137
|
+
high_channel_clinical_risk = False
|
|
138
|
+
if channel_summary is not None:
|
|
139
|
+
has_negative_channel = channel_summary["n_negative_channels"] > 0
|
|
140
|
+
fragile_below_threshold = (
|
|
141
|
+
np.isfinite(channel_summary["most_fragile_integrated_gain"])
|
|
142
|
+
and channel_summary["most_fragile_integrated_gain"] < channel_clinical_threshold
|
|
143
|
+
)
|
|
144
|
+
high_channel_clinical_risk = bool(has_negative_channel or fragile_below_threshold)
|
|
145
|
+
if high_channel_clinical_risk:
|
|
146
|
+
reasons.append(
|
|
147
|
+
"channel-level clinical loss: "
|
|
148
|
+
f"negative channels={channel_summary['n_negative_channels']}, "
|
|
149
|
+
f"most fragile ch{channel_summary['most_fragile_channel']} "
|
|
150
|
+
f"integrated gain={channel_summary['most_fragile_integrated_gain']:.4f}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
high_var = False
|
|
154
|
+
if prevalence_variance is not None:
|
|
155
|
+
var = np.asarray(prevalence_variance, dtype=float)
|
|
156
|
+
high_var = bool(np.max(var) > high_variance_threshold)
|
|
157
|
+
if high_var:
|
|
158
|
+
reasons.append("high public-prevalence uncertainty")
|
|
159
|
+
|
|
160
|
+
if small or flagged or high_slope_uncertainty or high_channel_clinical_risk:
|
|
161
|
+
return CertificateRecommendation(
|
|
162
|
+
spec=CertificateSpec(alpha=alpha, per_channel=True, weighted=True),
|
|
163
|
+
reasons=tuple(reasons) or ("small channels or slope-risk flags",),
|
|
164
|
+
)
|
|
165
|
+
if high_var:
|
|
166
|
+
return CertificateRecommendation(
|
|
167
|
+
spec=CertificateSpec(alpha=alpha, per_channel=False, weighted=True),
|
|
168
|
+
reasons=tuple(reasons),
|
|
169
|
+
)
|
|
170
|
+
return CertificateRecommendation(
|
|
171
|
+
spec=CertificateSpec(alpha=alpha, per_channel=False, weighted=False),
|
|
172
|
+
reasons=tuple(reasons) or ("large channels, no slope-risk flags, low prevalence uncertainty",),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def recommend_certificate_from_audit_pack(
|
|
177
|
+
pack: dict,
|
|
178
|
+
alpha: float = 0.1,
|
|
179
|
+
min_channel_n: int = 200,
|
|
180
|
+
high_variance_threshold: float = 0.02**2,
|
|
181
|
+
slope_uncertainty_threshold: float = 0.20,
|
|
182
|
+
channel_clinical_threshold: float = 0.0,
|
|
183
|
+
) -> CertificateRecommendation:
|
|
184
|
+
"""Recommend a certificate policy directly from an audit pack.
|
|
185
|
+
|
|
186
|
+
Requires `audit_pack(..., slope_boot=True)` so that `channel_n` and the
|
|
187
|
+
finite-label slope-repair uncertainty certificate are available.
|
|
188
|
+
"""
|
|
189
|
+
if not isinstance(pack, dict):
|
|
190
|
+
raise ValueError("pack must be an audit-pack dictionary")
|
|
191
|
+
slope_cert = pack.get("slope_repair_certificate")
|
|
192
|
+
if not isinstance(slope_cert, dict) or "channel_n" not in slope_cert:
|
|
193
|
+
raise ValueError("audit pack lacks slope_repair_certificate.channel_n; run audit_pack(..., slope_boot=True)")
|
|
194
|
+
counts = np.asarray(slope_cert["channel_n"], dtype=int)
|
|
195
|
+
return recommend_certificate(
|
|
196
|
+
counts,
|
|
197
|
+
slope_risk=pack.get("slope_risk"),
|
|
198
|
+
prevalence_variance=None,
|
|
199
|
+
slope_uncertainty=slope_cert,
|
|
200
|
+
channel_clinical=pack.get("channel_clinical_summary"),
|
|
201
|
+
alpha=alpha,
|
|
202
|
+
min_channel_n=min_channel_n,
|
|
203
|
+
high_variance_threshold=high_variance_threshold,
|
|
204
|
+
slope_uncertainty_threshold=slope_uncertainty_threshold,
|
|
205
|
+
channel_clinical_threshold=channel_clinical_threshold,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def evaluate_certificate(contract: DeploymentContract, X, y, channels) -> dict:
|
|
210
|
+
"""Replay a compiled certificate on labeled target data.
|
|
211
|
+
|
|
212
|
+
Returns overall empirical coverage and per-channel coverage, including the
|
|
213
|
+
minimum channel coverage. This closes the loop between certificate policy
|
|
214
|
+
recommendation and post-deployment evaluation.
|
|
215
|
+
"""
|
|
216
|
+
if contract.certificate is None:
|
|
217
|
+
raise ValueError("contract has no certificate; compile with CertificateSpec")
|
|
218
|
+
X = np.asarray(X)
|
|
219
|
+
y = np.asarray(y, dtype=int)
|
|
220
|
+
channels = np.asarray(channels, dtype=int)
|
|
221
|
+
if not (len(X) == len(y) == len(channels)):
|
|
222
|
+
raise ValueError("X, y, and channels must have matching lengths")
|
|
223
|
+
|
|
224
|
+
pred_sets = contract.certificate.predict_set(X, channels)
|
|
225
|
+
covered = np.array([int(yi in s) for yi, s in zip(y, pred_sets)], dtype=float)
|
|
226
|
+
per_channel = {}
|
|
227
|
+
for g in sorted(set(channels)):
|
|
228
|
+
sel = channels == g
|
|
229
|
+
per_channel[int(g)] = float(np.mean(covered[sel])) if sel.any() else np.nan
|
|
230
|
+
return {
|
|
231
|
+
"overall_coverage": float(np.mean(covered)),
|
|
232
|
+
"per_channel_coverage": per_channel,
|
|
233
|
+
"min_channel_coverage": float(np.nanmin(list(per_channel.values()))),
|
|
234
|
+
"n": int(len(y)),
|
|
235
|
+
"certificate_spec": _certificate_to_dict(contract.certificate_spec),
|
|
236
|
+
}
|