Skip to content

Commit 4d6c13c

Browse files
authored
Merge pull request #2851 from ales-erjavec/owdistances-normalize
[ENH] owdistances: Add 'Normalize' check box
2 parents 5301c23 + 5adf86f commit 4d6c13c

File tree

2 files changed

+52
-10
lines changed

2 files changed

+52
-10
lines changed

Orange/widgets/unsupervised/owdistances.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,16 @@ class Inputs:
3434
class Outputs:
3535
distances = Output("Distances", Orange.misc.DistMatrix, dynamic=False)
3636

37-
axis = settings.Setting(0)
38-
metric_idx = settings.Setting(0)
39-
autocommit = settings.Setting(True)
37+
settings_version = 2
38+
39+
axis = settings.Setting(0) # type: int
40+
metric_idx = settings.Setting(0) # type: int
41+
42+
#: Use normalized distances if the metric supports it.
43+
#: The default is `True`, expect when restoring from old pre v2 settings
44+
#: (see `migrate_settings`).
45+
normalized_dist = settings.Setting(True) # type: bool
46+
autocommit = settings.Setting(True) # type: bool
4047

4148
want_main_area = False
4249
buttons_area_orientation = Qt.Vertical
@@ -59,11 +66,21 @@ def __init__(self):
5966
gui.radioButtons(self.controlArea, self, "axis", ["Rows", "Columns"],
6067
box="Distances between", callback=self._invalidate
6168
)
62-
self.metrics_combo = gui.comboBox(self.controlArea, self, "metric_idx",
63-
box="Distance Metric",
64-
items=[m[0] for m in METRICS],
65-
callback=self._invalidate
66-
)
69+
box = gui.widgetBox(self.controlArea, "Distance Metric")
70+
self.metrics_combo = gui.comboBox(
71+
box, self, "metric_idx",
72+
items=[m[0] for m in METRICS],
73+
callback=self._metric_changed
74+
)
75+
self.normalization_check = gui.checkBox(
76+
box, self, "normalized_dist", "Normalized",
77+
callback=self._invalidate,
78+
tooltip=("All dimensions are (implicitly) scaled to a common"
79+
"scale to normalize the influence across the domain.")
80+
)
81+
_, metric = METRICS[self.metric_idx]
82+
self.normalization_check.setEnabled(metric.supports_normalization)
83+
6784
gui.auto_commit(self.controlArea, self, "autocommit", "Apply")
6885
self.layout().setSizeConstraint(self.layout().SetFixedSize)
6986

@@ -118,7 +135,11 @@ def _fix_missing():
118135
if check() is False:
119136
return
120137
try:
121-
return metric(data, axis=1 - self.axis, impute=True)
138+
if metric.supports_normalization and self.normalized_dist:
139+
return metric(data, axis=1 - self.axis, impute=True,
140+
normalize=True)
141+
else:
142+
return metric(data, axis=1 - self.axis, impute=True)
122143
except ValueError as e:
123144
self.Error.distances_value_error(e)
124145
except MemoryError:
@@ -127,9 +148,21 @@ def _fix_missing():
127148
def _invalidate(self):
128149
self.commit()
129150

151+
def _metric_changed(self):
152+
metric = METRICS[self.metric_idx][1]
153+
self.normalization_check.setEnabled(metric.supports_normalization)
154+
self._invalidate()
155+
130156
def send_report(self):
131157
# pylint: disable=invalid-sequence-index
132158
self.report_items((
133159
("Distances Between", ["Rows", "Columns"][self.axis]),
134160
("Metric", METRICS[self.metric_idx][0])
135161
))
162+
163+
@classmethod
164+
def migrate_settings(cls, settings, version):
165+
if version is None or version < 2 and "normalized_dist" not in settings:
166+
# normalize_dist is set to False when restoring settings from
167+
# an older version to preserve old semantics.
168+
settings["normalized_dist"] = False

Orange/widgets/unsupervised/tests/test_owdistances.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,13 @@ def test_distance_combo(self):
2727
self.widget.metrics_combo.activated.emit(i)
2828
self.widget.metrics_combo.setCurrentIndex(i)
2929
self.send_signal(self.widget.Inputs.data, self.iris)
30+
if metric.supports_normalization:
31+
expected = metric(self.iris, normalize=self.widget.normalized_dist)
32+
else:
33+
expected = metric(self.iris)
34+
3035
np.testing.assert_array_equal(
31-
metric(self.iris), self.get_output(self.widget.Outputs.distances))
36+
expected, self.get_output(self.widget.Outputs.distances))
3237

3338
def test_error_message(self):
3439
"""Check if error message appears and then disappears when
@@ -58,3 +63,7 @@ def test_too_big_array(self):
5863
self.widget.compute_distances(mock, self.iris)
5964
self.assertEqual(len(self.widget.Error.active), 1)
6065
self.assertTrue(self.widget.Error.distances_memory_error.is_shown())
66+
67+
def test_migrates_normalized_dist(self):
68+
w = self.create_widget(OWDistances, stored_settings={"metric_idx": 0})
69+
self.assertFalse(w.normalized_dist)

0 commit comments

Comments
 (0)