Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down
5 changes: 4 additions & 1 deletion python-lib/dku_error_analysis_tree_parsing/tree_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions resource/py/test_dku_visualizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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))
25 changes: 25 additions & 0 deletions resource/py/test_tree_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import numpy as np
import pytest
import logging
from scipy import sparse

@pytest.fixture
def create_parser(mocker):
Expand Down Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion webapps/error-analysis/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading