From 2e01854abf648ae014cb57c5c4d9ce9a185b2066 Mon Sep 17 00:00:00 2001 From: janezd Date: Mon, 23 Jan 2023 18:59:38 +0100 Subject: [PATCH] MDS: Show Kruskal stress --- Orange/widgets/unsupervised/owmds.py | 25 +++++++++++++++++-- .../widgets/unsupervised/tests/test_owmds.py | 21 +++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/Orange/widgets/unsupervised/owmds.py b/Orange/widgets/unsupervised/owmds.py index 940f2dd22e0..c7fa265e72a 100644 --- a/Orange/widgets/unsupervised/owmds.py +++ b/Orange/widgets/unsupervised/owmds.py @@ -200,6 +200,7 @@ def __init__(self): self.embedding = None # type: Optional[np.ndarray] self.effective_matrix = None # type: Optional[DistMatrix] + self.stress = None self.size_model = self.gui.points_models[2] self.size_model.order = \ @@ -241,6 +242,8 @@ def _add_controls_optimization(self): sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed), callback=self.__refresh_rate_combo_changed), 1, 1) + self.stress_label = QLabel("Kruskal Stress: -") + grid.addWidget(self.stress_label, 2, 0, 1, 3) def __refresh_rate_combo_changed(self): if self.task is not None: @@ -392,6 +395,7 @@ def on_partial_result(self, result: Result): if need_update: self.graph.update_coordinates() self.graph.update_density() + self.update_stress() def on_done(self, result: Result): assert isinstance(result.embedding, np.ndarray) @@ -399,10 +403,24 @@ def on_done(self, result: Result): self.embedding = result.embedding self.graph.update_coordinates() self.graph.update_density() + self.update_stress() self.run_button.setText("Start") self.step_button.setEnabled(True) self.commit.deferred() + def update_stress(self): + self.stress = self._compute_stress() + stress_val = "-" if self.stress is None else f"{self.stress:.3f}" + self.stress_label.setText(f"Kruskal Stress: {stress_val}") + + def _compute_stress(self): + if self.embedding is None or self.effective_matrix is None: + return None + actual = scipy.spatial.distance.pdist(self.embedding) + actual = scipy.spatial.distance.squareform(actual) + return np.sqrt(np.sum((actual - self.effective_matrix) ** 2) + / (np.sum(self.effective_matrix ** 2) or 1)) + def on_exception(self, ex: Exception): if isinstance(ex, MemoryError): self.Error.out_of_memory() @@ -436,6 +454,7 @@ def jitter_coord(part): # (Random or PCA), restarting the optimization if necessary. if self.effective_matrix is None: self.graph.reset_graph() + self.update_stress() return X = self.effective_matrix @@ -451,6 +470,8 @@ def jitter_coord(part): # restart the optimization if it was interrupted. if self.task is not None: self._run() + else: + self.update_stress() def handleNewSignals(self): self._initialize() @@ -473,12 +494,12 @@ def setup_plot(self): def get_size_data(self): if self.attr_size == "Stress": - return self.stress(self.embedding, self.effective_matrix) + return self.get_stress(self.embedding, self.effective_matrix) else: return super().get_size_data() @staticmethod - def stress(X, distD): + def get_stress(X, distD): assert X.shape[0] == distD.shape[0] == distD.shape[1] D1_c = scipy.spatial.distance.pdist(X, metric="euclidean") D1 = scipy.spatial.distance.squareform(D1_c, checks=False) diff --git a/Orange/widgets/unsupervised/tests/test_owmds.py b/Orange/widgets/unsupervised/tests/test_owmds.py index 40e5e70b2b6..bc8271770d1 100644 --- a/Orange/widgets/unsupervised/tests/test_owmds.py +++ b/Orange/widgets/unsupervised/tests/test_owmds.py @@ -1,5 +1,5 @@ # Test methods with long descriptive names can omit docstrings -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring,protected-access import os from itertools import chain import unittest @@ -320,6 +320,25 @@ def test_matrix_columns_default_label(self): label_text = self.widget.controls.attr_label.currentText() self.assertEqual(label_text, "labels") + def test_update_stress(self): + w = self.widget + w.effective_matrix = np.array([[0, 4, 1], + [4, 0, 1], + [1, 1, 0]]) # sum of squares is 36 + w.embedding = [[0, 0], [0, 3], + [4, 3]] + # dists [[0, 3, 5], diff [[0, 1, 4], sqr [[0, 1, 16], sum = 52 + # [3, 0, 4], [1, 0, 3], [1, 0, 9], + # [5, 4, 0]] [4, 3, 0]] [16, 9, 0]] + w.update_stress() + expected = np.sqrt(52 / 36) + self.assertAlmostEqual(w._compute_stress(), expected) + self.assertIn(f"{expected:.3f}", w.stress_label.text()) + + w.embedding = None + w.update_stress() + self.assertIsNone(w._compute_stress()) + self.assertIn("-", w.stress_label.text()) class TestOWMDSRunner(unittest.TestCase): @classmethod