diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a4a94..2e3fee5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 1.3.6 (2026-09-14) +- Add support for newer versions of Flask (enabling the model error analysis of custom plugin's algorithms) +- Fix bug now that sparse matrices can be given as a preprocessed dataframe + ## Version 1.3.5 (2026-01-29) - Add python 3.12, 3.13 and 3.14 official support diff --git a/Jenkinsfile b/Jenkinsfile index d932127..f229f31 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -2,7 +2,7 @@ pipeline { options { disableConcurrentBuilds() } agent { label 'dss-plugin-tests'} environment { - PLUGIN_INTEGRATION_TEST_INSTANCE="$HOME/instance_config.json" + PLUGIN_INTEGRATION_TEST_INSTANCE="/home/jenkins-agent/instance_config.json" UNIT_TEST_FILES_STATUS_CODE = sh(script: 'ls ./tests/*/unit/test*', returnStatus: true) INTEGRATION_TEST_FILES_STATUS_CODE = sh(script: 'ls ./tests/*/integration/test*', returnStatus: true) } diff --git a/plugin.json b/plugin.json index 7c54c72..9c8c844 100644 --- a/plugin.json +++ b/plugin.json @@ -1,6 +1,6 @@ { "id" : "model-error-analysis", - "version" : "1.3.5", + "version" : "1.3.6", "meta" : { "label" : "Model Error Analysis", "description" : "Debug model performance with error analysis. A code env is only required to use the Jupyter Notebook.", diff --git a/python-lib/dku_error_analysis_tree_parsing/tree_parser.py b/python-lib/dku_error_analysis_tree_parsing/tree_parser.py index 6e3c425..c46afc3 100644 --- a/python-lib/dku_error_analysis_tree_parsing/tree_parser.py +++ b/python-lib/dku_error_analysis_tree_parsing/tree_parser.py @@ -293,7 +293,10 @@ def parse_nodes(self, tree, preprocessed_x): preprocessed_feature = self.feature_list[feature_idx] split_parameters = self._get_split_parameters(preprocessed_feature) if split_parameters.feature not in tree.df: - tree.df[split_parameters.feature] = split_parameters.add_preprocessed_feature(preprocessed_x, feature_idx) + feature_values = split_parameters.add_preprocessed_feature(preprocessed_x, feature_idx) + if hasattr(feature_values, "toarray"): # for sparse matrices + feature_values = feature_values.toarray().reshape(-1) + tree.df[split_parameters.feature] = feature_values value = split_parameters.value if value is None: value = split_parameters.value_func(threshold) diff --git a/resource/py/test_dku_visualizer.py b/resource/py/test_dku_visualizer.py index 47c0132..5706262 100644 --- a/resource/py/test_dku_visualizer.py +++ b/resource/py/test_dku_visualizer.py @@ -43,7 +43,7 @@ def _get_stats(node_id, feature_name, nr_bins, bins=None): return _get_stats return f -def test_plot_feature_distributions_no_show_global(mocker, mocked_nodes, mocked_get_stats, caplog): +def test_plot_feature_distributions_no_show_global(mocker, mocked_nodes, mocked_get_stats, caplog, dss_target): analyzer = mocker.Mock(spec=DkuErrorAnalyzer) mocker.patch.object(analyzer, "_get_ranked_leaf_ids", return_value=[1]) analyzer.tree.ranked_features = [ @@ -120,7 +120,7 @@ def test_plot_feature_distributions_no_show_global(mocker, mocked_nodes, mocked_ assert patched_get_node.call_args_list[2][0] == (1,) and patched_get_node.call_count == 3 patched_get_stats.assert_called_once_with(1, "num", 10) -def test_plot_feature_distributions_show_global(mocker, mocked_nodes, mocked_get_stats, caplog): +def test_plot_feature_distributions_show_global(mocker, mocked_nodes, mocked_get_stats, caplog, dss_target): analyzer = mocker.Mock(spec=DkuErrorAnalyzer) mocker.patch.object(analyzer, "_get_ranked_leaf_ids", return_value=[1]) analyzer.tree.ranked_features = [ @@ -226,6 +226,6 @@ def test_plot_feature_distributions_show_global(mocker, mocked_nodes, mocked_get assert patched_get_stats.call_args_list[0][0] == (1, "num", 10) assert patched_get_stats.call_args_list[1][0] == (0, "num", 10, []) -def test_failed_init(mocker): +def test_failed_init(mocker, dss_target): with pytest.raises(TypeError, match="You need to input a DkuErrorAnalyzer object."): DkuErrorVisualizer(mocker.Mock(spec=ErrorAnalyzer)) diff --git a/resource/py/test_tree_parser.py b/resource/py/test_tree_parser.py index 520b8c9..e066482 100644 --- a/resource/py/test_tree_parser.py +++ b/resource/py/test_tree_parser.py @@ -8,6 +8,7 @@ import numpy as np import pytest import logging +from scipy import sparse @pytest.fixture def create_parser(mocker): @@ -250,6 +251,30 @@ def test_build_tree(mocker, df, create_parser, dss_target): pd.testing.assert_frame_equal(tree.df, pd.concat([dataframe, pd.Series(["toast"]*12, name="super_cat_1"), pd.Series(["hellow"]*12, name="super_cat_2")], axis=1)) + +@pytest.mark.parsing +def test_build_tree_with_sparse_preprocessed_features(mocker, create_parser, dss_target): + mocker.patch("dku_error_analysis_tree_parsing.tree_parser.descale_numerical_thresholds", + return_value=[.5, -2, -2]) + + error_model = mocker.Mock(classes_=np.array([ + ErrorAnalyzerConstants.WRONG_PREDICTION, + ErrorAnalyzerConstants.CORRECT_PREDICTION + ])) + error_model.tree_.children_left = np.array([1, -2, -2]) + error_model.tree_.children_right = np.array([2, -2, -2]) + error_model.tree_.feature = np.array([0, -2, -2]) + error_model.tree_.value = np.array([[[1, 2]], [[1, 0]], [[0, 2]]]) + + tree = mocker.Mock(df=pd.DataFrame(index=range(3))) + parser = create_parser(error_model=error_model, feature_names=["feature"]) + + parser.parse_nodes(tree, sparse.csr_matrix([[0], [1], [2]])) + + pd.testing.assert_series_equal( + tree.df["feature"], pd.Series([0, 1, 2], name="feature") + ) + # CATEGORICAL HANDLINGS def check_dummy(split, name, value=None, others=False): assert split.node_type == Node.TYPES.CAT diff --git a/webapps/error-analysis/backend.py b/webapps/error-analysis/backend.py index 1ae78e9..bb437b1 100644 --- a/webapps/error-analysis/backend.py +++ b/webapps/error-analysis/backend.py @@ -10,7 +10,18 @@ from dku_error_analysis_decision_tree.tree_handler import TreeHandler -app.json_encoder = DKUJSONEncoder +try: + from flask.json.provider import DefaultJSONProvider + class DSSJSONProvider(DefaultJSONProvider): + def default(self, obj): + return DKUJSONEncoder().default(obj) + + # Since Flask 2.2, jsonify delegates to app.json; app.json_encoder was + # deprecated then and removed in Flask 2.3. + app.json = DSSJSONProvider(app) +except ImportError: + # Flask < 2.2 still uses the legacy JSON encoder API. + app.json_encoder = DKUJSONEncoder LOGGER = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO, format="Error Analysis Plugin %(levelname)s - %(message)s")