erroranalysis 0.5.4__tar.gz → 0.6.0__tar.gz

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.
Files changed (58) hide show
  1. {erroranalysis-0.5.4/erroranalysis.egg-info → erroranalysis-0.6.0}/PKG-INFO +17 -8
  2. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/README.md +1 -1
  3. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/_internal/cohort_filter.py +142 -66
  4. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/_internal/matrix_filter.py +1000 -899
  5. erroranalysis-0.6.0/erroranalysis/_internal/metrics.py +681 -0
  6. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/_internal/surrogate_error_tree.py +1214 -1080
  7. erroranalysis-0.6.0/erroranalysis/_internal/utils.py +633 -0
  8. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/analyzer/error_analyzer.py +33 -6
  9. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/error_correlation_methods/gbm.py +2 -1
  10. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/version.py +2 -2
  11. {erroranalysis-0.5.4 → erroranalysis-0.6.0/erroranalysis.egg-info}/PKG-INFO +17 -8
  12. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis.egg-info/SOURCES.txt +15 -1
  13. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis.egg-info/requires.txt +2 -2
  14. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/requirements.txt +2 -2
  15. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/setup.py +1 -3
  16. erroranalysis-0.6.0/tests/test_binary_heatmap_inferred_classes.py +67 -0
  17. erroranalysis-0.6.0/tests/test_binary_heatmap_two_features.py +76 -0
  18. erroranalysis-0.6.0/tests/test_binary_metric_semantics.py +152 -0
  19. erroranalysis-0.6.0/tests/test_binary_metric_views.py +86 -0
  20. erroranalysis-0.6.0/tests/test_binary_tree_inferred_classes.py +121 -0
  21. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/tests/test_importances.py +21 -0
  22. erroranalysis-0.6.0/tests/test_integer_mae_views.py +138 -0
  23. erroranalysis-0.6.0/tests/test_integer_residual_boundaries.py +72 -0
  24. erroranalysis-0.6.0/tests/test_metric_consistency_followup.py +3362 -0
  25. erroranalysis-0.6.0/tests/test_optional_spark_import.py +20 -0
  26. erroranalysis-0.6.0/tests/test_prediction_list_residuals.py +21 -0
  27. erroranalysis-0.6.0/tests/test_prediction_residuals.py +56 -0
  28. erroranalysis-0.6.0/tests/test_pyspark_surrogate_error_tree.py +86 -0
  29. erroranalysis-0.6.0/tests/test_review_followup_regressions.py +108 -0
  30. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/tests/test_root_stats.py +2 -2
  31. erroranalysis-0.6.0/tests/test_root_stats_regressions.py +97 -0
  32. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/tests/test_surrogate_error_tree.py +108 -4
  33. erroranalysis-0.6.0/tests/test_tree_mean_prediction.py +41 -0
  34. erroranalysis-0.5.4/erroranalysis/_internal/metrics.py +0 -280
  35. erroranalysis-0.5.4/erroranalysis/_internal/utils.py +0 -53
  36. erroranalysis-0.5.4/tests/test_pyspark_surrogate_error_tree.py +0 -59
  37. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/LICENSE +0 -0
  38. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/MANIFEST.in +0 -0
  39. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/__init__.py +0 -0
  40. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/_internal/__init__.py +0 -0
  41. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/_internal/constants.py +0 -0
  42. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/_internal/error_analyzer/__init__.py +0 -0
  43. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/_internal/error_report/__init__.py +0 -0
  44. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/_internal/process_categoricals.py +0 -0
  45. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/_internal/version_checker.py +0 -0
  46. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/analyzer/__init__.py +0 -0
  47. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/error_correlation_methods/__init__.py +0 -0
  48. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/error_correlation_methods/ebm.py +0 -0
  49. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/report/__init__.py +0 -0
  50. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis/report/error_report.py +0 -0
  51. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis.egg-info/dependency_links.txt +0 -0
  52. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/erroranalysis.egg-info/top_level.txt +0 -0
  53. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/requirements-object-detection.txt +0 -0
  54. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/setup.cfg +0 -0
  55. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/tests/test_cohort_filter.py +0 -0
  56. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/tests/test_error_report.py +0 -0
  57. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/tests/test_matrix_filter.py +0 -0
  58. {erroranalysis-0.5.4 → erroranalysis-0.6.0}/tests/test_metrics.py +0 -0
