Skip to content

Commit b513d40

Browse files
authored
Merge pull request #2377 from jerneju/exception-silhouette
[FIX] Silhouette Plot: now setting axis range properly
2 parents 942877a + 323a65b commit b513d40

File tree

2 files changed

+77
-55
lines changed

2 files changed

+77
-55
lines changed

Orange/widgets/visualize/owsilhouetteplot.py

Lines changed: 57 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
if sys.version_info > (3, 5):
99
from typing import Optional
1010

11-
import numpy
11+
import numpy as np
1212
import sklearn.metrics
1313

1414
from AnyQt.QtWidgets import (
@@ -99,13 +99,13 @@ def __init__(self):
9999
self._matrix = None # type: Optional[Orange.misc.DistMatrix]
100100
#: An bool mask (size == len(data)) indicating missing group/cluster
101101
#: assignments
102-
self._mask = None # type: Optional[numpy.ndarray]
102+
self._mask = None # type: Optional[np.ndarray]
103103
#: An array of cluster/group labels for instances with valid group
104104
#: assignment
105-
self._labels = None # type: Optional[numpy.ndarray]
105+
self._labels = None # type: Optional[np.ndarray]
106106
#: An array of silhouette scores for instances with valid group
107107
#: assignment
108-
self._silhouette = None # type: Optional[numpy.ndarray]
108+
self._silhouette = None # type: Optional[np.ndarray]
109109
self._silplot = None # type: Optional[SilhouettePlot]
110110

111111
gui.comboBox(
@@ -263,7 +263,7 @@ def _update(self):
263263
if self._matrix is None and self._effective_data is not None:
264264
_, metric = self.Distances[self.distance_idx]
265265
try:
266-
self._matrix = numpy.asarray(metric(self._effective_data))
266+
self._matrix = np.asarray(metric(self._effective_data))
267267
except MemoryError:
268268
self.Error.memory_error()
269269
return
@@ -286,12 +286,12 @@ def _clear_messages(self):
286286
def _update_labels(self):
287287
labelvar = self.cluster_var_model[self.cluster_var_idx]
288288
labels, _ = self.data.get_column_view(labelvar)
289-
labels = numpy.asarray(labels, dtype=float)
290-
mask = numpy.isnan(labels)
289+
labels = np.asarray(labels, dtype=float)
290+
mask = np.isnan(labels)
291291
labels = labels.astype(int)
292292
labels = labels[~mask]
293293

294-
labels_unq, _ = numpy.unique(labels, return_counts=True)
294+
labels_unq, _ = np.unique(labels, return_counts=True)
295295

296296
if len(labels_unq) < 2:
297297
self.Error.need_two_clusters()
@@ -307,7 +307,7 @@ def _update_labels(self):
307307
self._silhouette = silhouette
308308

309309
if labels is not None:
310-
count_missing = numpy.count_nonzero(mask)
310+
count_missing = np.count_nonzero(mask)
311311
if count_missing:
312312
self.Warning.missing_cluster_assignment(
313313
count_missing, s="s" if count_missing > 1 else "")
@@ -333,8 +333,8 @@ def _replot(self):
333333
else:
334334
silplot.setScores(
335335
self._silhouette,
336-
numpy.zeros(len(self._silhouette), dtype=int),
337-
[""], numpy.array([[63, 207, 207]])
336+
np.zeros(len(self._silhouette), dtype=int),
337+
[""], np.array([[63, 207, 207]])
338338
)
339339

340340
self.scene.addItem(silplot)
@@ -379,17 +379,17 @@ def commit(self):
379379
"""
380380
selected = indices = data = None
381381
if self.data is not None:
382-
selectedmask = numpy.full(len(self.data), False, dtype=bool)
382+
selectedmask = np.full(len(self.data), False, dtype=bool)
383383
if self._silplot is not None:
384384
indices = self._silplot.selection()
385-
assert (numpy.diff(indices) > 0).all(), "strictly increasing"
385+
assert (np.diff(indices) > 0).all(), "strictly increasing"
386386
if self._mask is not None:
387-
indices = numpy.flatnonzero(~self._mask)[indices]
387+
indices = np.flatnonzero(~self._mask)[indices]
388388
selectedmask[indices] = True
389389

390390
if self._mask is not None:
391-
scores = numpy.full(shape=selectedmask.shape,
392-
fill_value=numpy.nan)
391+
scores = np.full(shape=selectedmask.shape,
392+
fill_value=np.nan)
393393
scores[~self._mask] = self._silhouette
394394
else:
395395
scores = self._silhouette
@@ -408,14 +408,14 @@ def commit(self):
408408
domain = self.data.domain
409409
data = self.data
410410

411-
if numpy.count_nonzero(selectedmask):
411+
if np.count_nonzero(selectedmask):
412412
selected = self.data.from_table(
413-
domain, self.data, numpy.flatnonzero(selectedmask))
413+
domain, self.data, np.flatnonzero(selectedmask))
414414

415415
if self.add_scores:
416416
if selected is not None:
417-
selected[:, silhouette_var] = numpy.c_[scores[selectedmask]]
418-
data[:, silhouette_var] = numpy.c_[scores]
417+
selected[:, silhouette_var] = np.c_[scores[selectedmask]]
418+
data[:, silhouette_var] = np.c_[scores]
419419

420420
self.Outputs.selected_data.send(selected)
421421
self.Outputs.annotated_data.send(create_annotated_table(data, indices))
@@ -456,7 +456,7 @@ def __init__(self, parent=None, **kwargs):
456456
self.__rowNamesVisible = True
457457
self.__barHeight = 3
458458
self.__selectionRect = None
459-
self.__selection = numpy.asarray([], dtype=int)
459+
self.__selection = np.asarray([], dtype=int)
460460
self.__selstate = None
461461
self.__pen = QPen(Qt.NoPen)
462462
self.__layout = QGraphicsGridLayout()
@@ -482,10 +482,10 @@ def setScores(self, scores, labels, values, colors, rownames=None):
482482
rownames : list of str, optional
483483
A list (len == N) of row names.
484484
"""
485-
scores = numpy.asarray(scores, dtype=float)
486-
labels = numpy.asarray(labels, dtype=int)
485+
scores = np.asarray(scores, dtype=float)
486+
labels = np.asarray(labels, dtype=int)
487487
if rownames is not None:
488-
rownames = numpy.asarray(rownames, dtype=object)
488+
rownames = np.asarray(rownames, dtype=object)
489489

490490
if not scores.ndim == labels.ndim == 1:
491491
raise ValueError("scores and labels must be 1 dimensional")
@@ -494,13 +494,13 @@ def setScores(self, scores, labels, values, colors, rownames=None):
494494
if rownames is not None and rownames.shape != scores.shape:
495495
raise ValueError("rownames must have the same size as scores")
496496

497-
Ck = numpy.unique(labels)
497+
Ck = np.unique(labels)
498498
if not Ck[0] >= 0 and Ck[-1] < len(values):
499499
raise ValueError(
500500
"All indices in `labels` must be in `range(len(values))`")
501-
cluster_indices = [numpy.flatnonzero(labels == i)
501+
cluster_indices = [np.flatnonzero(labels == i)
502502
for i in range(len(values))]
503-
cluster_indices = [indices[numpy.argsort(scores[indices])[::-1]]
503+
cluster_indices = [indices[np.argsort(scores[indices])[::-1]]
504504
for indices in cluster_indices]
505505
groups = [
506506
namespace(scores=scores[indices], indices=indices, label=label,
@@ -515,7 +515,7 @@ def setScores(self, scores, labels, values, colors, rownames=None):
515515

516516
def setRowNames(self, names):
517517
if names is not None:
518-
names = numpy.asarray(names, dtype=object)
518+
names = np.asarray(names, dtype=object)
519519

520520
layout = self.layout()
521521

@@ -589,13 +589,15 @@ def clear(self):
589589

590590
def __setup(self):
591591
# Setup the subwidgets/groups/layout
592-
smax = max((numpy.max(g.scores) for g in self.__groups
592+
smax = max((np.nanmax(g.scores) for g in self.__groups
593593
if g.scores.size),
594594
default=1)
595+
smax = 1 if np.isnan(smax) else smax
595596

596-
smin = min((numpy.min(g.scores) for g in self.__groups
597+
smin = min((np.nanmin(g.scores) for g in self.__groups
597598
if g.scores.size),
598599
default=-1)
600+
smin = -1 if np.isnan(smin) else smin
599601
smin = min(smin, 0)
600602

601603
font = self.font()
@@ -704,7 +706,7 @@ def mousePressEvent(self, event):
704706
rect=None,
705707
)
706708
if saction & SelectAction.Clear:
707-
self.__selstate.selection = numpy.array([], dtype=int)
709+
self.__selstate.selection = np.array([], dtype=int)
708710
self.setSelection(self.__selstate.selection)
709711
event.accept()
710712

@@ -761,9 +763,9 @@ def mouseReleaseEvent(self, event):
761763
self.__selstate = None
762764

763765
def __move_selection(self, selection, offset):
764-
ids = numpy.asarray([pi.data(0) for pi in self.__plotItems()]).ravel()
765-
indices = [numpy.where(ids == i)[0] for i in selection]
766-
indices = numpy.asarray(indices) + offset
766+
ids = np.asarray([pi.data(0) for pi in self.__plotItems()]).ravel()
767+
indices = [np.where(ids == i)[0] for i in selection]
768+
indices = np.asarray(indices) + offset
767769
if min(indices) >= 0 and max(indices) < len(ids):
768770
self.setSelection(ids[indices])
769771

@@ -786,18 +788,18 @@ def __setSelectionRect(self, rect, action):
786788
selection = self.__selection
787789

788790
if action & SelectAction.Toogle:
789-
selection = numpy.setxor1d(selection, indices)
791+
selection = np.setxor1d(selection, indices)
790792
elif action & SelectAction.Deselect:
791-
selection = numpy.setdiff1d(selection, indices)
793+
selection = np.setdiff1d(selection, indices)
792794
elif action & SelectAction.Select:
793-
selection = numpy.union1d(selection, indices)
795+
selection = np.union1d(selection, indices)
794796

795797
self.setSelection(selection)
796798

797799
def __selectionIndices(self, rect):
798800
items = [item for item in self.__plotItems()
799801
if item.geometry().intersects(rect)]
800-
selection = [numpy.array([], dtype=int)]
802+
selection = [np.array([], dtype=int)]
801803
for item in items:
802804
indices = item.data(0)
803805
itemrect = item.geometry().intersected(rect)
@@ -806,10 +808,10 @@ def __selectionIndices(self, rect):
806808
.intersected(crect))
807809
assert itemrect.top() >= 0
808810
rowh = crect.height() / item.count()
809-
indextop = numpy.floor(itemrect.top() / rowh)
810-
indexbottom = numpy.ceil(itemrect.bottom() / rowh)
811+
indextop = np.floor(itemrect.top() / rowh)
812+
indexbottom = np.ceil(itemrect.bottom() / rowh)
811813
selection.append(indices[int(indextop): int(indexbottom)])
812-
return numpy.hstack(selection)
814+
return np.hstack(selection)
813815

814816
def itemAtPos(self, pos):
815817
items = [item for item in self.__plotItems()
@@ -825,7 +827,7 @@ def itemAtPos(self, pos):
825827

826828
assert pos.x() >= 0
827829
rowh = crect.height() / item.count()
828-
index = int(numpy.floor(pos.y() / rowh))
830+
index = int(np.floor(pos.y() / rowh))
829831
index = min(index, item.count() - 1)
830832
if index >= 0:
831833
return item.items()[index]
@@ -840,7 +842,7 @@ def indexAtPos(self, pos):
840842
else:
841843
item = items[0]
842844
indices = item.data(0)
843-
assert (isinstance(indices, numpy.ndarray) and
845+
assert (isinstance(indices, np.ndarray) and
844846
indices.shape == (item.count(),))
845847
crect = item.contentsRect()
846848
pos = item.mapFromParent(pos)
@@ -849,7 +851,7 @@ def indexAtPos(self, pos):
849851

850852
assert pos.x() >= 0
851853
rowh = crect.height() / item.count()
852-
index = numpy.floor(pos.y() / rowh)
854+
index = np.floor(pos.y() / rowh)
853855
index = min(index, indices.size - 1)
854856

855857
if index >= 0:
@@ -859,16 +861,16 @@ def indexAtPos(self, pos):
859861

860862
def __selectionChanged(self, selected, deselected):
861863
for item, grp in zip(self.__plotItems(), self.__groups):
862-
select = numpy.flatnonzero(
863-
numpy.in1d(grp.indices, selected, assume_unique=True))
864+
select = np.flatnonzero(
865+
np.in1d(grp.indices, selected, assume_unique=True))
864866
items = item.items()
865867
if select.size:
866868
for i in select:
867-
color = numpy.hstack((grp.color, numpy.array([130])))
869+
color = np.hstack((grp.color, np.array([130])))
868870
items[i].setBrush(QBrush(QColor(*color)))
869871

870-
deselect = numpy.flatnonzero(
871-
numpy.in1d(grp.indices, deselected, assume_unique=True))
872+
deselect = np.flatnonzero(
873+
np.in1d(grp.indices, deselected, assume_unique=True))
872874
if deselect.size:
873875
for i in deselect:
874876
items[i].setBrush(QBrush(QColor(*grp.color)))
@@ -888,9 +890,9 @@ def __textItems(self):
888890
yield item
889891

890892
def setSelection(self, indices):
891-
indices = numpy.unique(numpy.asarray(indices, dtype=int))
892-
select = numpy.setdiff1d(indices, self.__selection)
893-
deselect = numpy.setdiff1d(self.__selection, indices)
893+
indices = np.unique(np.asarray(indices, dtype=int))
894+
select = np.setdiff1d(indices, self.__selection)
895+
deselect = np.setdiff1d(self.__selection, indices)
894896

895897
self.__selectionChanged(select, deselect)
896898

@@ -900,7 +902,7 @@ def setSelection(self, indices):
900902
self.selectionChanged.emit()
901903

902904
def selection(self):
903-
return numpy.asarray(self.__selection, dtype=int)
905+
return np.asarray(self.__selection, dtype=int)
904906

905907

906908
class BarPlotItem(QGraphicsWidget):
@@ -911,7 +913,7 @@ def __init__(self, parent=None, **kwargs):
911913
self.__pen = QPen(Qt.NoPen)
912914
self.__brush = QBrush(QColor("#3FCFCF"))
913915
self.__range = (0., 1.)
914-
self.__data = numpy.array([], dtype=float)
916+
self.__data = np.array([], dtype=float)
915917
self.__items = []
916918

917919
def count(self):
@@ -961,7 +963,7 @@ def brush(self):
961963
return QBrush(self.__brush)
962964

963965
def setPlotData(self, values):
964-
self.__data = numpy.array(values, copy=True)
966+
self.__data = np.array(values, copy=True)
965967
self.__update()
966968
self.updateGeometry()
967969

Orange/widgets/visualize/tests/test_owsilhouetteplot.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# pylint: disable=protected-access
12
# Test methods with long descriptive names can omit docstrings
23
# pylint: disable=missing-docstring
34
import random
@@ -6,6 +7,7 @@
67
import numpy as np
78

89
import Orange.data
10+
from Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable
911
from Orange.widgets.utils.annotated_data import ANNOTATED_DATA_SIGNAL_NAME
1012
from Orange.widgets.visualize.owsilhouetteplot import OWSilhouettePlot
1113
from Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin
@@ -105,3 +107,21 @@ def test_memory_error(self):
105107
self.widget._effective_data = data
106108
self.widget._update()
107109
self.assertTrue(self.widget.Error.memory_error.is_shown())
110+
111+
def test_bad_data_range(self):
112+
"""
113+
Silhouette Plot now sets axis range properly.
114+
GH-2377
115+
"""
116+
nan = np.NaN
117+
table = Table(
118+
Domain(
119+
[ContinuousVariable("a"), ContinuousVariable("b"), ContinuousVariable("c")],
120+
[DiscreteVariable("d", values=["y", "n"])]),
121+
list(zip([4, nan, nan],
122+
[15, nan, nan],
123+
[16, nan, nan],
124+
"nyy"))
125+
)
126+
self.widget.controls.add_scores.setChecked(1)
127+
self.send_signal(self.widget.Inputs.data, table)

0 commit comments

Comments
 (0)