Skip to content

Commit c42459a

Browse files
authored
Merge pull request #6309 from janezd/mds-kruskal-stress
[ENH] MDS: Show Kruskal stress
2 parents 25cc597 + 0d145ab commit c42459a

File tree

2 files changed

+44
-3
lines changed

2 files changed

+44
-3
lines changed

Orange/widgets/unsupervised/owmds.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ def __init__(self):
200200

201201
self.embedding = None # type: Optional[np.ndarray]
202202
self.effective_matrix = None # type: Optional[DistMatrix]
203+
self.stress = None
203204

204205
self.size_model = self.gui.points_models[2]
205206
self.size_model.order = \
@@ -241,6 +242,8 @@ def _add_controls_optimization(self):
241242
sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed),
242243
callback=self.__refresh_rate_combo_changed),
243244
1, 1)
245+
self.stress_label = QLabel("Kruskal Stress: -")
246+
grid.addWidget(self.stress_label, 2, 0, 1, 3)
244247

245248
def __refresh_rate_combo_changed(self):
246249
if self.task is not None:
@@ -392,17 +395,31 @@ def on_partial_result(self, result: Result):
392395
if need_update:
393396
self.graph.update_coordinates()
394397
self.graph.update_density()
398+
self.update_stress()
395399

396400
def on_done(self, result: Result):
397401
assert isinstance(result.embedding, np.ndarray)
398402
assert len(result.embedding) == len(self.effective_matrix)
399403
self.embedding = result.embedding
400404
self.graph.update_coordinates()
401405
self.graph.update_density()
406+
self.update_stress()
402407
self.run_button.setText("Start")
403408
self.step_button.setEnabled(True)
404409
self.commit.deferred()
405410

411+
def update_stress(self):
412+
self.stress = self._compute_stress()
413+
stress_val = "-" if self.stress is None else f"{self.stress:.3f}"
414+
self.stress_label.setText(f"Kruskal Stress: {stress_val}")
415+
416+
def _compute_stress(self):
417+
if self.embedding is None or self.effective_matrix is None:
418+
return None
419+
point_stress = self.get_stress(self.embedding, self.effective_matrix)
420+
return np.sqrt(2 * np.sum(point_stress)
421+
/ (np.sum(self.effective_matrix ** 2) or 1))
422+
406423
def on_exception(self, ex: Exception):
407424
if isinstance(ex, MemoryError):
408425
self.Error.out_of_memory()
@@ -436,6 +453,7 @@ def jitter_coord(part):
436453
# (Random or PCA), restarting the optimization if necessary.
437454
if self.effective_matrix is None:
438455
self.graph.reset_graph()
456+
self.update_stress()
439457
return
440458

441459
X = self.effective_matrix
@@ -451,6 +469,8 @@ def jitter_coord(part):
451469
# restart the optimization if it was interrupted.
452470
if self.task is not None:
453471
self._run()
472+
else:
473+
self.update_stress()
454474

455475
def handleNewSignals(self):
456476
self._initialize()
@@ -473,12 +493,12 @@ def setup_plot(self):
473493

474494
def get_size_data(self):
475495
if self.attr_size == "Stress":
476-
return self.stress(self.embedding, self.effective_matrix)
496+
return self.get_stress(self.embedding, self.effective_matrix)
477497
else:
478498
return super().get_size_data()
479499

480500
@staticmethod
481-
def stress(X, distD):
501+
def get_stress(X, distD):
482502
assert X.shape[0] == distD.shape[0] == distD.shape[1]
483503
D1_c = scipy.spatial.distance.pdist(X, metric="euclidean")
484504
D1 = scipy.spatial.distance.squareform(D1_c, checks=False)

Orange/widgets/unsupervised/tests/test_owmds.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Test methods with long descriptive names can omit docstrings
2-
# pylint: disable=missing-docstring
2+
# pylint: disable=missing-docstring,protected-access
33
import os
44
from itertools import chain
55
import unittest
@@ -320,6 +320,27 @@ def test_matrix_columns_default_label(self):
320320
label_text = self.widget.controls.attr_label.currentText()
321321
self.assertEqual(label_text, "labels")
322322

323+
def test_update_stress(self):
324+
w = self.widget
325+
w.effective_matrix = np.array([[0, 4, 1],
326+
[4, 0, 1],
327+
[1, 1, 0]]) # sum of squares is 36
328+
w.embedding = np.array([[0, 0],
329+
[0, 3],
330+
[4, 3]])
331+
# dists [[0, 3, 5], diff [[0, 1, 4], sqr [[0, 1, 16], sum = 52
332+
# [3, 0, 4], [1, 0, 3], [1, 0, 9],
333+
# [5, 4, 0]] [4, 3, 0]] [16, 9, 0]]
334+
w.update_stress()
335+
expected = np.sqrt(52 / 36)
336+
self.assertAlmostEqual(w._compute_stress(), expected)
337+
self.assertIn(f"{expected:.3f}", w.stress_label.text())
338+
339+
w.embedding = None
340+
w.update_stress()
341+
self.assertIsNone(w._compute_stress())
342+
self.assertIn("-", w.stress_label.text())
343+
323344

324345
class TestOWMDSRunner(unittest.TestCase):
325346
@classmethod

0 commit comments

Comments
 (0)