@@ -1,33 +1,42 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: erroranalysis
3
- Version: 0.5.4
3
+ Version: 0.6.0
4
4
  Summary: Core error analysis APIs
5
5
  Home-page: https://github.com/microsoft/responsible-ai-widgets
6
6
  Author: Roman Lutz, Ilya Matiach, Ke Xu
7
7
  Author-email: raiwidgets-maintain@microsoft.com
8
- Classifier: Programming Language :: Python :: 3.7
9
- Classifier: Programming Language :: Python :: 3.8
10
8
  Classifier: Programming Language :: Python :: 3.9
11
9
  Classifier: Programming Language :: Python :: 3.10
12
10
  Classifier: Programming Language :: Python :: 3.11
13
11
  Classifier: License :: OSI Approved :: MIT License
14
12
  Classifier: Operating System :: OS Independent
15
13
  Classifier: Development Status :: 3 - Alpha
16
- Requires-Python: >=3.7
14
+ Requires-Python: >=3.9
17
15
  Description-Content-Type: text/markdown
18
16
  License-File: LICENSE
19
- Requires-Dist: numpy>=1.17.2
20
- Requires-Dist: pandas<2.0.0,>=0.25.1
17
+ Requires-Dist: numpy<3,>=1.17.2
18
+ Requires-Dist: pandas<3,>=0.25.1
21
19
  Requires-Dist: scipy>=1.4.1
22
20
  Requires-Dist: scikit-learn>=0.22.1
23
21
  Requires-Dist: lightgbm>=2.0.11
24
22
  Requires-Dist: raiutils>=0.4.2
25
23
  Provides-Extra: object-detection
26
24
  Requires-Dist: vision_explanation_methods; extra == "object-detection"
25
+ Dynamic: author
26
+ Dynamic: author-email
27
+ Dynamic: classifier
28
+ Dynamic: description
29
+ Dynamic: description-content-type
30
+ Dynamic: home-page
31
+ Dynamic: license-file
32
+ Dynamic: provides-extra
33
+ Dynamic: requires-dist
34
+ Dynamic: requires-python
35
+ Dynamic: summary
27
36
 
28
37
  # Error Analysis SDK for Python
29
38
 
30
- ### This package has been tested with Python 3.7, 3.8, 3.9, 3.10 and 3.11
39
+ ### This package has been tested with Python 3.9, 3.10 and 3.11
31
40
 
32
41
  The error analysis sdk enables users to get a deeper understanding of machine learning model errors. When evaluating a machine learning model, aggregate accuracy is not sufficient and single-score evaluation may hide important conditions of inaccuracies. Use Error Analysis to identify cohorts with higher error rates and diagnose the root causes behind these errors.
33
42
 
@@ -1,6 +1,6 @@
1
1
  # Error Analysis SDK for Python
2
2
 
3
- ### This package has been tested with Python 3.7, 3.8, 3.9, 3.10 and 3.11
3
+ ### This package has been tested with Python 3.9, 3.10 and 3.11
4
4
 
5
5
  The error analysis sdk enables users to get a deeper understanding of machine learning model errors. When evaluating a machine learning model, aggregate accuracy is not sufficient and single-score evaluation may hide important conditions of inaccuracies. Use Error Analysis to identify cohorts with higher error rates and diagnose the root causes behind these errors.
6
6
 
@@ -11,14 +11,60 @@ from erroranalysis._internal.constants import (ARG, COLUMN, COMPOSITE_FILTERS,
11
11
  ROW_INDEX, TRUE_Y,
12
12
  CohortFilterMethods,
13
13
  CohortFilterOps, ModelTask)
14
- from erroranalysis._internal.metrics import get_ordered_classes
15
- from erroranalysis._internal.utils import is_spark
14
+ from erroranalysis._internal.metrics import (encode_classification_labels,
15
+ get_ordered_classes,
16
+ validate_ordered_classes)
17
+ from erroranalysis._internal.utils import (compute_classification_diff,
18
+ compute_regression_residuals,
19
+ is_spark,
20
+ normalize_multioutput_array,
21
+ normalize_single_output_array,
22
+ object_label_series)
16
23
 
