erroranalysis 0.4.2__tar.gz → 0.4.4__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.
- {erroranalysis-0.4.2/erroranalysis.egg-info → erroranalysis-0.4.4}/PKG-INFO +3 -2
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/README.md +1 -1
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/_internal/constants.py +13 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/_internal/surrogate_error_tree.py +4 -5
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/analyzer/error_analyzer.py +36 -17
- erroranalysis-0.4.4/erroranalysis/error_correlation_methods/__init__.py +9 -0
- erroranalysis-0.4.4/erroranalysis/error_correlation_methods/ebm.py +39 -0
- erroranalysis-0.4.4/erroranalysis/error_correlation_methods/gbm.py +39 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/version.py +1 -1
- {erroranalysis-0.4.2 → erroranalysis-0.4.4/erroranalysis.egg-info}/PKG-INFO +3 -2
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis.egg-info/SOURCES.txt +3 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis.egg-info/requires.txt +1 -1
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/requirements.txt +1 -1
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/setup.py +1 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/tests/test_cohort_filter.py +42 -41
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/tests/test_error_report.py +19 -9
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/tests/test_importances.py +53 -21
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/tests/test_matrix_filter.py +27 -26
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/tests/test_surrogate_error_tree.py +13 -1
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/LICENSE +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/MANIFEST.in +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/__init__.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/_internal/__init__.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/_internal/cohort_filter.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/_internal/error_analyzer/__init__.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/_internal/error_report/__init__.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/_internal/matrix_filter.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/_internal/metrics.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/_internal/process_categoricals.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/_internal/utils.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/_internal/version_checker.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/analyzer/__init__.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/report/__init__.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/report/error_report.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis.egg-info/dependency_links.txt +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis.egg-info/top_level.txt +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/setup.cfg +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/tests/test_metrics.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/tests/test_pyspark_surrogate_error_tree.py +0 -0
- {erroranalysis-0.4.2 → erroranalysis-0.4.4}/tests/test_root_stats.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: erroranalysis
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
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
|
|
@@ -9,6 +9,7 @@ Classifier: Programming Language :: Python :: 3.6
|
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.7
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.8
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
14
|
Classifier: Operating System :: OS Independent
|
|
14
15
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -18,7 +19,7 @@ License-File: LICENSE
|
|
|
18
19
|
|
|
19
20
|
# Error Analysis SDK for Python
|
|
20
21
|
|
|
21
|
-
### This package has been tested with Python 3.6, 3.7, 3.8 and 3.
|
|
22
|
+
### This package has been tested with Python 3.6, 3.7, 3.8, 3.9 and 3.10
|
|
22
23
|
|
|
23
24
|
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.
|
|
24
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Error Analysis SDK for Python
|
|
2
2
|
|
|
3
|
-
### This package has been tested with Python 3.6, 3.7, 3.8 and 3.
|
|
3
|
+
### This package has been tested with Python 3.6, 3.7, 3.8, 3.9 and 3.10
|
|
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
|
|
|
@@ -96,6 +96,9 @@ class Metrics(str, Enum):
|
|
|
96
96
|
FALSE_POSITIVE_RATE = 'false_positive_rate'
|
|
97
97
|
FALSE_NEGATIVE_RATE = 'false_negative_rate'
|
|
98
98
|
SELECTION_RATE = 'selection_rate'
|
|
99
|
+
MEAN_AVERAGE_PRECISION = 'mean_average_precision'
|
|
100
|
+
AVERAGE_PRECISION = 'average_precision'
|
|
101
|
+
AVERAGE_RECALL = 'average_recall'
|
|
99
102
|
|
|
100
103
|
|
|
101
104
|
class MetricKeys(str, Enum):
|
|
@@ -121,6 +124,16 @@ class TreeNode(str, Enum):
|
|
|
121
124
|
METRIC_VALUE = MetricKeys.METRIC_VALUE.value
|
|
122
125
|
|
|
123
126
|
|
|
127
|
+
class ErrorCorrelationMethods(str, Enum):
|
|
128
|
+
"""Provide the supported error correlation methods.
|
|
129
|
+
|
|
130
|
+
The supported methods are 'mutual_info', 'ebm' and 'gbm_shap'.
|
|
131
|
+
"""
|
|
132
|
+
MUTUAL_INFO = 'mutual_info'
|
|
133
|
+
EBM = 'ebm'
|
|
134
|
+
GBM_SHAP = 'gbm_shap'
|
|
135
|
+
|
|
136
|
+
|
|
124
137
|
metric_to_display_name = {
|
|
125
138
|
Metrics.ACCURACY_SCORE: 'Accuracy score',
|
|
126
139
|
Metrics.MEAN_PREDICTION: 'Mean prediction',
|
|
@@ -292,7 +292,6 @@ def get_surrogate_booster_local(filtered_df, analyzer, is_model_analyzer,
|
|
|
292
292
|
scored dataset.
|
|
293
293
|
:rtype: (Booster, pandas.DataFrame, (list[str], list[int]))
|
|
294
294
|
"""
|
|
295
|
-
row_index = filtered_df[ROW_INDEX]
|
|
296
295
|
true_y = filtered_df[TRUE_Y]
|
|
297
296
|
dropped_cols = [TRUE_Y, ROW_INDEX]
|
|
298
297
|
if not is_model_analyzer:
|
|
@@ -303,7 +302,7 @@ def get_surrogate_booster_local(filtered_df, analyzer, is_model_analyzer,
|
|
|
303
302
|
if is_pandas:
|
|
304
303
|
true_y = true_y.to_numpy()
|
|
305
304
|
else:
|
|
306
|
-
input_data = input_data.to_numpy()
|
|
305
|
+
input_data = input_data.to_numpy(copy=True)
|
|
307
306
|
if is_model_analyzer:
|
|
308
307
|
pred_y = analyzer.model.predict(input_data)
|
|
309
308
|
if analyzer.model_task == ModelTask.CLASSIFICATION:
|
|
@@ -317,7 +316,7 @@ def get_surrogate_booster_local(filtered_df, analyzer, is_model_analyzer,
|
|
|
317
316
|
if not isinstance(true_y, np.ndarray):
|
|
318
317
|
true_y = np.array(true_y)
|
|
319
318
|
if is_pandas:
|
|
320
|
-
input_data = input_data.to_numpy()
|
|
319
|
+
input_data = input_data.to_numpy(copy=True)
|
|
321
320
|
|
|
322
321
|
if analyzer.categorical_features:
|
|
323
322
|
# Inplace replacement of columns
|
|
@@ -330,7 +329,7 @@ def get_surrogate_booster_local(filtered_df, analyzer, is_model_analyzer,
|
|
|
330
329
|
else:
|
|
331
330
|
string_indexed_data = analyzer.string_indexed_data
|
|
332
331
|
for idx, c_i in enumerate(analyzer.categorical_indexes):
|
|
333
|
-
input_data[:, c_i] = string_indexed_data[
|
|
332
|
+
input_data[:, c_i] = string_indexed_data[:, idx]
|
|
334
333
|
dataset_sub_features = input_data[:, indexes]
|
|
335
334
|
|
|
336
335
|
categorical_info = get_categorical_info(analyzer,
|
|
@@ -605,7 +604,7 @@ def filter_to_used_features(df, tree):
|
|
|
605
604
|
"""
|
|
606
605
|
features = tree[CACHED_SUBTREE_FEATURES]
|
|
607
606
|
features = features.union({PRED_Y, TRUE_Y, DIFF})
|
|
608
|
-
return df[features]
|
|
607
|
+
return df[list(features)]
|
|
609
608
|
|
|
610
609
|
|
|
611
610
|
def cache_subtree_features(tree, feature_names, parent=None):
|
|
@@ -10,7 +10,8 @@ import pandas as pd
|
|
|
10
10
|
from sklearn.feature_selection import (mutual_info_classif,
|
|
11
11
|
mutual_info_regression)
|
|
12
12
|
|
|
13
|
-
from erroranalysis._internal.constants import (
|
|
13
|
+
from erroranalysis._internal.constants import (ErrorCorrelationMethods,
|
|
14
|
+
MatrixParams, Metrics,
|
|
14
15
|
ModelTask, RootKeys,
|
|
15
16
|
metric_to_display_name)
|
|
16
17
|
from erroranalysis._internal.matrix_filter import \
|
|
@@ -25,11 +26,14 @@ from erroranalysis._internal.surrogate_error_tree import \
|
|
|
25
26
|
compute_error_tree_on_dataset as _compute_error_tree_on_dataset
|
|
26
27
|
from erroranalysis._internal.utils import generate_random_unique_indexes
|
|
27
28
|
from erroranalysis._internal.version_checker import check_pandas_version
|
|
29
|
+
from erroranalysis.error_correlation_methods import (
|
|
30
|
+
compute_ebm_global_importance, compute_gbm_global_importance)
|
|
28
31
|
from erroranalysis.report import ErrorReport
|
|
29
32
|
|
|
30
33
|
BIN_THRESHOLD = MatrixParams.BIN_THRESHOLD
|
|
31
34
|
IMPORTANCES_THRESHOLD = 50000
|
|
32
35
|
ROOT_COVERAGE = 100
|
|
36
|
+
MUTUAL_INFO = ErrorCorrelationMethods.MUTUAL_INFO.value
|
|
33
37
|
|
|
34
38
|
|
|
35
39
|
class BaseAnalyzer(ABC):
|
|
@@ -372,7 +376,8 @@ class BaseAnalyzer(ABC):
|
|
|
372
376
|
num_leaves=None,
|
|
373
377
|
min_child_samples=None,
|
|
374
378
|
compute_importances=False,
|
|
375
|
-
compute_root_stats=False
|
|
379
|
+
compute_root_stats=False,
|
|
380
|
+
error_correlation_method=MUTUAL_INFO):
|
|
376
381
|
"""Creates the error analysis ErrorReport.
|
|
377
382
|
|
|
378
383
|
The ErrorReport contains the importances, heatmap and tree view json.
|
|
@@ -409,7 +414,7 @@ class BaseAnalyzer(ABC):
|
|
|
409
414
|
None)
|
|
410
415
|
importances = None
|
|
411
416
|
if compute_importances:
|
|
412
|
-
importances = self.compute_importances()
|
|
417
|
+
importances = self.compute_importances(error_correlation_method)
|
|
413
418
|
root_stats = None
|
|
414
419
|
if compute_root_stats:
|
|
415
420
|
root_stats = self.compute_root_stats()
|
|
@@ -419,7 +424,7 @@ class BaseAnalyzer(ABC):
|
|
|
419
424
|
importances=importances,
|
|
420
425
|
root_stats=root_stats)
|
|
421
426
|
|
|
422
|
-
def compute_importances(self):
|
|
427
|
+
def compute_importances(self, error_correlation_method=MUTUAL_INFO):
|
|
423
428
|
"""Compute the importances or correlation between features and error.
|
|
424
429
|
|
|
425
430
|
Computes the feature importances or the correlation between
|
|
@@ -428,6 +433,8 @@ class BaseAnalyzer(ABC):
|
|
|
428
433
|
uses the scikit-learn methods mutual_info_classif for classification
|
|
429
434
|
and mutual_info_regression for regression tasks.
|
|
430
435
|
|
|
436
|
+
:param error_correlation_method: Method to compute error correlation.
|
|
437
|
+
:type error_correlation_method: str
|
|
431
438
|
:return: The computed importances or correlation between the
|
|
432
439
|
features and error.
|
|
433
440
|
:rtype: list[float]
|
|
@@ -435,7 +442,7 @@ class BaseAnalyzer(ABC):
|
|
|
435
442
|
input_data = self.dataset
|
|
436
443
|
diff = self.get_diff()
|
|
437
444
|
if isinstance(self.dataset, pd.DataFrame):
|
|
438
|
-
input_data = input_data.to_numpy()
|
|
445
|
+
input_data = input_data.to_numpy(copy=True)
|
|
439
446
|
if self.categorical_features:
|
|
440
447
|
# Inplace replacement of columns
|
|
441
448
|
indexes = self.categorical_indexes
|
|
@@ -451,24 +458,29 @@ class BaseAnalyzer(ABC):
|
|
|
451
458
|
input_data = input_data[indexes]
|
|
452
459
|
diff = diff[indexes]
|
|
453
460
|
try:
|
|
454
|
-
importances = self.
|
|
461
|
+
importances = self._compute_error_correlation(
|
|
462
|
+
input_data, diff, error_correlation_method)
|
|
455
463
|
except ValueError:
|
|
456
464
|
# Impute input_data if it contains NaNs, infinity or a value too
|
|
457
465
|
# large for dtype('float64')
|
|
458
466
|
input_data = np.nan_to_num(input_data)
|
|
459
|
-
importances = self.
|
|
467
|
+
importances = self._compute_error_correlation(
|
|
468
|
+
input_data, diff, error_correlation_method)
|
|
460
469
|
return importances
|
|
461
470
|
|
|
462
|
-
def
|
|
463
|
-
|
|
471
|
+
def _compute_error_correlation(self, input_data, diff,
|
|
472
|
+
error_correlation_method):
|
|
473
|
+
"""Compute the correlation between the features and error.
|
|
464
474
|
|
|
465
|
-
:param input_data: The input data to compute the
|
|
475
|
+
:param input_data: The input data to compute the error correlation
|
|
466
476
|
on.
|
|
467
477
|
:type input_data: numpy.ndarray
|
|
468
478
|
:param diff: The difference between the label and prediction
|
|
469
479
|
columns.
|
|
470
480
|
:type diff: numpy.ndarray
|
|
471
|
-
:
|
|
481
|
+
:param error_correlation_method: Method to compute error correlation.
|
|
482
|
+
:type error_correlation_method: str
|
|
483
|
+
:return: The computed error correlation between the features and
|
|
472
484
|
error.
|
|
473
485
|
:rtype: list[float]
|
|
474
486
|
"""
|
|
@@ -476,13 +488,20 @@ class BaseAnalyzer(ABC):
|
|
|
476
488
|
if input_data.shape[0] == 1:
|
|
477
489
|
input_data = np.concatenate((input_data, input_data))
|
|
478
490
|
diff = np.concatenate((diff, diff))
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
491
|
+
if error_correlation_method == ErrorCorrelationMethods.MUTUAL_INFO:
|
|
492
|
+
n_neighbors = min(3, input_data.shape[0] - 1)
|
|
493
|
+
if self._model_task == ModelTask.CLASSIFICATION:
|
|
494
|
+
return mutual_info_classif(
|
|
495
|
+
input_data, diff, n_neighbors=n_neighbors).tolist()
|
|
496
|
+
else:
|
|
497
|
+
return mutual_info_regression(
|
|
498
|
+
input_data, diff, n_neighbors=n_neighbors).tolist()
|
|
499
|
+
elif error_correlation_method == ErrorCorrelationMethods.EBM:
|
|
500
|
+
return compute_ebm_global_importance(
|
|
501
|
+
input_data, diff, self._model_task)
|
|
483
502
|
else:
|
|
484
|
-
return
|
|
485
|
-
input_data, diff,
|
|
503
|
+
return compute_gbm_global_importance(
|
|
504
|
+
input_data, diff, self._model_task, self._categorical_indexes)
|
|
486
505
|
|
|
487
506
|
def compute_root_stats(self):
|
|
488
507
|
"""Compute the root all data statistics.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""Module for defining error correlation methods."""
|
|
5
|
+
|
|
6
|
+
from .ebm import compute_ebm_global_importance
|
|
7
|
+
from .gbm import compute_gbm_global_importance
|
|
8
|
+
|
|
9
|
+
__all__ = ["compute_ebm_global_importance", "compute_gbm_global_importance"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""Defines the error correlation computation using an EBM."""
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from interpret.glassbox import (ExplainableBoostingClassifier,
|
|
8
|
+
ExplainableBoostingRegressor)
|
|
9
|
+
ebm_installed = True
|
|
10
|
+
except ImportError:
|
|
11
|
+
ebm_installed = False
|
|
12
|
+
|
|
13
|
+
from erroranalysis._internal.constants import ModelTask
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def compute_ebm_global_importance(input_data, diff, model_task):
|
|
17
|
+
"""Compute global importance score for EBM between the features and error.
|
|
18
|
+
:param input_data: The input data to compute the EBM global importance
|
|
19
|
+
score on.
|
|
20
|
+
:type input_data: numpy.ndarray
|
|
21
|
+
:param diff: The difference between the label and prediction
|
|
22
|
+
columns.
|
|
23
|
+
:type diff: numpy.ndarray
|
|
24
|
+
:param model_task: The model task.
|
|
25
|
+
:type model_task: str
|
|
26
|
+
:return: The computed EBM global importance score between the features and
|
|
27
|
+
error.
|
|
28
|
+
:rtype: list[float]
|
|
29
|
+
"""
|
|
30
|
+
if not ebm_installed:
|
|
31
|
+
raise ImportError('EBM is not installed. Please install it by running '
|
|
32
|
+
'"pip install interpret"')
|
|
33
|
+
if model_task == ModelTask.CLASSIFICATION:
|
|
34
|
+
model = ExplainableBoostingClassifier(interactions=0)
|
|
35
|
+
else:
|
|
36
|
+
model = ExplainableBoostingRegressor(interactions=0)
|
|
37
|
+
model.fit(input_data, diff)
|
|
38
|
+
ebm_importances = model.term_importances()
|
|
39
|
+
return ebm_importances.tolist()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Copyright (c) Microsoft Corporation
|
|
2
|
+
# Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
"""Defines the error correlation computation using LightGBM with TreeShap."""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import shap
|
|
8
|
+
from lightgbm import LGBMClassifier, LGBMRegressor
|
|
9
|
+
|
|
10
|
+
from erroranalysis._internal.constants import ModelTask
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def compute_gbm_global_importance(input_data, diff, model_task,
|
|
14
|
+
categorical_indexes):
|
|
15
|
+
"""Compute global importance score for EBM between the features and error.
|
|
16
|
+
:param input_data: The input data to compute the EBM global importance
|
|
17
|
+
score on.
|
|
18
|
+
:type input_data: numpy.ndarray
|
|
19
|
+
:param diff: The difference between the label and prediction
|
|
20
|
+
columns.
|
|
21
|
+
:type diff: numpy.ndarray
|
|
22
|
+
:param model_task: The model task.
|
|
23
|
+
:type model_task: str
|
|
24
|
+
:return: The computed EBM global importance score between the features and
|
|
25
|
+
error.
|
|
26
|
+
:rtype: list[float]
|
|
27
|
+
"""
|
|
28
|
+
is_classification = model_task == ModelTask.CLASSIFICATION
|
|
29
|
+
if is_classification:
|
|
30
|
+
model = LGBMClassifier()
|
|
31
|
+
else:
|
|
32
|
+
model = LGBMRegressor()
|
|
33
|
+
model.fit(input_data, diff, categorical_feature=categorical_indexes)
|
|
34
|
+
explainer = shap.TreeExplainer(model)
|
|
35
|
+
shap_values = explainer.shap_values(input_data)
|
|
36
|
+
shap_mean_abs = np.abs(shap_values).mean(axis=0)
|
|
37
|
+
if is_classification:
|
|
38
|
+
shap_mean_abs = shap_mean_abs.mean(axis=0)
|
|
39
|
+
return shap_mean_abs.tolist()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: erroranalysis
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
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
|
|
@@ -9,6 +9,7 @@ Classifier: Programming Language :: Python :: 3.6
|
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.7
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.8
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
14
|
Classifier: Operating System :: OS Independent
|
|
14
15
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -18,7 +19,7 @@ License-File: LICENSE
|
|
|
18
19
|
|
|
19
20
|
# Error Analysis SDK for Python
|
|
20
21
|
|
|
21
|
-
### This package has been tested with Python 3.6, 3.7, 3.8 and 3.
|
|
22
|
+
### This package has been tested with Python 3.6, 3.7, 3.8, 3.9 and 3.10
|
|
22
23
|
|
|
23
24
|
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.
|
|
24
25
|
|
|
@@ -23,6 +23,9 @@ erroranalysis/_internal/error_analyzer/__init__.py
|
|
|
23
23
|
erroranalysis/_internal/error_report/__init__.py
|
|
24
24
|
erroranalysis/analyzer/__init__.py
|
|
25
25
|
erroranalysis/analyzer/error_analyzer.py
|
|
26
|
+
erroranalysis/error_correlation_methods/__init__.py
|
|
27
|
+
erroranalysis/error_correlation_methods/ebm.py
|
|
28
|
+
erroranalysis/error_correlation_methods/gbm.py
|
|
26
29
|
erroranalysis/report/__init__.py
|
|
27
30
|
erroranalysis/report/error_report.py
|
|
28
31
|
tests/test_cohort_filter.py
|
|
@@ -32,6 +32,7 @@ setuptools.setup(
|
|
|
32
32
|
"Programming Language :: Python :: 3.7",
|
|
33
33
|
"Programming Language :: Python :: 3.8",
|
|
34
34
|
"Programming Language :: Python :: 3.9",
|
|
35
|
+
"Programming Language :: Python :: 3.10",
|
|
35
36
|
"License :: OSI Approved :: MIT License",
|
|
36
37
|
"Operating System :: OS Independent",
|
|
37
38
|
"Development Status :: 3 - Alpha"
|
|
@@ -6,8 +6,8 @@ import pandas as pd
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
8
|
from erroranalysis._internal.cohort_filter import filter_from_cohort
|
|
9
|
-
from erroranalysis._internal.constants import (
|
|
10
|
-
ModelTask)
|
|
9
|
+
from erroranalysis._internal.constants import (ARG, COLUMN, METHOD, PRED_Y,
|
|
10
|
+
ROW_INDEX, TRUE_Y, ModelTask)
|
|
11
11
|
from erroranalysis._internal.error_analyzer import ModelAnalyzer
|
|
12
12
|
from rai_test_utils.datasets.tabular import (create_diabetes_data,
|
|
13
13
|
create_iris_data,
|
|
@@ -15,6 +15,7 @@ from rai_test_utils.datasets.tabular import (create_diabetes_data,
|
|
|
15
15
|
from rai_test_utils.models.sklearn import (
|
|
16
16
|
create_sklearn_random_forest_regressor, create_sklearn_svm_classifier,
|
|
17
17
|
create_titanic_pipeline)
|
|
18
|
+
from raiutils.cohort import CohortFilterMethods
|
|
18
19
|
|
|
19
20
|
TOL = 1e-10
|
|
20
21
|
SEPAL_WIDTH = 'sepal width'
|
|
@@ -28,9 +29,9 @@ class TestCohortFilter(object):
|
|
|
28
29
|
def test_cohort_filter_equal(self):
|
|
29
30
|
X_train, X_test, y_train, y_test, feature_names, _ = \
|
|
30
31
|
create_iris_pandas()
|
|
31
|
-
filters = [{
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
filters = [{ARG: [2.8],
|
|
33
|
+
COLUMN: SEPAL_WIDTH,
|
|
34
|
+
METHOD: CohortFilterMethods.METHOD_EQUAL}]
|
|
34
35
|
validation_data = create_validation_data(X_test, y_test)
|
|
35
36
|
validation_data = validation_data.loc[X_test[SEPAL_WIDTH] == 2.8]
|
|
36
37
|
model_task = ModelTask.CLASSIFICATION
|
|
@@ -53,9 +54,9 @@ class TestCohortFilter(object):
|
|
|
53
54
|
"in Predicted Y cohort filtering")
|
|
54
55
|
X_train, X_test, y_train, y_test, feature_names, classes = \
|
|
55
56
|
create_iris_pandas(use_str_labels)
|
|
56
|
-
filters = [{
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
filters = [{ARG: [2],
|
|
58
|
+
COLUMN: target_type,
|
|
59
|
+
METHOD: CohortFilterMethods.METHOD_INCLUDES}]
|
|
59
60
|
validation_data = create_validation_data(X_test, y_test)
|
|
60
61
|
if use_str_labels:
|
|
61
62
|
validation_filter = y_test == classes[2]
|
|
@@ -79,9 +80,9 @@ class TestCohortFilter(object):
|
|
|
79
80
|
def test_cohort_filter_less(self):
|
|
80
81
|
X_train, X_test, y_train, y_test, feature_names, _ = \
|
|
81
82
|
create_iris_pandas()
|
|
82
|
-
filters = [{
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
filters = [{ARG: [2.8],
|
|
84
|
+
COLUMN: SEPAL_WIDTH,
|
|
85
|
+
METHOD: CohortFilterMethods.METHOD_LESS}]
|
|
85
86
|
validation_data = create_validation_data(X_test, y_test)
|
|
86
87
|
validation_data = validation_data.loc[X_test[SEPAL_WIDTH] < 2.8]
|
|
87
88
|
model_task = ModelTask.CLASSIFICATION
|
|
@@ -100,9 +101,9 @@ class TestCohortFilter(object):
|
|
|
100
101
|
X_train, X_test, y_train, y_test, feature_names, _ = \
|
|
101
102
|
create_iris_pandas()
|
|
102
103
|
|
|
103
|
-
filters = [{
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
filters = [{ARG: [2.8],
|
|
105
|
+
COLUMN: SEPAL_WIDTH,
|
|
106
|
+
METHOD: CohortFilterMethods.METHOD_LESS_AND_EQUAL}]
|
|
106
107
|
validation_data = create_validation_data(X_test, y_test)
|
|
107
108
|
validation_data = validation_data.loc[X_test[SEPAL_WIDTH] <= 2.8]
|
|
108
109
|
model_task = ModelTask.CLASSIFICATION
|
|
@@ -120,9 +121,9 @@ class TestCohortFilter(object):
|
|
|
120
121
|
def test_cohort_filter_greater(self):
|
|
121
122
|
X_train, X_test, y_train, y_test, feature_names, _ = \
|
|
122
123
|
create_iris_pandas()
|
|
123
|
-
filters = [{
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
filters = [{ARG: [2.8],
|
|
125
|
+
COLUMN: SEPAL_WIDTH,
|
|
126
|
+
METHOD: CohortFilterMethods.METHOD_GREATER}]
|
|
126
127
|
validation_data = create_validation_data(X_test, y_test)
|
|
127
128
|
validation_data = validation_data.loc[X_test[SEPAL_WIDTH] > 2.8]
|
|
128
129
|
model_task = ModelTask.CLASSIFICATION
|
|
@@ -140,9 +141,9 @@ class TestCohortFilter(object):
|
|
|
140
141
|
def test_cohort_filter_greater_and_equal(self):
|
|
141
142
|
X_train, X_test, y_train, y_test, feature_names, _ = \
|
|
142
143
|
create_iris_pandas()
|
|
143
|
-
filters = [{
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
filters = [{ARG: [2.8],
|
|
145
|
+
COLUMN: SEPAL_WIDTH,
|
|
146
|
+
METHOD: CohortFilterMethods.METHOD_GREATER_AND_EQUAL}]
|
|
146
147
|
validation_data = create_validation_data(X_test, y_test)
|
|
147
148
|
validation_data = validation_data.loc[X_test[SEPAL_WIDTH] >= 2.8]
|
|
148
149
|
model_task = ModelTask.CLASSIFICATION
|
|
@@ -160,9 +161,9 @@ class TestCohortFilter(object):
|
|
|
160
161
|
def test_cohort_filter_in_the_range_of(self):
|
|
161
162
|
X_train, X_test, y_train, y_test, feature_names, _ = \
|
|
162
163
|
create_iris_pandas()
|
|
163
|
-
filters = [{
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
filters = [{ARG: [2.8, 3.4],
|
|
165
|
+
COLUMN: SEPAL_WIDTH,
|
|
166
|
+
METHOD: CohortFilterMethods.METHOD_RANGE}]
|
|
166
167
|
validation_data = create_validation_data(X_test, y_test)
|
|
167
168
|
validation_data = validation_data.loc[
|
|
168
169
|
(X_test[SEPAL_WIDTH] <= 3.4) & (X_test[SEPAL_WIDTH] >= 2.8)]
|
|
@@ -190,9 +191,9 @@ class TestCohortFilter(object):
|
|
|
190
191
|
|
|
191
192
|
# the index 1, corresponds to incorrect prediction
|
|
192
193
|
# the index 0 correspond to correct prediction
|
|
193
|
-
filters = [{
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
filters = [{ARG: arg,
|
|
195
|
+
COLUMN: CLASSIFICATION_OUTCOME,
|
|
196
|
+
METHOD: CohortFilterMethods.METHOD_INCLUDES}]
|
|
196
197
|
pred_y = model.predict(X_test)
|
|
197
198
|
validation_data = create_validation_data(X_test, y_test, pred_y)
|
|
198
199
|
if correct_prediction:
|
|
@@ -221,9 +222,9 @@ class TestCohortFilter(object):
|
|
|
221
222
|
clf = create_titanic_pipeline(X_train, y_train)
|
|
222
223
|
categorical_features = categorical
|
|
223
224
|
# the indexes 0, 2 correspond to S, C
|
|
224
|
-
filters = [{
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
filters = [{ARG: [0, 2],
|
|
226
|
+
COLUMN: EMBARKED,
|
|
227
|
+
METHOD: CohortFilterMethods.METHOD_INCLUDES}]
|
|
227
228
|
validation_data = create_validation_data(X_test, y_test)
|
|
228
229
|
filter_embarked = X_test[EMBARKED].isin(['S', 'C'])
|
|
229
230
|
validation_data = validation_data.loc[filter_embarked]
|
|
@@ -244,9 +245,9 @@ class TestCohortFilter(object):
|
|
|
244
245
|
clf = create_titanic_pipeline(X_train, y_train)
|
|
245
246
|
categorical_features = categorical
|
|
246
247
|
# the indexes other than 0, 2 correspond to Q
|
|
247
|
-
filters = [{
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
filters = [{ARG: [0, 2],
|
|
249
|
+
COLUMN: EMBARKED,
|
|
250
|
+
METHOD: CohortFilterMethods.METHOD_EXCLUDES}]
|
|
250
251
|
validation_data = create_validation_data(X_test, y_test)
|
|
251
252
|
filter_embarked = X_test[EMBARKED].isin(['Q'])
|
|
252
253
|
validation_data = validation_data.loc[filter_embarked]
|
|
@@ -269,9 +270,9 @@ class TestCohortFilter(object):
|
|
|
269
270
|
categorical_features = categorical
|
|
270
271
|
# the indexes 1, 2 correspond to false positives and false negatives
|
|
271
272
|
# the indexes 0, 3 correspond to true positives and true negatives
|
|
272
|
-
filters = [{
|
|
273
|
-
|
|
274
|
-
|
|
273
|
+
filters = [{ARG: arg,
|
|
274
|
+
COLUMN: CLASSIFICATION_OUTCOME,
|
|
275
|
+
METHOD: CohortFilterMethods.METHOD_INCLUDES}]
|
|
275
276
|
pred_y = clf.predict(X_test)
|
|
276
277
|
validation_data = create_validation_data(X_test, y_test, pred_y)
|
|
277
278
|
if not outcome:
|
|
@@ -296,9 +297,9 @@ class TestCohortFilter(object):
|
|
|
296
297
|
X_train, X_test, y_train, y_test, feature_names, _ = \
|
|
297
298
|
create_iris_pandas()
|
|
298
299
|
# filter on index, which can be done from the RAI dashboard
|
|
299
|
-
filters = [{
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
filters = [{ARG: [40],
|
|
301
|
+
COLUMN: ROW_INDEX,
|
|
302
|
+
METHOD: CohortFilterMethods.METHOD_LESS_AND_EQUAL}]
|
|
302
303
|
validation_data = create_validation_data(X_test, y_test)
|
|
303
304
|
validation_data = validation_data.loc[validation_data[ROW_INDEX] <= 40]
|
|
304
305
|
model_task = ModelTask.CLASSIFICATION
|
|
@@ -321,9 +322,9 @@ class TestCohortFilter(object):
|
|
|
321
322
|
|
|
322
323
|
# filter on regression error, which can be done from the
|
|
323
324
|
# RAI dashboard
|
|
324
|
-
filters = [{
|
|
325
|
-
|
|
326
|
-
|
|
325
|
+
filters = [{ARG: [40],
|
|
326
|
+
COLUMN: REGRESSION_ERROR,
|
|
327
|
+
METHOD: CohortFilterMethods.METHOD_LESS_AND_EQUAL}]
|
|
327
328
|
|
|
328
329
|
model = create_sklearn_random_forest_regressor(X_train, y_train)
|
|
329
330
|
pred_y = model.predict(X_test)
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# Copyright (c) Microsoft Corporation
|
|
2
2
|
# Licensed under the MIT License.
|
|
3
3
|
|
|
4
|
-
import uuid
|
|
5
|
-
|
|
6
4
|
import numpy as np
|
|
7
5
|
import pandas as pd
|
|
8
6
|
import pytest
|
|
@@ -11,9 +9,13 @@ from erroranalysis._internal.error_analyzer import ModelAnalyzer
|
|
|
11
9
|
from erroranalysis._internal.error_report import ErrorReport
|
|
12
10
|
from rai_test_utils.datasets.tabular import (create_cancer_data,
|
|
13
11
|
create_housing_data,
|
|
14
|
-
create_iris_data
|
|
12
|
+
create_iris_data,
|
|
13
|
+
create_simple_titanic_data)
|
|
15
14
|
from rai_test_utils.models.model_utils import (create_models_classification,
|
|
16
15
|
create_models_regression)
|
|
16
|
+
from rai_test_utils.models.sklearn import \
|
|
17
|
+
create_complex_classification_pipeline
|
|
18
|
+
from rai_test_utils.utilities import is_valid_uuid
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class TestErrorReport(object):
|
|
@@ -79,13 +81,21 @@ class TestErrorReport(object):
|
|
|
79
81
|
categorical_features,
|
|
80
82
|
filter_features=filter_features)
|
|
81
83
|
|
|
84
|
+
def test_error_report_titanic(self):
|
|
85
|
+
X_train, X_test, y_train, y_test, numeric, categorical = \
|
|
86
|
+
create_simple_titanic_data()
|
|
87
|
+
|
|
88
|
+
# Drop all numeric features
|
|
89
|
+
X_train = X_train.drop(['pclass', 'age', 'fare'], axis=1)
|
|
90
|
+
X_test = X_test.drop(['pclass', 'age', 'fare'], axis=1)
|
|
91
|
+
|
|
92
|
+
clf = create_complex_classification_pipeline(
|
|
93
|
+
X_train, y_train, [], ['embarked', 'sex'])
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
except ValueError:
|
|
88
|
-
return False
|
|
95
|
+
# Pass the remaining categorical features
|
|
96
|
+
run_error_analyzer(clf, X_test, y_test, ['embarked', 'sex'],
|
|
97
|
+
['embarked', 'sex'],
|
|
98
|
+
filter_features=[])
|
|
89
99
|
|
|
90
100
|
|
|
91
101
|
def run_error_analyzer(model, X_test, y_test, feature_names,
|
|
@@ -7,7 +7,8 @@ import numpy as np
|
|
|
7
7
|
import pytest
|
|
8
8
|
from common_utils import replicate_dataset
|
|
9
9
|
|
|
10
|
-
from erroranalysis._internal.constants import
|
|
10
|
+
from erroranalysis._internal.constants import (ErrorCorrelationMethods,
|
|
11
|
+
ModelTask)
|
|
11
12
|
from erroranalysis._internal.error_analyzer import ModelAnalyzer
|
|
12
13
|
from rai_test_utils.datasets.tabular import (
|
|
13
14
|
create_binary_classification_dataset, create_cancer_data,
|
|
@@ -22,11 +23,17 @@ from rai_test_utils.models.sklearn import (
|
|
|
22
23
|
TOL = 1e-10
|
|
23
24
|
NUM_SAMPLE_ROWS = 100
|
|
24
25
|
DEFAULT_SAMPLE_COLS = 20
|
|
26
|
+
MUTUAL_INFO = ErrorCorrelationMethods.MUTUAL_INFO
|
|
27
|
+
EBM = ErrorCorrelationMethods.EBM
|
|
28
|
+
GBM_SHAP = ErrorCorrelationMethods.GBM_SHAP
|
|
29
|
+
GBM_ERROR_SAMPLES_TOL = 6
|
|
25
30
|
|
|
26
31
|
|
|
27
32
|
class TestImportances(object):
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
@pytest.mark.parametrize('error_correlation_method',
|
|
35
|
+
[MUTUAL_INFO, EBM, GBM_SHAP])
|
|
36
|
+
def test_importances_iris(self, error_correlation_method):
|
|
30
37
|
X_train, X_test, y_train, y_test, feature_names, _ = create_iris_data()
|
|
31
38
|
|
|
32
39
|
models = create_models_classification(X_train, y_train)
|
|
@@ -34,9 +41,11 @@ class TestImportances(object):
|
|
|
34
41
|
for model in models:
|
|
35
42
|
categorical_features = []
|
|
36
43
|
run_error_analyzer(model, X_test, y_test, feature_names,
|
|
37
|
-
categorical_features)
|
|
44
|
+
categorical_features, error_correlation_method)
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
@pytest.mark.parametrize('error_correlation_method',
|
|
47
|
+
[MUTUAL_INFO, EBM, GBM_SHAP])
|
|
48
|
+
def test_importances_cancer(self, error_correlation_method):
|
|
40
49
|
X_train, X_test, y_train, y_test, feature_names, _ = \
|
|
41
50
|
create_cancer_data()
|
|
42
51
|
|
|
@@ -45,9 +54,12 @@ class TestImportances(object):
|
|
|
45
54
|
for model in models:
|
|
46
55
|
categorical_features = []
|
|
47
56
|
run_error_analyzer(model, X_test, y_test, feature_names,
|
|
48
|
-
categorical_features)
|
|
57
|
+
categorical_features, error_correlation_method)
|
|
49
58
|
|
|
50
|
-
|
|
59
|
+
@pytest.mark.parametrize('error_correlation_method',
|
|
60
|
+
[MUTUAL_INFO, EBM, GBM_SHAP])
|
|
61
|
+
def test_importances_binary_classification(self,
|
|
62
|
+
error_correlation_method):
|
|
51
63
|
X_train, y_train, X_test, y_test, _ = \
|
|
52
64
|
create_binary_classification_dataset()
|
|
53
65
|
feature_names = list(X_train.columns)
|
|
@@ -56,18 +68,22 @@ class TestImportances(object):
|
|
|
56
68
|
for model in models:
|
|
57
69
|
categorical_features = []
|
|
58
70
|
run_error_analyzer(model, X_test, y_test, feature_names,
|
|
59
|
-
categorical_features)
|
|
71
|
+
categorical_features, error_correlation_method)
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
@pytest.mark.parametrize('error_correlation_method',
|
|
74
|
+
[MUTUAL_INFO, EBM, GBM_SHAP])
|
|
75
|
+
def test_importances_titanic(self, error_correlation_method):
|
|
62
76
|
X_train, X_test, y_train, y_test, numeric, categorical = \
|
|
63
77
|
create_simple_titanic_data()
|
|
64
78
|
feature_names = categorical + numeric
|
|
65
79
|
clf = create_titanic_pipeline(X_train, y_train)
|
|
66
80
|
categorical_features = categorical
|
|
67
81
|
run_error_analyzer(clf, X_test, y_test, feature_names,
|
|
68
|
-
categorical_features)
|
|
82
|
+
categorical_features, error_correlation_method)
|
|
69
83
|
|
|
70
|
-
|
|
84
|
+
@pytest.mark.parametrize('error_correlation_method',
|
|
85
|
+
[MUTUAL_INFO, EBM, GBM_SHAP])
|
|
86
|
+
def test_importances_housing(self, error_correlation_method):
|
|
71
87
|
X_train, X_test, y_train, y_test, feature_names = \
|
|
72
88
|
create_housing_data()
|
|
73
89
|
models = create_models_regression(X_train, y_train)
|
|
@@ -75,9 +91,11 @@ class TestImportances(object):
|
|
|
75
91
|
for model in models:
|
|
76
92
|
categorical_features = []
|
|
77
93
|
run_error_analyzer(model, X_test, y_test, feature_names,
|
|
78
|
-
categorical_features)
|
|
94
|
+
categorical_features, error_correlation_method)
|
|
79
95
|
|
|
80
|
-
|
|
96
|
+
@pytest.mark.parametrize('error_correlation_method',
|
|
97
|
+
[MUTUAL_INFO, EBM, GBM_SHAP])
|
|
98
|
+
def test_large_data_importances(self, error_correlation_method):
|
|
81
99
|
# mutual information can be very costly for large number of rows
|
|
82
100
|
# hence, assert we downsample to compute importances for large data
|
|
83
101
|
X_train, y_train, X_test, y_test, _ = \
|
|
@@ -91,16 +109,22 @@ class TestImportances(object):
|
|
|
91
109
|
model_analyzer = ModelAnalyzer(model, X_test, y_test,
|
|
92
110
|
feature_names,
|
|
93
111
|
categorical_features)
|
|
94
|
-
model_analyzer.compute_importances()
|
|
112
|
+
model_analyzer.compute_importances(error_correlation_method)
|
|
95
113
|
t1 = time.time()
|
|
96
114
|
execution_time = t1 - t0
|
|
97
115
|
print(execution_time)
|
|
98
116
|
# assert we don't take too long and downsample the dataset
|
|
99
117
|
# note execution time is in seconds
|
|
100
|
-
|
|
118
|
+
# note GBM_SHAP is slower than the other methods
|
|
119
|
+
if error_correlation_method == GBM_SHAP:
|
|
120
|
+
assert execution_time < 35
|
|
121
|
+
else:
|
|
122
|
+
assert execution_time < 25
|
|
101
123
|
|
|
102
124
|
@pytest.mark.parametrize('num_rows', [1, 2, 3, 4])
|
|
103
|
-
|
|
125
|
+
@pytest.mark.parametrize('error_correlation_method',
|
|
126
|
+
[MUTUAL_INFO, EBM, GBM_SHAP])
|
|
127
|
+
def test_small_data_importances(self, num_rows, error_correlation_method):
|
|
104
128
|
# validate we can run on very few rows
|
|
105
129
|
X_train, y_train, X_test, y_test, _ = \
|
|
106
130
|
create_binary_classification_dataset(NUM_SAMPLE_ROWS)
|
|
@@ -112,10 +136,12 @@ class TestImportances(object):
|
|
|
112
136
|
model_analyzer = ModelAnalyzer(model, X_test, y_test,
|
|
113
137
|
feature_names,
|
|
114
138
|
categorical_features)
|
|
115
|
-
scores = model_analyzer.compute_importances()
|
|
139
|
+
scores = model_analyzer.compute_importances(error_correlation_method)
|
|
116
140
|
assert len(scores) == DEFAULT_SAMPLE_COLS
|
|
117
141
|
|
|
118
|
-
|
|
142
|
+
@pytest.mark.parametrize('error_correlation_method',
|
|
143
|
+
[MUTUAL_INFO, EBM, GBM_SHAP])
|
|
144
|
+
def test_importances_missings(self, error_correlation_method):
|
|
119
145
|
X_train, X_test, y_train, y_test, feature_names, _ = create_iris_data()
|
|
120
146
|
|
|
121
147
|
# add missing values to X_test
|
|
@@ -127,23 +153,29 @@ class TestImportances(object):
|
|
|
127
153
|
|
|
128
154
|
categorical_features = []
|
|
129
155
|
run_error_analyzer(model, X_test, y_test, feature_names,
|
|
130
|
-
categorical_features)
|
|
156
|
+
categorical_features, error_correlation_method)
|
|
131
157
|
|
|
132
158
|
|
|
133
159
|
def run_error_analyzer(model, X_test, y_test, feature_names,
|
|
134
|
-
categorical_features
|
|
160
|
+
categorical_features,
|
|
161
|
+
error_correlation_method):
|
|
135
162
|
model_analyzer = ModelAnalyzer(model, X_test, y_test,
|
|
136
163
|
feature_names,
|
|
137
164
|
categorical_features)
|
|
138
|
-
scores = model_analyzer.compute_importances()
|
|
165
|
+
scores = model_analyzer.compute_importances(error_correlation_method)
|
|
139
166
|
if model_analyzer.model_task == ModelTask.CLASSIFICATION:
|
|
140
167
|
diff = model.predict(model_analyzer.dataset) != model_analyzer.true_y
|
|
141
168
|
else:
|
|
142
169
|
diff = model.predict(model_analyzer.dataset) - model_analyzer.true_y
|
|
143
170
|
assert isinstance(scores, list)
|
|
144
171
|
assert len(scores) == len(feature_names)
|
|
172
|
+
# GBM methods seems to predict all zeros
|
|
173
|
+
# (and sometimes otherwise) even if very few instances non-zero
|
|
174
|
+
is_gbm = error_correlation_method == GBM_SHAP
|
|
175
|
+
sum_diff = sum(diff)
|
|
176
|
+
noisy_gbm = sum_diff < GBM_ERROR_SAMPLES_TOL and sum_diff > 0 and is_gbm
|
|
145
177
|
# If model predicted perfectly, assert all scores are zeros
|
|
146
178
|
if not any(diff):
|
|
147
179
|
assert all(abs(score - 0) < TOL for score in scores)
|
|
148
|
-
|
|
180
|
+
elif not noisy_gbm:
|
|
149
181
|
assert any(score != 0 for score in scores)
|
|
@@ -6,8 +6,9 @@ import pandas as pd
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
8
|
from erroranalysis._internal.cohort_filter import filter_from_cohort
|
|
9
|
-
from erroranalysis._internal.constants import (
|
|
10
|
-
|
|
9
|
+
from erroranalysis._internal.constants import (ARG, COLUMN, METHOD, ROW_INDEX,
|
|
10
|
+
TRUE_Y, MatrixParams, Metrics,
|
|
11
|
+
ModelTask, f1_metrics,
|
|
11
12
|
metric_to_display_name,
|
|
12
13
|
precision_metrics,
|
|
13
14
|
recall_metrics)
|
|
@@ -50,9 +51,9 @@ class TestMatrixFilter(object):
|
|
|
50
51
|
# cohort was chosen in tree view
|
|
51
52
|
X_train, X_test, y_train, y_test, feature_names, _ = create_iris_data()
|
|
52
53
|
|
|
53
|
-
filters = [{
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
filters = [{ARG: [2.85],
|
|
55
|
+
COLUMN: feature_names[1],
|
|
56
|
+
METHOD: 'less and equal'}]
|
|
56
57
|
|
|
57
58
|
model_task = ModelTask.CLASSIFICATION
|
|
58
59
|
run_error_analyzer_on_models(X_train, y_train, X_test,
|
|
@@ -67,9 +68,9 @@ class TestMatrixFilter(object):
|
|
|
67
68
|
X_train = pd.DataFrame(X_train, columns=feature_names)
|
|
68
69
|
X_test = pd.DataFrame(X_test, columns=feature_names)
|
|
69
70
|
|
|
70
|
-
filters = [{
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
filters = [{ARG: [2.85],
|
|
72
|
+
COLUMN: feature_names[1],
|
|
73
|
+
METHOD: 'less and equal'}]
|
|
73
74
|
|
|
74
75
|
model_task = ModelTask.CLASSIFICATION
|
|
75
76
|
run_error_analyzer_on_models(X_train, y_train, X_test,
|
|
@@ -222,24 +223,24 @@ class TestMatrixFilter(object):
|
|
|
222
223
|
|
|
223
224
|
composite_filters = [{'compositeFilters':
|
|
224
225
|
[{'compositeFilters':
|
|
225
|
-
[{
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
[{ARG: [11.364, 13.182],
|
|
227
|
+
COLUMN: 'mean radius',
|
|
228
|
+
METHOD: 'in the range of'}],
|
|
228
229
|
'operation': 'and'},
|
|
229
230
|
{'compositeFilters':
|
|
230
|
-
[{
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
[{ARG: [13.182, 15],
|
|
232
|
+
COLUMN: 'mean radius',
|
|
233
|
+
METHOD: 'in the range of'}],
|
|
233
234
|
'operation': 'and'},
|
|
234
235
|
{'compositeFilters':
|
|
235
|
-
[{
|
|
236
|
-
|
|
237
|
-
|
|
236
|
+
[{ARG: [15, 16.817],
|
|
237
|
+
COLUMN: 'mean radius',
|
|
238
|
+
METHOD: 'in the range of'}],
|
|
238
239
|
'operation': 'and'},
|
|
239
240
|
{'compositeFilters':
|
|
240
|
-
[{
|
|
241
|
-
|
|
242
|
-
|
|
241
|
+
[{ARG: [16.817, 18.635],
|
|
242
|
+
COLUMN: 'mean radius',
|
|
243
|
+
METHOD: 'in the range of'}],
|
|
243
244
|
'operation': 'and'}],
|
|
244
245
|
'operation': 'or'}]
|
|
245
246
|
|
|
@@ -293,12 +294,12 @@ class TestMatrixFilter(object):
|
|
|
293
294
|
def test_matrix_filter_housing_filters(self):
|
|
294
295
|
X_train, X_test, y_train, y_test, feature_names = create_housing_data()
|
|
295
296
|
|
|
296
|
-
filters = [{
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
{
|
|
300
|
-
|
|
301
|
-
|
|
297
|
+
filters = [{ARG: [600],
|
|
298
|
+
COLUMN: 'Population',
|
|
299
|
+
METHOD: 'less and equal'},
|
|
300
|
+
{ARG: [6],
|
|
301
|
+
COLUMN: 'AveRooms',
|
|
302
|
+
METHOD: 'greater'}]
|
|
302
303
|
|
|
303
304
|
model_task = ModelTask.REGRESSION
|
|
304
305
|
run_error_analyzer_on_models(X_train, y_train, X_test,
|
|
@@ -66,12 +66,24 @@ class TestSurrogateErrorTree(object):
|
|
|
66
66
|
def test_surrogate_error_tree_int_categorical(self, analyzer_type):
|
|
67
67
|
X_train, X_test, y_train, y_test, categorical_features = \
|
|
68
68
|
create_adult_census_data()
|
|
69
|
-
|
|
70
69
|
model = create_kneighbors_classifier(X_train, y_train)
|
|
71
70
|
|
|
72
71
|
run_error_analyzer(model, X_test, y_test, list(X_train.columns),
|
|
73
72
|
analyzer_type, categorical_features)
|
|
74
73
|
|
|
74
|
+
@pytest.mark.parametrize('analyzer_type', [AnalyzerType.MODEL,
|
|
75
|
+
AnalyzerType.PREDICTIONS])
|
|
76
|
+
def test_surrogate_error_tree_categorical_filtered(self, analyzer_type):
|
|
77
|
+
X_train, X_test, y_train, y_test, categorical_features = \
|
|
78
|
+
create_adult_census_data()
|
|
79
|
+
model = create_kneighbors_classifier(X_train, y_train)
|
|
80
|
+
filters = [{ARG: [40],
|
|
81
|
+
COLUMN: 'Age',
|
|
82
|
+
METHOD: 'less and equal'}]
|
|
83
|
+
run_error_analyzer(model, X_test, y_test, list(X_train.columns),
|
|
84
|
+
analyzer_type, categorical_features,
|
|
85
|
+
filters=filters)
|
|
86
|
+
|
|
75
87
|
def test_large_data_surrogate_error_tree(self):
|
|
76
88
|
# validate tree trains quickly for large data
|
|
77
89
|
X_train, y_train, X_test, y_test, _ = \
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/_internal/error_analyzer/__init__.py
RENAMED
|
File without changes
|
{erroranalysis-0.4.2 → erroranalysis-0.4.4}/erroranalysis/_internal/error_report/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|