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.
@@ -0,0 +1,104 @@
1
+ from typing import Any
2
+
3
+ from ml_reliability.core.result import CheckResult
4
+
5
+
6
+ def check_duplicates(X: Any) -> CheckResult:
7
+ """
8
+ Check a dataset for duplicate rows.
9
+
10
+ Duplicate rows are counted after the first occurrence.
11
+ """
12
+
13
+ if X is None:
14
+ return CheckResult(
15
+ check_id="data.duplicates",
16
+ title="Duplicate Rows",
17
+ category="data_quality",
18
+ passed=False,
19
+ severity="critical",
20
+ score=0.0,
21
+ message="Input dataset is missing.",
22
+ recommendation="Provide a valid dataset before running reliability checks.",
23
+ )
24
+
25
+ if not hasattr(X, "duplicated") or not hasattr(X, "shape"):
26
+ return CheckResult(
27
+ check_id="data.duplicates",
28
+ title="Duplicate Rows",
29
+ category="data_quality",
30
+ passed=False,
31
+ severity="critical",
32
+ score=0.0,
33
+ message="Input must be a pandas DataFrame.",
34
+ recommendation="Provide the dataset as a pandas DataFrame.",
35
+ )
36
+
37
+ total_rows = int(X.shape[0])
38
+
39
+ if total_rows == 0:
40
+ return CheckResult(
41
+ check_id="data.duplicates",
42
+ title="Duplicate Rows",
43
+ category="data_quality",
44
+ passed=True,
45
+ severity="info",
46
+ score=100.0,
47
+ message="Dataset is empty; no duplicate rows were detected.",
48
+ evidence={
49
+ "total_rows": 0,
50
+ "duplicate_rows": 0,
51
+ "duplicate_percentage": 0.0,
52
+ },
53
+ )
54
+
55
+ duplicate_rows = int(X.duplicated().sum())
56
+ duplicate_percentage = (duplicate_rows / total_rows) * 100
57
+
58
+ if duplicate_rows == 0:
59
+ return CheckResult(
60
+ check_id="data.duplicates",
61
+ title="Duplicate Rows",
62
+ category="data_quality",
63
+ passed=True,
64
+ severity="info",
65
+ score=100.0,
66
+ message="No duplicate rows were detected.",
67
+ evidence={
68
+ "total_rows": total_rows,
69
+ "duplicate_rows": 0,
70
+ "duplicate_percentage": 0.0,
71
+ },
72
+ )
73
+
74
+ if duplicate_percentage > 20:
75
+ severity = "critical"
76
+ score = 0.0
77
+ elif duplicate_percentage >= 5:
78
+ severity = "warning"
79
+ score = 50.0
80
+ else:
81
+ severity = "info"
82
+ score = 80.0
83
+
84
+ return CheckResult(
85
+ check_id="data.duplicates",
86
+ title="Duplicate Rows",
87
+ category="data_quality",
88
+ passed=False,
89
+ severity=severity,
90
+ score=score,
91
+ message=(
92
+ f"Detected {duplicate_rows} duplicate rows "
93
+ f"({duplicate_percentage:.2f}% of all rows)."
94
+ ),
95
+ evidence={
96
+ "total_rows": total_rows,
97
+ "duplicate_rows": duplicate_rows,
98
+ "duplicate_percentage": round(duplicate_percentage, 2),
99
+ },
100
+ recommendation=(
101
+ "Investigate duplicate records and remove or handle them "
102
+ "appropriately before model training."
103
+ ),
104
+ )
@@ -0,0 +1,217 @@
1
+ import math
2
+ from numbers import Real
3
+ from typing import Any
4
+
5
+ from ml_reliability.core.result import CheckResult
6
+
7
+
8
+ def check_generalization_gap(
9
+ model: Any,
10
+ X_train: Any,
11
+ y_train: Any,
12
+ X_test: Any,
13
+ y_test: Any,
14
+ task: str,
15
+ threshold: float = 0.10,
16
+ ) -> CheckResult:
17
+ """
18
+ Measure the performance gap between training and test data.
19
+
20
+ Supported tasks:
21
+ - classification: accuracy
22
+ - regression: R²
23
+ """
24
+
25
+ if model is None:
26
+ return CheckResult(
27
+ check_id="model.generalization_gap",
28
+ title="Generalization Gap",
29
+ category="performance",
30
+ passed=False,
31
+ severity="critical",
32
+ score=0.0,
33
+ message="A trained model is required.",
34
+ recommendation="Provide a trained model with a predict() method.",
35
+ )
36
+
37
+ if (
38
+ isinstance(threshold, bool)
39
+ or not isinstance(threshold, Real)
40
+ or not math.isfinite(threshold)
41
+ or threshold < 0
42
+ ):
43
+ return CheckResult(
44
+ check_id="model.generalization_gap",
45
+ title="Generalization Gap",
46
+ category="performance",
47
+ passed=False,
48
+ severity="critical",
49
+ score=0.0,
50
+ message=(
51
+ "Invalid threshold: threshold must be a finite, non-negative number."
52
+ ),
53
+ evidence={"threshold": repr(threshold)},
54
+ recommendation=(
55
+ "Provide a finite, non-negative threshold for the generalization gap."
56
+ ),
57
+ )
58
+
59
+ if X_train is None or y_train is None or X_test is None or y_test is None:
60
+ return CheckResult(
61
+ check_id="model.generalization_gap",
62
+ title="Generalization Gap",
63
+ category="performance",
64
+ passed=False,
65
+ severity="critical",
66
+ score=0.0,
67
+ message="Training and test data are required.",
68
+ recommendation=("Provide X_train, y_train, X_test, and y_test."),
69
+ )
70
+
71
+ if task not in {"classification", "regression"}:
72
+ return CheckResult(
73
+ check_id="model.generalization_gap",
74
+ title="Generalization Gap",
75
+ category="performance",
76
+ passed=False,
77
+ severity="critical",
78
+ score=0.0,
79
+ message=f"Unsupported task type: {task}.",
80
+ recommendation=("Use task='classification' or task='regression'."),
81
+ )
82
+
83
+ if not hasattr(model, "predict") or not callable(model.predict):
84
+ return CheckResult(
85
+ check_id="model.generalization_gap",
86
+ title="Generalization Gap",
87
+ category="performance",
88
+ passed=False,
89
+ severity="critical",
90
+ score=0.0,
91
+ message="The model must provide a callable predict() method.",
92
+ recommendation="Provide a trained model implementing predict().",
93
+ )
94
+
95
+ try:
96
+ train_predictions = model.predict(X_train)
97
+ test_predictions = model.predict(X_test)
98
+
99
+ train_actual = list(y_train)
100
+ test_actual = list(y_test)
101
+
102
+ train_predicted = list(train_predictions)
103
+ test_predicted = list(test_predictions)
104
+
105
+ if not train_actual or not test_actual:
106
+ raise ValueError(
107
+ "Training and test targets must contain at least one sample."
108
+ )
109
+
110
+ if len(train_actual) != len(train_predicted):
111
+ raise ValueError(
112
+ "Training predictions and y_train must have the same length."
113
+ )
114
+
115
+ if len(test_actual) != len(test_predicted):
116
+ raise ValueError("Test predictions and y_test must have the same length.")
117
+
118
+ if task == "classification":
119
+ train_correct = sum(
120
+ prediction == target
121
+ for prediction, target in zip(
122
+ train_predicted,
123
+ train_actual,
124
+ )
125
+ )
126
+
127
+ test_correct = sum(
128
+ prediction == target
129
+ for prediction, target in zip(
130
+ test_predicted,
131
+ test_actual,
132
+ )
133
+ )
134
+
135
+ train_metric = train_correct / len(train_actual)
136
+ test_metric = test_correct / len(test_actual)
137
+
138
+ metric_name = "accuracy"
139
+
140
+ else:
141
+ train_mean = sum(train_actual) / len(train_actual)
142
+ test_mean = sum(test_actual) / len(test_actual)
143
+
144
+ train_total = sum((target - train_mean) ** 2 for target in train_actual)
145
+
146
+ test_total = sum((target - test_mean) ** 2 for target in test_actual)
147
+
148
+ if train_total == 0 or test_total == 0:
149
+ raise ValueError(
150
+ "R² cannot be calculated when target variance is zero."
151
+ )
152
+
153
+ train_residual = sum(
154
+ (target - prediction) ** 2
155
+ for target, prediction in zip(
156
+ train_actual,
157
+ train_predicted,
158
+ )
159
+ )
160
+
161
+ test_residual = sum(
162
+ (target - prediction) ** 2
163
+ for target, prediction in zip(
164
+ test_actual,
165
+ test_predicted,
166
+ )
167
+ )
168
+
169
+ train_metric = 1 - train_residual / train_total
170
+ test_metric = 1 - test_residual / test_total
171
+
172
+ metric_name = "r2"
173
+
174
+ generalization_gap = train_metric - test_metric
175
+
176
+ passed = generalization_gap <= threshold
177
+
178
+ return CheckResult(
179
+ check_id="model.generalization_gap",
180
+ title="Generalization Gap",
181
+ category="performance",
182
+ passed=passed,
183
+ severity="info" if passed else "warning",
184
+ score=100.0 if passed else 0.0,
185
+ message=(
186
+ f"Generalization gap calculated successfully: {generalization_gap:.4f}."
187
+ ),
188
+ evidence={
189
+ "task_type": task,
190
+ "metric": metric_name,
191
+ "train_metric": round(float(train_metric), 4),
192
+ "test_metric": round(float(test_metric), 4),
193
+ "generalization_gap": round(
194
+ float(generalization_gap),
195
+ 4,
196
+ ),
197
+ "threshold": threshold,
198
+ },
199
+ recommendation=(
200
+ ""
201
+ if passed
202
+ else "Investigate possible overfitting or "
203
+ "distribution differences between training and test data."
204
+ ),
205
+ )
206
+
207
+ except Exception as exc: # noqa: BLE001 - Convert evaluation failures to check results.
208
+ return CheckResult(
209
+ check_id="model.generalization_gap",
210
+ title="Generalization Gap",
211
+ category="performance",
212
+ passed=False,
213
+ severity="critical",
214
+ score=0.0,
215
+ message=f"Generalization gap calculation failed: {exc}",
216
+ recommendation=("Verify that the model and training/test data are valid."),
217
+ )
@@ -0,0 +1,103 @@
1
+ from typing import Any
2
+
3
+ from ml_reliability.core.result import CheckResult
4
+
5
+
6
+ def check_missing_values(X: Any) -> CheckResult:
7
+ """
8
+ Check a dataset for missing values.
9
+
10
+ Returns a CheckResult containing:
11
+ - total missing values
12
+ - affected columns
13
+ - missing percentage
14
+ """
15
+
16
+ if X is None:
17
+ return CheckResult(
18
+ check_id="data.missing_values",
19
+ title="Missing Values",
20
+ category="data_quality",
21
+ passed=False,
22
+ severity="critical",
23
+ score=0.0,
24
+ message="Input dataset is missing.",
25
+ recommendation="Provide a valid dataset before running reliability checks.",
26
+ )
27
+
28
+ # Pandas DataFrame support
29
+ if not hasattr(X, "isna") or not hasattr(X, "columns"):
30
+ return CheckResult(
31
+ check_id="data.missing_values",
32
+ title="Missing Values",
33
+ category="data_quality",
34
+ passed=False,
35
+ severity="critical",
36
+ score=0.0,
37
+ message="Input must be a pandas DataFrame.",
38
+ recommendation="Provide the dataset as a pandas DataFrame.",
39
+ )
40
+
41
+ missing_by_column = X.isna().sum()
42
+ missing_by_column = missing_by_column[missing_by_column > 0]
43
+
44
+ total_missing = int(missing_by_column.sum())
45
+ total_cells = int(X.shape[0] * X.shape[1])
46
+
47
+ if total_cells == 0:
48
+ missing_percentage = 0.0
49
+ else:
50
+ missing_percentage = (total_missing / total_cells) * 100
51
+
52
+ if total_missing == 0:
53
+ return CheckResult(
54
+ check_id="data.missing_values",
55
+ title="Missing Values",
56
+ category="data_quality",
57
+ passed=True,
58
+ severity="info",
59
+ score=100.0,
60
+ message="No missing values were detected.",
61
+ evidence={
62
+ "total_missing": 0,
63
+ "missing_percentage": 0.0,
64
+ "affected_columns": {},
65
+ },
66
+ )
67
+
68
+ affected_columns = {
69
+ str(column): int(count) for column, count in missing_by_column.items()
70
+ }
71
+
72
+ # Initial severity thresholds
73
+ if missing_percentage >= 20:
74
+ severity = "critical"
75
+ score = 0.0
76
+ elif missing_percentage >= 5:
77
+ severity = "warning"
78
+ score = 50.0
79
+ else:
80
+ severity = "info"
81
+ score = 80.0
82
+
83
+ return CheckResult(
84
+ check_id="data.missing_values",
85
+ title="Missing Values",
86
+ category="data_quality",
87
+ passed=False,
88
+ severity=severity,
89
+ score=score,
90
+ message=(
91
+ f"Detected {total_missing} missing values "
92
+ f"({missing_percentage:.2f}% of all cells)."
93
+ ),
94
+ evidence={
95
+ "total_missing": total_missing,
96
+ "missing_percentage": round(missing_percentage, 2),
97
+ "affected_columns": affected_columns,
98
+ },
99
+ recommendation=(
100
+ "Investigate affected columns and apply an appropriate "
101
+ "missing-value handling strategy."
102
+ ),
103
+ )
@@ -0,0 +1,187 @@
1
+ import math
2
+ from typing import Any
3
+
4
+ from ml_reliability.core.result import CheckResult
5
+
6
+
7
+ def check_model_performance(
8
+ model: Any,
9
+ X_test: Any,
10
+ y_test: Any,
11
+ task: str,
12
+ ) -> CheckResult:
13
+ """
14
+ Evaluate basic model performance on test data.
15
+
16
+ Supported tasks:
17
+ - classification: accuracy
18
+ - regression: R²
19
+
20
+ The reliability score is based on the calculated metric:
21
+ - Classification: accuracy * 100
22
+ - Regression: R² * 100, bounded between 0 and 100
23
+
24
+ A successful calculation does not necessarily mean the model
25
+ has acceptable performance. Check the score and metric evidence.
26
+ """
27
+
28
+ check_id = "model.performance"
29
+ title = "Model Performance"
30
+ category = "performance"
31
+
32
+ if model is None:
33
+ return CheckResult(
34
+ check_id=check_id,
35
+ title=title,
36
+ category=category,
37
+ passed=False,
38
+ severity="critical",
39
+ score=0.0,
40
+ message="A trained model is required.",
41
+ recommendation="Provide a trained model with a predict() method.",
42
+ )
43
+
44
+ if X_test is None or y_test is None:
45
+ return CheckResult(
46
+ check_id=check_id,
47
+ title=title,
48
+ category=category,
49
+ passed=False,
50
+ severity="critical",
51
+ score=0.0,
52
+ message="X_test and y_test are required.",
53
+ recommendation="Provide both X_test and y_test.",
54
+ )
55
+
56
+ if task not in {"classification", "regression"}:
57
+ return CheckResult(
58
+ check_id=check_id,
59
+ title=title,
60
+ category=category,
61
+ passed=False,
62
+ severity="critical",
63
+ score=0.0,
64
+ message=f"Unsupported task type: {task}.",
65
+ recommendation=("Use task='classification' or task='regression'."),
66
+ )
67
+
68
+ if not callable(getattr(model, "predict", None)):
69
+ return CheckResult(
70
+ check_id=check_id,
71
+ title=title,
72
+ category=category,
73
+ passed=False,
74
+ severity="critical",
75
+ score=0.0,
76
+ message="The model must provide a callable predict() method.",
77
+ recommendation="Provide a trained model implementing predict().",
78
+ )
79
+
80
+ try:
81
+ predictions = model.predict(X_test)
82
+ except Exception as exc: # noqa: BLE001 - Convert evaluation failures to check results.
83
+ return CheckResult(
84
+ check_id=check_id,
85
+ title=title,
86
+ category=category,
87
+ passed=False,
88
+ severity="critical",
89
+ score=0.0,
90
+ message=f"Model prediction failed: {exc}",
91
+ recommendation=(
92
+ "Verify that the model is trained and compatible with X_test."
93
+ ),
94
+ )
95
+
96
+ try:
97
+ actual = list(y_test)
98
+ predicted = list(predictions)
99
+
100
+ if not actual:
101
+ raise ValueError("y_test must contain at least one sample.")
102
+
103
+ if len(actual) != len(predicted):
104
+ raise ValueError("Predictions and y_test must have the same length.")
105
+
106
+ def contains_non_finite_numeric_value(values: list[Any]) -> bool:
107
+ for value in values:
108
+ if isinstance(value, (int, float)):
109
+ if not math.isfinite(value):
110
+ return True
111
+ elif hasattr(value, "item"):
112
+ # Handle NumPy scalar values without rejecting strings.
113
+ scalar = value.item()
114
+ if isinstance(scalar, (int, float)) and not math.isfinite(scalar):
115
+ return True
116
+ return False
117
+
118
+ if contains_non_finite_numeric_value(
119
+ predicted
120
+ ) or contains_non_finite_numeric_value(actual):
121
+ raise ValueError(
122
+ "Predictions and targets must not contain non-finite numeric values."
123
+ )
124
+
125
+ if task == "classification":
126
+ correct = sum(
127
+ prediction == target for prediction, target in zip(predicted, actual)
128
+ )
129
+
130
+ metric_name = "accuracy"
131
+ metric_value = correct / len(actual)
132
+
133
+ else:
134
+ mean_actual = sum(actual) / len(actual)
135
+
136
+ total_sum_squares = sum((target - mean_actual) ** 2 for target in actual)
137
+
138
+ if total_sum_squares == 0:
139
+ raise ValueError(
140
+ "R² cannot be calculated when y_test has zero variance."
141
+ )
142
+
143
+ residual_sum_squares = sum(
144
+ (target - prediction) ** 2
145
+ for target, prediction in zip(actual, predicted)
146
+ )
147
+
148
+ metric_name = "r2"
149
+ metric_value = 1 - residual_sum_squares / total_sum_squares
150
+
151
+ score = max(
152
+ 0.0,
153
+ min(100.0, metric_value * 100.0),
154
+ )
155
+
156
+ except Exception as exc: # noqa: BLE001 - Convert evaluation failures to check results.
157
+ return CheckResult(
158
+ check_id=check_id,
159
+ title=title,
160
+ category=category,
161
+ passed=False,
162
+ severity="critical",
163
+ score=0.0,
164
+ message=f"Performance calculation failed: {exc}",
165
+ recommendation=("Verify that predictions and test targets are valid."),
166
+ )
167
+
168
+ return CheckResult(
169
+ check_id=check_id,
170
+ title=title,
171
+ category=category,
172
+ passed=True,
173
+ severity="info",
174
+ score=round(float(score), 2),
175
+ message=(
176
+ f"Model {metric_name} calculated successfully: "
177
+ f"{metric_value:.4f}. "
178
+ f"Reliability score: {score:.2f}/100."
179
+ ),
180
+ evidence={
181
+ "task_type": task,
182
+ "metric": metric_name,
183
+ "metric_value": round(float(metric_value), 4),
184
+ "test_samples": len(actual),
185
+ },
186
+ recommendation="",
187
+ )