17
24
  MODEL = 'model'
18
25
  CLASSIFICATION_OUTCOME = 'Classification outcome'
19
26
  REGRESSION_ERROR = 'Regression error'
20
27
 
21
28
 
29
+ def _get_object_detection_values(values):
30
+ """Return one object cell per sample for nested labels."""
31
+ if isinstance(values, pd.DataFrame):
32
+ if values.shape[1] != 1:
33
+ raise ValueError(
34
+ "Object detection labels must have exactly one column")
35
+ values = values.iloc[:, 0]
36
+ elif isinstance(values, np.ndarray) and \
37
+ values.ndim == 2 and values.shape[1] == 1:
38
+ values = values[:, 0]
39
+ result = np.empty(len(values), dtype=object)
40
+ result[:] = list(values)
41
+ return result
42
+
43
+
44
+ def _get_cohort_values(values):
45
+ """Return scalar labels or one object cell per multi-output row."""
46
+ multioutput = normalize_multioutput_array(values)
47
+ if multioutput is not None:
48
+ result = np.empty(len(multioutput), dtype=object)
49
+ result[:] = list(multioutput)
50
+ return result
51
+ normalized = normalize_single_output_array(values)
52
+ if normalized.ndim == 2 and normalized.shape[1] > 1:
53
+ result = np.empty(len(normalized), dtype=object)
54
+ result[:] = list(normalized)
55
+ return result
56
+ return normalized
57
+
58
+
59
+ def _assign_label_column(df, column, values):
60
+ """Assign object labels without pandas temporal or numeric inference."""
61
+ if (isinstance(values, np.ndarray) and
62
+ values.dtype.kind in ("M", "O", "m")):
63
+ df[column] = object_label_series(values, index=df.index)
64
+ else:
65
+ df[column] = values
66
+
67
+
22
68
  def filter_from_cohort(analyzer, filters, composite_filters,
23
69
  include_original_columns_only=False):
24
70
  """Filters the dataset on the analyzer based on the specified filters.
@@ -124,7 +170,7 @@ class FilterDataWithCohortFilters:
124
170
  # Note: we make a non-deep copy of the input DataFrame since
125
171
  # we will add columns below
126
172
  df = df.copy()
127
- self._add_filter_cols(df, filters)
173
+ self._add_filter_cols(df, (filters or []) + (composite_filters or []))
128
174
  df = self._apply_recursive_filter(
129
175
  df, filters, self.categorical_features, self.categories)
130
176
  df = self._apply_recursive_filter(
@@ -148,11 +194,7 @@ class FilterDataWithCohortFilters:
148
194
  if (model_task == ModelTask.CLASSIFICATION or
149
195
  model_task == ModelTask.OBJECT_DETECTION) \
150
196
  and filters:
151
- for filter in filters:
152
- if COLUMN in filter:
153
- column = filter[COLUMN]
154
- if column == CLASSIFICATION_OUTCOME:
155
- return True
197
+ return self._filters_has_column(filters, CLASSIFICATION_OUTCOME)
156
198
  return False
157
199
 
158
200
  def _filters_has_regression_error(self, filters):
@@ -169,11 +211,17 @@ class FilterDataWithCohortFilters:
169
211
  # is included in filters, and if it is then
170
212
  # compute the necessary column data on the fly
171
213
  if model_task == ModelTask.REGRESSION and filters:
172
- for filter in filters:
173
- if COLUMN in filter:
174
- column = filter[COLUMN]
175
- if column == REGRESSION_ERROR:
176
- return True
214
+ return self._filters_has_column(filters, REGRESSION_ERROR)
215
+ return False
216
+
217
+ def _filters_has_column(self, filters, column):
218
+ """Find a column in flat filters or nested composite filters."""
219
+ for filter in filters:
220
+ if filter.get(COLUMN) == column:
221
+ return True
222
+ if self._filters_has_column(
223
+ filter.get(COMPOSITE_FILTERS, []), column):
224
+ return True
177
225
  return False
178
226
 
179
227
  def _compute_binary_classification_outcome_data(self, true_y, pred_y,
@@ -181,6 +229,10 @@ class FilterDataWithCohortFilters:
181
229
  """Creates the classification outcome data for binary
