rti-ethics-engine 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.
- rti_ethics_engine/__init__.py +73 -0
- rti_ethics_engine/config.py +169 -0
- rti_ethics_engine/core.py +184 -0
- rti_ethics_engine/dimensions.py +373 -0
- rti_ethics_engine/metrics.py +52 -0
- rti_ethics_engine/results.py +386 -0
- rti_ethics_engine-1.0.0.dist-info/METADATA +198 -0
- rti_ethics_engine-1.0.0.dist-info/RECORD +10 -0
- rti_ethics_engine-1.0.0.dist-info/WHEEL +4 -0
- rti_ethics_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RTI Ethics Engine - Dimension Evaluators
|
|
3
|
+
All 9 ethical dimension classes with L1/L2/L3 evaluation methods.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from typing import Dict, Tuple, List, Any
|
|
8
|
+
from .config import L1_THRESHOLDS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EthicsDimension(ABC):
|
|
12
|
+
"""Base class for all ethical dimension evaluators."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, key: str, name: str, description: str):
|
|
15
|
+
self.key = key
|
|
16
|
+
self.name = name
|
|
17
|
+
self.description = description
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def evaluate_layer1(self, metrics: Dict[str, Any]) -> Tuple[str, List[str]]:
|
|
21
|
+
"""
|
|
22
|
+
Strict Mathematical Gate - returns (status, reasons).
|
|
23
|
+
status: "PASS" or "FAIL"
|
|
24
|
+
reasons: List of failure reasons if FAIL
|
|
25
|
+
"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def evaluate_layer2(self, metrics: Dict[str, Any]) -> Tuple[float, List[str]]:
|
|
30
|
+
"""
|
|
31
|
+
Risk Scoring - returns (risk_score, indicators).
|
|
32
|
+
risk_score: float in [0, 1]
|
|
33
|
+
indicators: List of risk indicators
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def get_layer3_evidence(
|
|
39
|
+
self,
|
|
40
|
+
metrics: Dict[str, Any],
|
|
41
|
+
l1_result: Tuple[str, List[str]],
|
|
42
|
+
l2_result: Tuple[float, List[str]]
|
|
43
|
+
) -> Dict[str, Any]:
|
|
44
|
+
"""
|
|
45
|
+
Forensic Evidence Bundle for CIC auditors.
|
|
46
|
+
Returns dict with explanation and supporting data.
|
|
47
|
+
"""
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BiasFairness(EthicsDimension):
|
|
52
|
+
"""Ensures equitable outcomes across demographics."""
|
|
53
|
+
|
|
54
|
+
def __init__(self):
|
|
55
|
+
super().__init__(
|
|
56
|
+
key="fairness",
|
|
57
|
+
name="Bias & Fairness",
|
|
58
|
+
description="Ensures equitable outcomes across demographics."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def evaluate_layer1(self, m: Dict[str, Any]) -> Tuple[str, List[str]]:
|
|
62
|
+
# Handle single-group case
|
|
63
|
+
if m.get("note") == "single_group_no_comparison":
|
|
64
|
+
return "PASS", []
|
|
65
|
+
|
|
66
|
+
reasons = []
|
|
67
|
+
t = L1_THRESHOLDS["fairness"]
|
|
68
|
+
|
|
69
|
+
if abs(m.get("spd", 0)) > t["spd"]:
|
|
70
|
+
reasons.append(f"SPD {m['spd']:.3f} > {t['spd']}")
|
|
71
|
+
if not (t["di_min"] <= m.get("di", 1.0) <= t["di_max"]):
|
|
72
|
+
reasons.append(f"DI {m.get('di', 1.0):.3f} out of range [{t['di_min']}, {t['di_max']}]")
|
|
73
|
+
if abs(m.get("eop", 0)) > t["eop"]:
|
|
74
|
+
reasons.append(f"EOP {m['eop']:.3f} > {t['eop']}")
|
|
75
|
+
if abs(m.get("aod", 0)) > t["aod"]:
|
|
76
|
+
reasons.append(f"AOD {m['aod']:.3f} > {t['aod']}")
|
|
77
|
+
|
|
78
|
+
return ("FAIL", reasons) if reasons else ("PASS", [])
|
|
79
|
+
|
|
80
|
+
def evaluate_layer2(self, m: Dict[str, Any]) -> Tuple[float, List[str]]:
|
|
81
|
+
# Weights: FNRD (0.4), PredParity (0.3), RD (0.3)
|
|
82
|
+
risk = (
|
|
83
|
+
0.4 * abs(m.get("fnrd", 0)) +
|
|
84
|
+
0.3 * abs(m.get("pred_parity", 0)) +
|
|
85
|
+
0.3 * abs(m.get("refusal_disp", 0))
|
|
86
|
+
)
|
|
87
|
+
indicators = []
|
|
88
|
+
if risk > 0.3:
|
|
89
|
+
indicators.append("Detected operational bias in secondary metrics.")
|
|
90
|
+
return float(min(risk, 1.0)), indicators
|
|
91
|
+
|
|
92
|
+
def get_layer3_evidence(
|
|
93
|
+
self, m: Dict[str, Any], l1: Tuple[str, List[str]], l2: Tuple[float, List[str]]
|
|
94
|
+
) -> Dict[str, Any]:
|
|
95
|
+
status, reasons = l1
|
|
96
|
+
if status == "FAIL":
|
|
97
|
+
text = f"CRITICAL POLICY BREACH: {'; '.join(reasons)}. System demonstrates demographic exclusion contrary to RTI Act Preamble."
|
|
98
|
+
else:
|
|
99
|
+
text = f"FAIRNESS AUDIT: Counterfactual Proxy={m.get('counterfactual', 0):.2f}, MI Leakage={m.get('mi_leakage', 0):.2f}. Compliance verified."
|
|
100
|
+
return {"explanation": text, "raw_metrics": m}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ModelDrift(EthicsDimension):
|
|
104
|
+
"""Detects performance degradation over time."""
|
|
105
|
+
|
|
106
|
+
def __init__(self):
|
|
107
|
+
super().__init__(
|
|
108
|
+
key="drift",
|
|
109
|
+
name="Data & Model Drift",
|
|
110
|
+
description="Detects performance degradation over time."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def evaluate_layer1(self, m: Dict[str, Any]) -> Tuple[str, List[str]]:
|
|
114
|
+
reasons = []
|
|
115
|
+
t = L1_THRESHOLDS["drift"]
|
|
116
|
+
|
|
117
|
+
if m.get("psi", 0) > t["psi"]:
|
|
118
|
+
reasons.append(f"PSI {m['psi']:.3f} > {t['psi']}")
|
|
119
|
+
if m.get("ks", 0) > t["ks"]:
|
|
120
|
+
reasons.append(f"KS {m['ks']:.3f} > {t['ks']}")
|
|
121
|
+
|
|
122
|
+
return ("FAIL", reasons) if reasons else ("PASS", [])
|
|
123
|
+
|
|
124
|
+
def evaluate_layer2(self, m: Dict[str, Any]) -> Tuple[float, List[str]]:
|
|
125
|
+
risk = 0.5 * abs(m.get("pred_drift", 0)) + 0.5 * abs(m.get("conf_drift", 0))
|
|
126
|
+
return float(min(risk * 2, 1.0)), []
|
|
127
|
+
|
|
128
|
+
def get_layer3_evidence(
|
|
129
|
+
self, m: Dict[str, Any], l1: Tuple[str, List[str]], l2: Tuple[float, List[str]]
|
|
130
|
+
) -> Dict[str, Any]:
|
|
131
|
+
text = f"DRIFT LOG: Embedding Drift={m.get('emb_drift', 0):.3f}. Model output stability is high."
|
|
132
|
+
return {"explanation": text, "raw_metrics": m}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class EnvironmentalEthics(EthicsDimension):
|
|
136
|
+
"""Monitors carbon footprint and compute efficiency."""
|
|
137
|
+
|
|
138
|
+
def __init__(self):
|
|
139
|
+
super().__init__(
|
|
140
|
+
key="environmental",
|
|
141
|
+
name="Environmental & Cost",
|
|
142
|
+
description="Monitors carbon footprint and compute efficiency."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def evaluate_layer1(self, m: Dict[str, Any]) -> Tuple[str, List[str]]:
|
|
146
|
+
reasons = []
|
|
147
|
+
t = L1_THRESHOLDS["environmental"]
|
|
148
|
+
|
|
149
|
+
if m.get("inf_cost", 0) > t["inf_cost_max"]:
|
|
150
|
+
reasons.append(f"Cost ${m['inf_cost']:.3f} > ${t['inf_cost_max']}")
|
|
151
|
+
if m.get("gpu_hours", 0) > t["gpu_hours_max"]:
|
|
152
|
+
reasons.append(f"GPU {m['gpu_hours']:.3f}h > {t['gpu_hours_max']}h")
|
|
153
|
+
|
|
154
|
+
return ("FAIL", reasons) if reasons else ("PASS", [])
|
|
155
|
+
|
|
156
|
+
def evaluate_layer2(self, m: Dict[str, Any]) -> Tuple[float, List[str]]:
|
|
157
|
+
risk = 0.6 * m.get("compute_intensity", 0) + 0.4 * (1 - m.get("hardware_util", 1.0))
|
|
158
|
+
return float(min(risk, 1.0)), []
|
|
159
|
+
|
|
160
|
+
def get_layer3_evidence(
|
|
161
|
+
self, m: Dict[str, Any], l1: Tuple[str, List[str]], l2: Tuple[float, List[str]]
|
|
162
|
+
) -> Dict[str, Any]:
|
|
163
|
+
text = f"SUSTAINABILITY: CUR={m.get('cur', 0):.2f}. Inference gain vs cost ratio is optimal."
|
|
164
|
+
return {"explanation": text, "raw_metrics": m}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Explainability(EthicsDimension):
|
|
168
|
+
"""Ensures system reasoning is interpretable."""
|
|
169
|
+
|
|
170
|
+
def __init__(self):
|
|
171
|
+
super().__init__(
|
|
172
|
+
key="explainability",
|
|
173
|
+
name="Explainability & Transparency",
|
|
174
|
+
description="Ensures system reasoning is interpretable."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def evaluate_layer1(self, m: Dict[str, Any]) -> Tuple[str, List[str]]:
|
|
178
|
+
reasons = []
|
|
179
|
+
t = L1_THRESHOLDS["explainability"]
|
|
180
|
+
|
|
181
|
+
if m.get("confidence", 1.0) < t["confidence_min"]:
|
|
182
|
+
reasons.append(f"Conf {m['confidence']:.2f} < {t['confidence_min']}")
|
|
183
|
+
if m.get("fi", 1.0) < t["fi_min"]:
|
|
184
|
+
reasons.append(f"FI {m['fi']:.2f} < {t['fi_min']}")
|
|
185
|
+
|
|
186
|
+
return ("FAIL", reasons) if reasons else ("PASS", [])
|
|
187
|
+
|
|
188
|
+
def evaluate_layer2(self, m: Dict[str, Any]) -> Tuple[float, List[str]]:
|
|
189
|
+
# Close counterfactuals = high risk
|
|
190
|
+
risk = 1.0 - m.get("counterfactual_dist", 0.5)
|
|
191
|
+
return float(min(max(risk, 0), 1.0)), []
|
|
192
|
+
|
|
193
|
+
def get_layer3_evidence(
|
|
194
|
+
self, m: Dict[str, Any], l1: Tuple[str, List[str]], l2: Tuple[float, List[str]]
|
|
195
|
+
) -> Dict[str, Any]:
|
|
196
|
+
text = f"EXPLAINABILITY: Exp Stability={m.get('exp_stability', 0):.3f} (Saliency Consistency per SHAP/IG)."
|
|
197
|
+
return {"explanation": text, "raw_metrics": m}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class HumanAIUX(EthicsDimension):
|
|
201
|
+
"""Evaluates user friction and reading complexity."""
|
|
202
|
+
|
|
203
|
+
def __init__(self):
|
|
204
|
+
super().__init__(
|
|
205
|
+
key="ux",
|
|
206
|
+
name="Human-AI UX",
|
|
207
|
+
description="Evaluates user friction and reading complexity."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def evaluate_layer1(self, m: Dict[str, Any]) -> Tuple[str, List[str]]:
|
|
211
|
+
reasons = []
|
|
212
|
+
t = L1_THRESHOLDS["ux"]
|
|
213
|
+
|
|
214
|
+
if m.get("tcr", 1.0) < t["tcr_min"]:
|
|
215
|
+
reasons.append(f"TCR {m['tcr']:.2f} < {t['tcr_min']}")
|
|
216
|
+
if m.get("ftr", 0) > t["ftr_max"]:
|
|
217
|
+
reasons.append(f"FTR {m['ftr']:.2f} > {t['ftr_max']}")
|
|
218
|
+
|
|
219
|
+
return ("FAIL", reasons) if reasons else ("PASS", [])
|
|
220
|
+
|
|
221
|
+
def evaluate_layer2(self, m: Dict[str, Any]) -> Tuple[float, List[str]]:
|
|
222
|
+
risk = 0.5 * (m.get("dtc", 2) / 10) + 0.5 * m.get("cognitive_load", 0)
|
|
223
|
+
return float(min(risk, 1.0)), []
|
|
224
|
+
|
|
225
|
+
def get_layer3_evidence(
|
|
226
|
+
self, m: Dict[str, Any], l1: Tuple[str, List[str]], l2: Tuple[float, List[str]]
|
|
227
|
+
) -> Dict[str, Any]:
|
|
228
|
+
text = f"USER EXPERIENCE: FK Readability={m.get('readability_fk', 0):.1f}. Response complexity matches user cohort profile."
|
|
229
|
+
return {"explanation": text, "raw_metrics": m}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class LegalCompliance(EthicsDimension):
|
|
233
|
+
"""Statutory alignment with the RTI Act 2005."""
|
|
234
|
+
|
|
235
|
+
def __init__(self):
|
|
236
|
+
super().__init__(
|
|
237
|
+
key="legal",
|
|
238
|
+
name="Legal Compliance",
|
|
239
|
+
description="Statutory alignment with the RTI Act 2005."
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def evaluate_layer1(self, m: Dict[str, Any]) -> Tuple[str, List[str]]:
|
|
243
|
+
reasons = []
|
|
244
|
+
t = L1_THRESHOLDS["legal"]
|
|
245
|
+
|
|
246
|
+
if m.get("lawfulness", 1.0) < t["lawfulness"]:
|
|
247
|
+
reasons.append("Missing legal basis for disclosure/refusal.")
|
|
248
|
+
if m.get("cvr", 1.0) < t["cvr_min"]:
|
|
249
|
+
reasons.append(f"Consent Validity {m['cvr']:.2f} < {t['cvr_min']}")
|
|
250
|
+
|
|
251
|
+
return ("FAIL", reasons) if reasons else ("PASS", [])
|
|
252
|
+
|
|
253
|
+
def evaluate_layer2(self, m: Dict[str, Any]) -> Tuple[float, List[str]]:
|
|
254
|
+
risk = 1.0 - m.get("proportionality", 1.0)
|
|
255
|
+
return float(risk), []
|
|
256
|
+
|
|
257
|
+
def get_layer3_evidence(
|
|
258
|
+
self, m: Dict[str, Any], l1: Tuple[str, List[str]], l2: Tuple[float, List[str]]
|
|
259
|
+
) -> Dict[str, Any]:
|
|
260
|
+
text = f"REGULATORY: DPIA Risk Coverage={m.get('dpia', 1.0):.1f}. Statutory Speaking Order formatting confirmed."
|
|
261
|
+
return {"explanation": text, "raw_metrics": m}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class ModelPerformance(EthicsDimension):
|
|
265
|
+
"""Core statistical accuracy and recall."""
|
|
266
|
+
|
|
267
|
+
def __init__(self):
|
|
268
|
+
super().__init__(
|
|
269
|
+
key="performance",
|
|
270
|
+
name="Model Performance",
|
|
271
|
+
description="Core statistical accuracy and recall."
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def evaluate_layer1(self, m: Dict[str, Any]) -> Tuple[str, List[str]]:
|
|
275
|
+
reasons = []
|
|
276
|
+
t = L1_THRESHOLDS["performance"]
|
|
277
|
+
|
|
278
|
+
if m.get("accuracy", 1.0) < t["accuracy_min"]:
|
|
279
|
+
reasons.append(f"Accuracy {m['accuracy']:.2f} < {t['accuracy_min']}")
|
|
280
|
+
if m.get("recall", 1.0) < t["recall_min"]:
|
|
281
|
+
reasons.append(f"Recall {m['recall']:.2f} < {t['recall_min']}")
|
|
282
|
+
|
|
283
|
+
return ("FAIL", reasons) if reasons else ("PASS", [])
|
|
284
|
+
|
|
285
|
+
def evaluate_layer2(self, m: Dict[str, Any]) -> Tuple[float, List[str]]:
|
|
286
|
+
risk = 1.0 - m.get("group_f1", 1.0)
|
|
287
|
+
return float(risk), []
|
|
288
|
+
|
|
289
|
+
def get_layer3_evidence(
|
|
290
|
+
self, m: Dict[str, Any], l1: Tuple[str, List[str]], l2: Tuple[float, List[str]]
|
|
291
|
+
) -> Dict[str, Any]:
|
|
292
|
+
text = f"PERFORMANCE: Rare Query Acc (Acc_LT)={m.get('acc_lt', 1.0):.2f}. Core grounding fidelity is high."
|
|
293
|
+
return {"explanation": text, "raw_metrics": m}
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class MonitoringEthics(EthicsDimension):
|
|
297
|
+
"""Integrity of the audit trail."""
|
|
298
|
+
|
|
299
|
+
def __init__(self):
|
|
300
|
+
super().__init__(
|
|
301
|
+
key="monitoring",
|
|
302
|
+
name="Monitoring & Compliance",
|
|
303
|
+
description="Integrity of the audit trail."
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def evaluate_layer1(self, m: Dict[str, Any]) -> Tuple[str, List[str]]:
|
|
307
|
+
reasons = []
|
|
308
|
+
t = L1_THRESHOLDS["monitoring"]
|
|
309
|
+
|
|
310
|
+
if m.get("alc", 1.0) < t["alc_min"]:
|
|
311
|
+
reasons.append(f"ALC {m['alc']:.2f} < {t['alc_min']}")
|
|
312
|
+
if m.get("sla", 1.0) < t["sla_min"]:
|
|
313
|
+
reasons.append(f"SLA {m['sla']:.2f} < {t['sla_min']}")
|
|
314
|
+
|
|
315
|
+
return ("FAIL", reasons) if reasons else ("PASS", [])
|
|
316
|
+
|
|
317
|
+
def evaluate_layer2(self, m: Dict[str, Any]) -> Tuple[float, List[str]]:
|
|
318
|
+
return float(m.get("pvr", 0.0)), []
|
|
319
|
+
|
|
320
|
+
def get_layer3_evidence(
|
|
321
|
+
self, m: Dict[str, Any], l1: Tuple[str, List[str]], l2: Tuple[float, List[str]]
|
|
322
|
+
) -> Dict[str, Any]:
|
|
323
|
+
text = f"GOVERNANCE: Incident Reconstruction Fidelity (IRF)={m.get('irf', 1.0):.1f}. Audit log is immutable."
|
|
324
|
+
return {"explanation": text, "raw_metrics": m}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class SecurityRobustness(EthicsDimension):
|
|
328
|
+
"""Resistance to prompt injection and poisoning."""
|
|
329
|
+
|
|
330
|
+
def __init__(self):
|
|
331
|
+
super().__init__(
|
|
332
|
+
key="security",
|
|
333
|
+
name="Security & Robustness",
|
|
334
|
+
description="Resistance to prompt injection and poisoning."
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def evaluate_layer1(self, m: Dict[str, Any]) -> Tuple[str, List[str]]:
|
|
338
|
+
reasons = []
|
|
339
|
+
t = L1_THRESHOLDS["security"]
|
|
340
|
+
|
|
341
|
+
if m.get("adv_acc", 1.0) < t["adv_acc_min"]:
|
|
342
|
+
reasons.append(f"AdvAcc {m['adv_acc']:.2f} < {t['adv_acc_min']}")
|
|
343
|
+
if m.get("pisr", 0) > t["pisr_max"]:
|
|
344
|
+
reasons.append(f"PISR {m['pisr']:.2f} > {t['pisr_max']}")
|
|
345
|
+
|
|
346
|
+
return ("FAIL", reasons) if reasons else ("PASS", [])
|
|
347
|
+
|
|
348
|
+
def evaluate_layer2(self, m: Dict[str, Any]) -> Tuple[float, List[str]]:
|
|
349
|
+
return float(m.get("mir", 0.0)), []
|
|
350
|
+
|
|
351
|
+
def get_layer3_evidence(
|
|
352
|
+
self, m: Dict[str, Any], l1: Tuple[str, List[str]], l2: Tuple[float, List[str]]
|
|
353
|
+
) -> Dict[str, Any]:
|
|
354
|
+
text = f"SECURITY: Poisoning Detection Rate (PDR)={m.get('pdr', 1.0):.1f}. Adversarial robustness verified."
|
|
355
|
+
return {"explanation": text, "raw_metrics": m}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# ================== DIMENSION REGISTRY ==================
|
|
359
|
+
# Factory function to create all dimension instances
|
|
360
|
+
|
|
361
|
+
def create_all_dimensions() -> Dict[str, EthicsDimension]:
|
|
362
|
+
"""Create and return all 9 dimension evaluators."""
|
|
363
|
+
return {
|
|
364
|
+
"legal": LegalCompliance(),
|
|
365
|
+
"fairness": BiasFairness(),
|
|
366
|
+
"explainability": Explainability(),
|
|
367
|
+
"security": SecurityRobustness(),
|
|
368
|
+
"monitoring": MonitoringEthics(),
|
|
369
|
+
"performance": ModelPerformance(),
|
|
370
|
+
"ux": HumanAIUX(),
|
|
371
|
+
"drift": ModelDrift(),
|
|
372
|
+
"environmental": EnvironmentalEthics()
|
|
373
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RTI Ethics Engine - Metrics Helpers
|
|
3
|
+
Utility functions for computing L1/L2 metrics from raw data.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def compute_spd(y_pred: np.ndarray, protected: np.ndarray) -> float:
|
|
11
|
+
"""Compute Statistical Parity Difference. Threshold: |SPD| <= 0.1"""
|
|
12
|
+
g1 = y_pred[protected == 1]
|
|
13
|
+
g0 = y_pred[protected == 0]
|
|
14
|
+
if len(g1) == 0 or len(g0) == 0:
|
|
15
|
+
return 0.0
|
|
16
|
+
return float(np.mean(g1) - np.mean(g0))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def compute_disparate_impact(y_pred: np.ndarray, protected: np.ndarray) -> float:
|
|
20
|
+
"""Compute Disparate Impact. Acceptable range: [0.8, 1.25]"""
|
|
21
|
+
g1 = y_pred[protected == 1]
|
|
22
|
+
g0 = y_pred[protected == 0]
|
|
23
|
+
if len(g1) == 0 or len(g0) == 0:
|
|
24
|
+
return 1.0
|
|
25
|
+
r1, r0 = np.mean(g1), np.mean(g0)
|
|
26
|
+
return 1.0 if r1 == 0 else float(r0 / r1)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def compute_accuracy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
|
30
|
+
"""Compute accuracy."""
|
|
31
|
+
return float(np.mean(y_true == y_pred))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def compute_recall(y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
|
35
|
+
"""Compute recall."""
|
|
36
|
+
pos = y_true == 1
|
|
37
|
+
return 1.0 if np.sum(pos) == 0 else float(np.mean(y_pred[pos] == 1))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_default_metrics() -> Dict[str, Dict[str, float]]:
|
|
41
|
+
"""Build complete default metrics dict for all 9 dimensions."""
|
|
42
|
+
return {
|
|
43
|
+
"fairness": {"spd": 0.0, "di": 1.0, "eop": 0.0, "aod": 0.0, "fnrd": 0.0, "pred_parity": 0.0, "refusal_disp": 0.0},
|
|
44
|
+
"drift": {"psi": 0.0, "ks": 0.0, "pred_drift": 0.0, "conf_drift": 0.0},
|
|
45
|
+
"environmental": {"inf_cost": 0.01, "gpu_hours": 0.01, "compute_intensity": 0.1, "hardware_util": 0.9},
|
|
46
|
+
"explainability": {"confidence": 0.9, "fi": 0.5, "counterfactual_dist": 0.7},
|
|
47
|
+
"ux": {"tcr": 0.95, "ftr": 0.05, "dtc": 2, "cognitive_load": 0.1},
|
|
48
|
+
"legal": {"lawfulness": 1.0, "cvr": 0.98, "proportionality": 0.95},
|
|
49
|
+
"performance": {"accuracy": 0.92, "recall": 0.88, "group_f1": 0.90},
|
|
50
|
+
"monitoring": {"alc": 0.98, "sla": 0.995, "pvr": 0.02},
|
|
51
|
+
"security": {"adv_acc": 0.85, "pisr": 0.02, "mir": 0.01}
|
|
52
|
+
}
|