Skip to content

Commit cbfedc3

Browse files
committed
MDS: Show Kruskal stress
1 parent a25a9d2 commit cbfedc3

File tree

2 files changed

+40
-0
lines changed

2 files changed

+40
-0
lines changed

Orange/widgets/unsupervised/owmds.py

Lines changed: 21 additions & 0 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,32 @@ 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+
actual = scipy.spatial.distance.pdist(self.embedding)
420+
actual = scipy.spatial.distance.squareform(actual)
421+
return np.sqrt(np.sum((actual - self.effective_matrix) ** 2)
422+
/ (np.sum(self.effective_matrix ** 2) or 1))
423+
406424
def on_exception(self, ex: Exception):
407425
if isinstance(ex, MemoryError):
408426
self.Error.out_of_memory()
@@ -436,6 +454,7 @@ def jitter_coord(part):
436454
# (Random or PCA), restarting the optimization if necessary.
437455
if self.effective_matrix is None:
438456
self.graph.reset_graph()
457+
self.update_stress()
439458
return
440459

441460
X = self.effective_matrix
@@ -451,6 +470,8 @@ def jitter_coord(part):
451470
# restart the optimization if it was interrupted.
452471
if self.task is not None:
453472
self._run()
473+
else:
474+
self.update_stress()
454475

455476
def handleNewSignals(self):
456477
self._initialize()

Orange/widgets/unsupervised/tests/test_owmds.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,25 @@ 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 = [[0, 0], [0, 3],
329+
[4, 3]]
330+
# dists [[0, 3, 5], diff [[0, 1, 4], sqr [[0, 1, 16], sum = 52
331+
# [3, 0, 4], [1, 0, 3], [1, 0, 9],
332+
# [5, 4, 0]] [4, 3, 0]] [16, 9, 0]]
333+
w.update_stress()
334+
expected = np.sqrt(52 / 36)
335+
self.assertAlmostEqual(w._compute_stress(), expected)
336+
self.assertIn(f"{expected:.3f}", w.stress_label.text())
337+
338+
w.embedding = None
339+
w.update_stress()
340+
self.assertIsNone(w._compute_stress())
341+
self.assertIn("-", w.stress_label.text())
323342

324343
class TestOWMDSRunner(unittest.TestCase):
325344
@classmethod

0 commit comments

Comments
 (0)