182
230
  classification scenario.
183
231
 
232
+ The second class is positive. Outcome codes match the dashboard:
233
+ 0 is true negative, 1 false positive, 2 false negative,
234
+ and 3 true positive.
235
+
184
236
  :param true_y: The true labels.
185
237
  :type true_y: list or numpy.ndarray
186
238
  :param pred_y: The predicted labels.
@@ -190,29 +242,15 @@ class FilterDataWithCohortFilters:
190
242
  :return: The classification outcome data.
191
243
  :rtype: list
192
244
  """
193
- classification_outcome = []
194
- if not isinstance(pred_y, np.ndarray):
195
- pred_y = np.array(pred_y)
196
-
197
- if not isinstance(true_y, np.ndarray):
198
- true_y = np.array(true_y)
199
-
200
- for i in range(len(true_y)):
201
- if true_y[i] == pred_y[i]:
202
- if true_y[i] == classes[0]:
203
- # True positive == 0
204
- classification_outcome.append(0)
205
- else:
206
- # True negative == 3
207
- classification_outcome.append(3)
208
- else:
209
- if true_y[i] == classes[0]:
210
- # False negative == 2
211
- classification_outcome.append(2)
212
- else:
213
- # False positive == 1
214
- classification_outcome.append(1)
215
- return classification_outcome
245
+ true_codes = encode_classification_labels(
246
+ classes, true_y, allow_unknown=True)[0]
247
+ matches = ~compute_classification_diff(pred_y, true_y)
248
+ true_negative_class = true_codes == 0
249
+ return np.where(
250
+ matches,
251
+ np.where(true_negative_class, 0, 3),
252
+ np.where(true_negative_class, 1, 2),
253
+ ).tolist()
216
254
 
217
255
  def _compute_multiclass_classification_outcome_data(
218
256
  self, true_y, pred_y):
@@ -226,22 +264,7 @@ class FilterDataWithCohortFilters:
226
264
  :return: The classification outcome data.
227
265
  :rtype: list
228
266
  """
229
- classification_outcome = []
230
- if not isinstance(pred_y, np.ndarray):
231
- pred_y = np.array(pred_y)
232
-
233
- if not isinstance(true_y, np.ndarray):
234
- true_y = np.array(true_y)
235
-
236
- for i in range(len(true_y)):
237
- if true_y[i] == pred_y[i]:
238
- # Correct prediction == 0
239
- classification_outcome.append(0)
240
- else:
241
- # Incorrect prediction == 1
242
- classification_outcome.append(1)
243
-
244
- return classification_outcome
267
+ return compute_classification_diff(pred_y, true_y).astype(int).tolist()
245
268
 
246
269
  def _compute_regression_error_data(self, true_y, pred_y):
247
270
  """Computes the per instance absolute difference
@@ -254,16 +277,7 @@ class FilterDataWithCohortFilters:
254
277
  :return: The regression error data.
255
278
  :rtype: list
256
279
  """
257
- regression_error = []
258
- if not isinstance(pred_y, np.ndarray):
259
- pred_y = np.array(pred_y)
260
-
261
- if not isinstance(true_y, np.ndarray):
262
- true_y = np.array(true_y)
263
-
264
- for i in range(len(true_y)):
265
- regression_error.append(abs(true_y[i] - pred_y[i]))
266
- return regression_error
280
+ return np.abs(compute_regression_residuals(pred_y, true_y)).tolist()
267
281
 
268
282
  def _add_filter_cols(self, df, filters):
