ml-reliability-toolkit 0.1.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.
- ml_reliability/__init__.py +3 -0
- ml_reliability/checks/__init__.py +0 -0
- ml_reliability/checks/calibration.py +152 -0
- ml_reliability/checks/cardinality.py +155 -0
- ml_reliability/checks/classification_metrics.py +151 -0
- ml_reliability/checks/distribution_shift.py +172 -0
- ml_reliability/checks/duplicates.py +104 -0
- ml_reliability/checks/generalization_gap.py +217 -0
- ml_reliability/checks/missing_values.py +103 -0
- ml_reliability/checks/model_performance.py +187 -0
- ml_reliability/checks/regression_metrics.py +202 -0
- ml_reliability/checks/schema.py +94 -0
- ml_reliability/checks/slice_analysis.py +155 -0
- ml_reliability/checks/stress/__init__.py +41 -0
- ml_reliability/checks/stress/base.py +74 -0
- ml_reliability/checks/stress/categorical.py +181 -0
- ml_reliability/checks/stress/distribution.py +154 -0
- ml_reliability/checks/stress/feature_dropout.py +122 -0
- ml_reliability/checks/stress/missing_values.py +142 -0
- ml_reliability/checks/stress/noise.py +161 -0
- ml_reliability/checks/target_distribution.py +189 -0
- ml_reliability/core/__init__.py +0 -0
- ml_reliability/core/result.py +15 -0
- ml_reliability/py.typed +0 -0
- ml_reliability/reporting.py +642 -0
- ml_reliability/suite.py +611 -0
- ml_reliability_toolkit-0.1.0.dist-info/METADATA +359 -0
- ml_reliability_toolkit-0.1.0.dist-info/RECORD +30 -0
- ml_reliability_toolkit-0.1.0.dist-info/WHEEL +4 -0
- ml_reliability_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
ml_reliability/suite.py
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ml_reliability.checks.calibration import check_calibration
|
|
8
|
+
from ml_reliability.checks.cardinality import check_cardinality
|
|
9
|
+
from ml_reliability.checks.classification_metrics import check_classification_metrics
|
|
10
|
+
from ml_reliability.checks.distribution_shift import check_distribution_shift
|
|
11
|
+
from ml_reliability.checks.duplicates import check_duplicates
|
|
12
|
+
from ml_reliability.checks.generalization_gap import check_generalization_gap
|
|
13
|
+
from ml_reliability.checks.missing_values import check_missing_values
|
|
14
|
+
from ml_reliability.checks.model_performance import check_model_performance
|
|
15
|
+
from ml_reliability.checks.regression_metrics import check_regression_metrics
|
|
16
|
+
from ml_reliability.checks.schema import check_schema
|
|
17
|
+
from ml_reliability.checks.slice_analysis import check_slice_analysis
|
|
18
|
+
from ml_reliability.checks.stress import (
|
|
19
|
+
StressTestOutcome,
|
|
20
|
+
evaluate_categorical_corruption_stress,
|
|
21
|
+
evaluate_distribution_perturbation_stress,
|
|
22
|
+
evaluate_feature_dropout_stress,
|
|
23
|
+
evaluate_missing_value_stress,
|
|
24
|
+
evaluate_noise_stress,
|
|
25
|
+
)
|
|
26
|
+
from ml_reliability.checks.stress.base import validate_rate
|
|
27
|
+
from ml_reliability.checks.target_distribution import check_target_distribution
|
|
28
|
+
from ml_reliability.core.result import CheckResult
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class ReliabilityReport:
|
|
33
|
+
score: float | None
|
|
34
|
+
grade: str
|
|
35
|
+
checks: list[CheckResult] = field(default_factory=list)
|
|
36
|
+
stress_tests: list[StressTestOutcome] = field(default_factory=list)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def failed_checks(self) -> list[CheckResult]:
|
|
40
|
+
return [check for check in self.checks if not check.passed]
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def passed_checks(self) -> list[CheckResult]:
|
|
44
|
+
return [check for check in self.checks if check.passed]
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def warnings(self) -> list[CheckResult]:
|
|
48
|
+
return [check for check in self.checks if check.severity == "warning"]
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def invalid_score_checks(self) -> list[CheckResult]:
|
|
52
|
+
return [
|
|
53
|
+
check
|
|
54
|
+
for check in self.checks
|
|
55
|
+
if check.score is not None
|
|
56
|
+
and (
|
|
57
|
+
isinstance(check.score, bool)
|
|
58
|
+
or not isinstance(check.score, (int, float))
|
|
59
|
+
or not math.isfinite(check.score)
|
|
60
|
+
or not 0.0 <= check.score <= 100.0
|
|
61
|
+
)
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def successful_stress_tests(self) -> list[StressTestOutcome]:
|
|
66
|
+
return [result for result in self.stress_tests if result.passed]
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def failed_stress_tests(self) -> list[StressTestOutcome]:
|
|
70
|
+
return [result for result in self.stress_tests if not result.passed]
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def worst_stress_degradation(self) -> float | None:
|
|
74
|
+
degradations = [
|
|
75
|
+
result.degradation
|
|
76
|
+
for result in self.successful_stress_tests
|
|
77
|
+
if all(
|
|
78
|
+
isinstance(metric, (int, float))
|
|
79
|
+
and not isinstance(metric, bool)
|
|
80
|
+
and math.isfinite(metric)
|
|
81
|
+
for metric in (
|
|
82
|
+
result.baseline_metric,
|
|
83
|
+
result.stressed_metric,
|
|
84
|
+
result.degradation,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
]
|
|
88
|
+
return max(degradations) if degradations else None
|
|
89
|
+
|
|
90
|
+
def to_json(self, path: str) -> None:
|
|
91
|
+
"""Export this report as a JSON file."""
|
|
92
|
+
from ml_reliability.reporting import write_json_report
|
|
93
|
+
|
|
94
|
+
write_json_report(self, path)
|
|
95
|
+
|
|
96
|
+
def to_markdown(self, path: str) -> None:
|
|
97
|
+
"""Export this report as a Markdown file."""
|
|
98
|
+
from ml_reliability.reporting import write_markdown_report
|
|
99
|
+
|
|
100
|
+
write_markdown_report(self, path)
|
|
101
|
+
|
|
102
|
+
def to_html(self, path: str) -> None:
|
|
103
|
+
"""Export this report as an HTML file."""
|
|
104
|
+
from ml_reliability.reporting import write_html_report
|
|
105
|
+
|
|
106
|
+
write_html_report(self, path)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _run_check_safely(
|
|
110
|
+
check_id,
|
|
111
|
+
title,
|
|
112
|
+
category,
|
|
113
|
+
check_function,
|
|
114
|
+
*args,
|
|
115
|
+
**kwargs,
|
|
116
|
+
):
|
|
117
|
+
try:
|
|
118
|
+
return check_function(*args, **kwargs)
|
|
119
|
+
except Exception as exc: # noqa: BLE001 - Isolate individual check failures.
|
|
120
|
+
return CheckResult(
|
|
121
|
+
check_id=check_id,
|
|
122
|
+
title=title,
|
|
123
|
+
category=category,
|
|
124
|
+
passed=False,
|
|
125
|
+
severity="critical",
|
|
126
|
+
score=None,
|
|
127
|
+
message=(f"Check execution failed with {type(exc).__name__}: {exc}"),
|
|
128
|
+
evidence={
|
|
129
|
+
"error_type": type(exc).__name__,
|
|
130
|
+
"error_message": str(exc),
|
|
131
|
+
},
|
|
132
|
+
recommendation=(
|
|
133
|
+
"Investigate the check implementation and "
|
|
134
|
+
"the input that caused the execution failure."
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _run_stress_test_safely(
|
|
140
|
+
stress_type,
|
|
141
|
+
evaluator,
|
|
142
|
+
*args,
|
|
143
|
+
**kwargs,
|
|
144
|
+
):
|
|
145
|
+
try:
|
|
146
|
+
return evaluator(*args, **kwargs)
|
|
147
|
+
except Exception as exc: # noqa: BLE001 - Isolate individual stress-test failures.
|
|
148
|
+
return StressTestOutcome(
|
|
149
|
+
stress_type=stress_type,
|
|
150
|
+
baseline_metric=float("nan"),
|
|
151
|
+
stressed_metric=float("nan"),
|
|
152
|
+
degradation=float("inf"),
|
|
153
|
+
passed=False,
|
|
154
|
+
evidence={
|
|
155
|
+
"error_type": type(exc).__name__,
|
|
156
|
+
"error_message": str(exc),
|
|
157
|
+
"execution_failed": True,
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ReliabilitySuite:
|
|
163
|
+
def __init__(self, task_type: str | None = None):
|
|
164
|
+
if task_type not in {None, "classification", "regression"}:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
"task_type must be 'classification', 'regression', or None."
|
|
167
|
+
)
|
|
168
|
+
self.task_type = task_type
|
|
169
|
+
|
|
170
|
+
def run(
|
|
171
|
+
self,
|
|
172
|
+
*,
|
|
173
|
+
X_train=None,
|
|
174
|
+
y_train=None,
|
|
175
|
+
X_test=None,
|
|
176
|
+
y_test=None,
|
|
177
|
+
model=None,
|
|
178
|
+
slices=None,
|
|
179
|
+
stress_tests=None,
|
|
180
|
+
) -> ReliabilityReport:
|
|
181
|
+
if stress_tests is not None and not isinstance(stress_tests, dict):
|
|
182
|
+
raise TypeError("stress_tests must be a dictionary or None")
|
|
183
|
+
|
|
184
|
+
if stress_tests is not None:
|
|
185
|
+
supported_stress_types = (
|
|
186
|
+
"missing_values",
|
|
187
|
+
"feature_noise",
|
|
188
|
+
"feature_dropout",
|
|
189
|
+
"categorical_corruption",
|
|
190
|
+
"distribution_perturbation",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
unsupported_stress_types = stress_tests.keys() - set(supported_stress_types)
|
|
194
|
+
|
|
195
|
+
if unsupported_stress_types:
|
|
196
|
+
unsupported = sorted(
|
|
197
|
+
unsupported_stress_types,
|
|
198
|
+
key=lambda value: (type(value).__name__, repr(value)),
|
|
199
|
+
)
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"Unsupported stress test(s): {unsupported}. "
|
|
202
|
+
f"Supported stress tests are: "
|
|
203
|
+
f"{list(supported_stress_types)}"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
supported_config_options = {
|
|
207
|
+
"missing_values": {"rate", "random_state"},
|
|
208
|
+
"feature_noise": {"noise_level", "random_state"},
|
|
209
|
+
"feature_dropout": {"features"},
|
|
210
|
+
"categorical_corruption": {
|
|
211
|
+
"columns",
|
|
212
|
+
"rate",
|
|
213
|
+
"random_state",
|
|
214
|
+
},
|
|
215
|
+
"distribution_perturbation": {
|
|
216
|
+
"shift",
|
|
217
|
+
"scale",
|
|
218
|
+
"random_state",
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for config_key in supported_stress_types:
|
|
223
|
+
config = stress_tests.get(config_key)
|
|
224
|
+
|
|
225
|
+
if config is None:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
if not isinstance(config, dict):
|
|
229
|
+
raise TypeError(
|
|
230
|
+
f"stress_tests['{config_key}'] must be a dictionary"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
unsupported_options = (
|
|
234
|
+
config.keys() - supported_config_options[config_key]
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if unsupported_options:
|
|
238
|
+
unsupported = sorted(
|
|
239
|
+
unsupported_options,
|
|
240
|
+
key=lambda value: (type(value).__name__, repr(value)),
|
|
241
|
+
)
|
|
242
|
+
raise ValueError(
|
|
243
|
+
f"Unsupported option(s) for stress test "
|
|
244
|
+
f"'{config_key}': {unsupported}. "
|
|
245
|
+
f"Supported options are: "
|
|
246
|
+
f"{sorted(supported_config_options[config_key])}"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
rate_options = {
|
|
250
|
+
"missing_values": "rate",
|
|
251
|
+
"feature_noise": "noise_level",
|
|
252
|
+
"categorical_corruption": "rate",
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if config_key in rate_options:
|
|
256
|
+
option_name = rate_options[config_key]
|
|
257
|
+
validate_rate(config.get(option_name, 0.1))
|
|
258
|
+
|
|
259
|
+
if config_key in {
|
|
260
|
+
"missing_values",
|
|
261
|
+
"feature_noise",
|
|
262
|
+
"categorical_corruption",
|
|
263
|
+
"distribution_perturbation",
|
|
264
|
+
}:
|
|
265
|
+
random_state = config.get("random_state", 42)
|
|
266
|
+
|
|
267
|
+
if (
|
|
268
|
+
isinstance(random_state, (bool, np.bool_))
|
|
269
|
+
or not isinstance(random_state, (int, np.integer))
|
|
270
|
+
or random_state < 0
|
|
271
|
+
):
|
|
272
|
+
raise ValueError("random_state must be a non-negative integer")
|
|
273
|
+
|
|
274
|
+
if config_key == "distribution_perturbation":
|
|
275
|
+
shift = config.get("shift", 0.0)
|
|
276
|
+
scale = config.get("scale", 1.0)
|
|
277
|
+
|
|
278
|
+
for name, value in (("shift", shift), ("scale", scale)):
|
|
279
|
+
if isinstance(value, (bool, np.bool_)) or not isinstance(
|
|
280
|
+
value, (int, float, np.integer, np.floating)
|
|
281
|
+
):
|
|
282
|
+
raise TypeError(f"{name} must be a numeric value")
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
is_finite = math.isfinite(value)
|
|
286
|
+
except OverflowError:
|
|
287
|
+
is_finite = False
|
|
288
|
+
|
|
289
|
+
if not is_finite:
|
|
290
|
+
raise ValueError(f"{name} must be a finite number")
|
|
291
|
+
|
|
292
|
+
if scale <= 0:
|
|
293
|
+
raise ValueError("scale must be a finite number greater than 0")
|
|
294
|
+
|
|
295
|
+
if config_key == "feature_dropout":
|
|
296
|
+
features = config.get("features", [])
|
|
297
|
+
|
|
298
|
+
if isinstance(features, (str, bytes)) or not isinstance(
|
|
299
|
+
features, Sequence
|
|
300
|
+
):
|
|
301
|
+
raise TypeError("features must be a sequence of column names")
|
|
302
|
+
|
|
303
|
+
if any(not isinstance(feature, str) for feature in features):
|
|
304
|
+
raise ValueError("features must contain only column names")
|
|
305
|
+
|
|
306
|
+
if config_key == "categorical_corruption":
|
|
307
|
+
columns = config.get("columns", [])
|
|
308
|
+
|
|
309
|
+
if isinstance(columns, (str, bytes)) or not isinstance(
|
|
310
|
+
columns, Sequence
|
|
311
|
+
):
|
|
312
|
+
raise TypeError("columns must be a sequence of column names")
|
|
313
|
+
|
|
314
|
+
if any(not isinstance(column, str) for column in columns):
|
|
315
|
+
raise ValueError("columns must contain only column names")
|
|
316
|
+
|
|
317
|
+
if len(columns) != len(set(columns)):
|
|
318
|
+
raise ValueError(
|
|
319
|
+
"duplicate requested column names are not allowed"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
checks: list[CheckResult] = []
|
|
323
|
+
stress_results: list[StressTestOutcome] = []
|
|
324
|
+
detected_task_type = self.task_type
|
|
325
|
+
|
|
326
|
+
if X_train is not None:
|
|
327
|
+
checks.append(
|
|
328
|
+
_run_check_safely(
|
|
329
|
+
"data.schema",
|
|
330
|
+
"Dataset Schema",
|
|
331
|
+
"data_quality",
|
|
332
|
+
check_schema,
|
|
333
|
+
X_train,
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
checks.append(
|
|
337
|
+
_run_check_safely(
|
|
338
|
+
"data.missing_values",
|
|
339
|
+
"Missing Values",
|
|
340
|
+
"data_quality",
|
|
341
|
+
check_missing_values,
|
|
342
|
+
X_train,
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
checks.append(
|
|
346
|
+
_run_check_safely(
|
|
347
|
+
"data.duplicates",
|
|
348
|
+
"Duplicate Rows",
|
|
349
|
+
"data_quality",
|
|
350
|
+
check_duplicates,
|
|
351
|
+
X_train,
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
checks.append(
|
|
355
|
+
_run_check_safely(
|
|
356
|
+
"data.cardinality",
|
|
357
|
+
"Feature Cardinality",
|
|
358
|
+
"data_quality",
|
|
359
|
+
check_cardinality,
|
|
360
|
+
X_train,
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
if X_train is not None and X_test is not None:
|
|
365
|
+
checks.append(
|
|
366
|
+
_run_check_safely(
|
|
367
|
+
"data.distribution_shift",
|
|
368
|
+
"Train/Test Distribution Shift",
|
|
369
|
+
"distribution",
|
|
370
|
+
check_distribution_shift,
|
|
371
|
+
X_train,
|
|
372
|
+
X_test,
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
if y_train is not None:
|
|
377
|
+
target_check = _run_check_safely(
|
|
378
|
+
"target.distribution",
|
|
379
|
+
"Target Distribution",
|
|
380
|
+
"target_quality",
|
|
381
|
+
check_target_distribution,
|
|
382
|
+
y_train,
|
|
383
|
+
task_type=self.task_type,
|
|
384
|
+
)
|
|
385
|
+
checks.append(target_check)
|
|
386
|
+
|
|
387
|
+
detected_task_type = target_check.evidence.get(
|
|
388
|
+
"task_type",
|
|
389
|
+
self.task_type,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if (
|
|
393
|
+
model is not None
|
|
394
|
+
and X_test is not None
|
|
395
|
+
and y_test is not None
|
|
396
|
+
and detected_task_type in {"classification", "regression"}
|
|
397
|
+
):
|
|
398
|
+
checks.append(
|
|
399
|
+
_run_check_safely(
|
|
400
|
+
"model.performance",
|
|
401
|
+
"Model Performance",
|
|
402
|
+
"performance",
|
|
403
|
+
check_model_performance,
|
|
404
|
+
model=model,
|
|
405
|
+
X_test=X_test,
|
|
406
|
+
y_test=y_test,
|
|
407
|
+
task=detected_task_type,
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if X_train is not None and y_train is not None:
|
|
412
|
+
checks.append(
|
|
413
|
+
_run_check_safely(
|
|
414
|
+
"model.generalization_gap",
|
|
415
|
+
"Generalization Gap",
|
|
416
|
+
"performance",
|
|
417
|
+
check_generalization_gap,
|
|
418
|
+
model=model,
|
|
419
|
+
X_train=X_train,
|
|
420
|
+
y_train=y_train,
|
|
421
|
+
X_test=X_test,
|
|
422
|
+
y_test=y_test,
|
|
423
|
+
task=detected_task_type,
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
if detected_task_type == "classification":
|
|
428
|
+
checks.append(
|
|
429
|
+
_run_check_safely(
|
|
430
|
+
"model.classification_metrics",
|
|
431
|
+
"Classification Metrics",
|
|
432
|
+
"performance",
|
|
433
|
+
check_classification_metrics,
|
|
434
|
+
model=model,
|
|
435
|
+
X_test=X_test,
|
|
436
|
+
y_test=y_test,
|
|
437
|
+
)
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
checks.append(
|
|
441
|
+
_run_check_safely(
|
|
442
|
+
"model.calibration",
|
|
443
|
+
"Classification Calibration",
|
|
444
|
+
"performance",
|
|
445
|
+
check_calibration,
|
|
446
|
+
model=model,
|
|
447
|
+
X_test=X_test,
|
|
448
|
+
y_test=y_test,
|
|
449
|
+
)
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
if slices is not None:
|
|
453
|
+
checks.append(
|
|
454
|
+
_run_check_safely(
|
|
455
|
+
"model.slice_analysis",
|
|
456
|
+
"Slice Analysis",
|
|
457
|
+
"performance",
|
|
458
|
+
check_slice_analysis,
|
|
459
|
+
model=model,
|
|
460
|
+
X_test=X_test,
|
|
461
|
+
y_test=y_test,
|
|
462
|
+
slices=slices,
|
|
463
|
+
task=detected_task_type,
|
|
464
|
+
)
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
if detected_task_type == "regression":
|
|
468
|
+
checks.append(
|
|
469
|
+
_run_check_safely(
|
|
470
|
+
"model.regression_metrics",
|
|
471
|
+
"Regression Metrics",
|
|
472
|
+
"performance",
|
|
473
|
+
check_regression_metrics,
|
|
474
|
+
model=model,
|
|
475
|
+
X_test=X_test,
|
|
476
|
+
y_test=y_test,
|
|
477
|
+
)
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Stress tests execute independently of ordinary model diagnostics.
|
|
481
|
+
if (
|
|
482
|
+
stress_tests is not None
|
|
483
|
+
and model is not None
|
|
484
|
+
and X_test is not None
|
|
485
|
+
and y_test is not None
|
|
486
|
+
and detected_task_type in {"classification", "regression"}
|
|
487
|
+
):
|
|
488
|
+
missing_value_config = stress_tests.get("missing_values")
|
|
489
|
+
|
|
490
|
+
if missing_value_config is not None:
|
|
491
|
+
stress_results.append(
|
|
492
|
+
_run_stress_test_safely(
|
|
493
|
+
"missing_values",
|
|
494
|
+
evaluate_missing_value_stress,
|
|
495
|
+
model=model,
|
|
496
|
+
X_test=X_test,
|
|
497
|
+
y_test=y_test,
|
|
498
|
+
task=detected_task_type,
|
|
499
|
+
rate=missing_value_config.get("rate", 0.1),
|
|
500
|
+
random_state=missing_value_config.get("random_state", 42),
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
feature_noise_config = stress_tests.get("feature_noise")
|
|
505
|
+
|
|
506
|
+
if feature_noise_config is not None:
|
|
507
|
+
stress_results.append(
|
|
508
|
+
_run_stress_test_safely(
|
|
509
|
+
"feature_noise",
|
|
510
|
+
evaluate_noise_stress,
|
|
511
|
+
model=model,
|
|
512
|
+
X_test=X_test,
|
|
513
|
+
y_test=y_test,
|
|
514
|
+
task=detected_task_type,
|
|
515
|
+
noise_level=feature_noise_config.get("noise_level", 0.1),
|
|
516
|
+
random_state=feature_noise_config.get("random_state", 42),
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
feature_dropout_config = stress_tests.get("feature_dropout")
|
|
521
|
+
|
|
522
|
+
if feature_dropout_config is not None:
|
|
523
|
+
stress_results.append(
|
|
524
|
+
_run_stress_test_safely(
|
|
525
|
+
"feature_dropout",
|
|
526
|
+
evaluate_feature_dropout_stress,
|
|
527
|
+
model=model,
|
|
528
|
+
X_test=X_test,
|
|
529
|
+
y_test=y_test,
|
|
530
|
+
task=detected_task_type,
|
|
531
|
+
features=feature_dropout_config.get("features", []),
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
categorical_corruption_config = stress_tests.get("categorical_corruption")
|
|
536
|
+
|
|
537
|
+
if categorical_corruption_config is not None:
|
|
538
|
+
stress_results.append(
|
|
539
|
+
_run_stress_test_safely(
|
|
540
|
+
"categorical_corruption",
|
|
541
|
+
evaluate_categorical_corruption_stress,
|
|
542
|
+
model=model,
|
|
543
|
+
X_test=X_test,
|
|
544
|
+
y_test=y_test,
|
|
545
|
+
task=detected_task_type,
|
|
546
|
+
columns=categorical_corruption_config.get("columns", []),
|
|
547
|
+
rate=categorical_corruption_config.get("rate", 0.1),
|
|
548
|
+
random_state=categorical_corruption_config.get(
|
|
549
|
+
"random_state", 42
|
|
550
|
+
),
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
distribution_perturbation_config = stress_tests.get(
|
|
555
|
+
"distribution_perturbation"
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
if distribution_perturbation_config is not None:
|
|
559
|
+
stress_results.append(
|
|
560
|
+
_run_stress_test_safely(
|
|
561
|
+
"distribution_perturbation",
|
|
562
|
+
evaluate_distribution_perturbation_stress,
|
|
563
|
+
model=model,
|
|
564
|
+
X_test=X_test,
|
|
565
|
+
y_test=y_test,
|
|
566
|
+
task=detected_task_type,
|
|
567
|
+
shift=distribution_perturbation_config.get("shift", 0.0),
|
|
568
|
+
scale=distribution_perturbation_config.get("scale", 1.0),
|
|
569
|
+
random_state=distribution_perturbation_config.get(
|
|
570
|
+
"random_state", 42
|
|
571
|
+
),
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
scored_checks = [
|
|
576
|
+
check
|
|
577
|
+
for check in checks
|
|
578
|
+
if check.score is not None
|
|
579
|
+
and not isinstance(check.score, bool)
|
|
580
|
+
and isinstance(check.score, (int, float))
|
|
581
|
+
and math.isfinite(check.score)
|
|
582
|
+
and 0.0 <= check.score <= 100.0
|
|
583
|
+
]
|
|
584
|
+
|
|
585
|
+
if not scored_checks:
|
|
586
|
+
return ReliabilityReport(
|
|
587
|
+
score=None,
|
|
588
|
+
grade="N/A",
|
|
589
|
+
checks=checks,
|
|
590
|
+
stress_tests=stress_results,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
score = sum(check.score for check in scored_checks) / len(scored_checks)
|
|
594
|
+
|
|
595
|
+
if score >= 90:
|
|
596
|
+
grade = "A"
|
|
597
|
+
elif score >= 75:
|
|
598
|
+
grade = "B"
|
|
599
|
+
elif score >= 60:
|
|
600
|
+
grade = "C"
|
|
601
|
+
elif score >= 40:
|
|
602
|
+
grade = "D"
|
|
603
|
+
else:
|
|
604
|
+
grade = "F"
|
|
605
|
+
|
|
606
|
+
return ReliabilityReport(
|
|
607
|
+
score=score,
|
|
608
|
+
grade=grade,
|
|
609
|
+
checks=checks,
|
|
610
|
+
stress_tests=stress_results,
|
|
611
|
+
)
|