269
283
  """Adds special columns to the dataset for filtering
@@ -277,17 +291,39 @@ class FilterDataWithCohortFilters:
277
291
  has_classification_outcome = self._filters_has_classification_outcome(
278
292
  filters)
279
293
  has_regression_error = self._filters_has_regression_error(filters)
294
+ if self.model is None and self.pred_y is None and (
295
+ has_classification_outcome or has_regression_error):
296
+ raise ValueError(
297
+ "A model or predicted labels are required to filter by "
298
+ "classification outcome or regression error.")
280
299
 
281
300
  if isinstance(self.true_y, str):
282
301
  df.rename(columns={self.true_y: TRUE_Y})
302
+ elif self.model_task in (
303
+ ModelTask.OBJECT_DETECTION,
304
+ ModelTask.MULTILABEL_IMAGE_CLASSIFICATION):
305
+ _assign_label_column(
306
+ df, TRUE_Y, _get_object_detection_values(self.true_y))
283
307
  else:
284
- df[TRUE_Y] = self.true_y
308
+ _assign_label_column(
309
+ df, TRUE_Y, _get_cohort_values(self.true_y))
285
310
 
286
311
  if self.model is None:
287
- df[PRED_Y] = self.pred_y
312
+ if self.pred_y is not None and self.model_task in (
313
+ ModelTask.OBJECT_DETECTION,
314
+ ModelTask.MULTILABEL_IMAGE_CLASSIFICATION):
315
+ _assign_label_column(
316
+ df, PRED_Y,
317
+ _get_object_detection_values(self.pred_y))
318
+ elif self.pred_y is not None:
319
+ _assign_label_column(
320
+ df, PRED_Y, _get_cohort_values(self.pred_y))
288
321
 
289
322
  if not is_spark(df):
290
- df[ROW_INDEX] = np.arange(0, len(self.true_y))
323
+ row_count = (self.true_y.shape[0]
324
+ if hasattr(self.true_y, "shape")
325
+ else len(self.true_y))
326
+ df[ROW_INDEX] = np.arange(0, row_count)
291
327
  if has_classification_outcome:
292
328
  if PRED_Y in df:
293
329
  pred_y = df[PRED_Y]
@@ -296,11 +332,51 @@ class FilterDataWithCohortFilters:
296
332
  pred_y = self.model.predict(
297
333
  df.drop(columns=[TRUE_Y, ROW_INDEX]))
298
334
 
335
+ multi_true_y = normalize_multioutput_array(self.true_y)
336
+ multi_pred_y = normalize_multioutput_array(pred_y)
337
+ if multi_true_y is not None and multi_pred_y is not None:
338
+ df[CLASSIFICATION_OUTCOME] = \
339
+ self._compute_multiclass_classification_outcome_data(
340
+ multi_true_y, multi_pred_y)
341
+ return
342
+
343
+ model_classes = getattr(self.model, "classes_", None)
344
+ declared_classes = self.classes
345
+ if declared_classes is None and model_classes is not None:
346
+ model_order = get_ordered_classes(
347
+ model_classes, self.true_y, pred_y)
348
+ if model_order is model_classes and len(model_classes) == 2:
349
+ declared_classes = model_classes
350
+ true_labels = normalize_single_output_array(self.true_y).ravel()
351
+ nonmissing_true_labels = [
352
+ label for label in true_labels
353
+ if not (
354
+ np.asarray(pd.isna(label)).ndim == 0 and
355
+ bool(pd.isna(label)))
356
+ ]
299
357
  classes = get_ordered_classes(
300
- self.classes, self.true_y, pred_y)
358
+ declared_classes, nonmissing_true_labels,
359
+ nonmissing_true_labels)
360
+ if declared_classes is None or classes is not declared_classes:
361
+ if model_classes is not None and declared_classes is not None:
362
+ model_order = get_ordered_classes(
363
+ model_classes, self.true_y, pred_y)
364
+ if model_order is model_classes and \
365
+ len(model_classes) == 2:
366
+ classes = model_classes
367
+ else:
368
+ classes = get_ordered_classes(
369
+ None, self.true_y, pred_y)
370
+ else:
371
+ classes = get_ordered_classes(
372
+ None, self.true_y, pred_y)
301
373
 
302
374
  # calculate classification outcome and add to df
303
375
  if len(classes) == 2:
376
+ validate_ordered_classes(
377
+ classes, nonmissing_true_labels,
378
+ nonmissing_true_labels,
379
+ require_two_classes=True)
304
380
  df[CLASSIFICATION_OUTCOME] = \
305
381
  self._compute_binary_classification_outcome_data(
306
382
  self.true_y, pred_y, classes)