Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 57 additions & 55 deletions Orange/widgets/visualize/owsilhouetteplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
if sys.version_info > (3, 5):
from typing import Optional

import numpy
import numpy as np
import sklearn.metrics

from AnyQt.QtWidgets import (
Expand Down Expand Up @@ -99,13 +99,13 @@ def __init__(self):
self._matrix = None # type: Optional[Orange.misc.DistMatrix]
#: An bool mask (size == len(data)) indicating missing group/cluster
#: assignments
self._mask = None # type: Optional[numpy.ndarray]
self._mask = None # type: Optional[np.ndarray]
#: An array of cluster/group labels for instances with valid group
#: assignment
self._labels = None # type: Optional[numpy.ndarray]
self._labels = None # type: Optional[np.ndarray]
#: An array of silhouette scores for instances with valid group
#: assignment
self._silhouette = None # type: Optional[numpy.ndarray]
self._silhouette = None # type: Optional[np.ndarray]
self._silplot = None # type: Optional[SilhouettePlot]

gui.comboBox(
Expand Down Expand Up @@ -263,7 +263,7 @@ def _update(self):
if self._matrix is None and self._effective_data is not None:
_, metric = self.Distances[self.distance_idx]
try:
self._matrix = numpy.asarray(metric(self._effective_data))
self._matrix = np.asarray(metric(self._effective_data))
except MemoryError:
self.Error.memory_error()
return
Expand All @@ -286,12 +286,12 @@ def _clear_messages(self):
def _update_labels(self):
labelvar = self.cluster_var_model[self.cluster_var_idx]
labels, _ = self.data.get_column_view(labelvar)
labels = numpy.asarray(labels, dtype=float)
mask = numpy.isnan(labels)
labels = np.asarray(labels, dtype=float)
mask = np.isnan(labels)
labels = labels.astype(int)
labels = labels[~mask]

labels_unq, _ = numpy.unique(labels, return_counts=True)
labels_unq, _ = np.unique(labels, return_counts=True)

if len(labels_unq) < 2:
self.Error.need_two_clusters()
Expand All @@ -307,7 +307,7 @@ def _update_labels(self):
self._silhouette = silhouette

if labels is not None:
count_missing = numpy.count_nonzero(mask)
count_missing = np.count_nonzero(mask)
if count_missing:
self.Warning.missing_cluster_assignment(
count_missing, s="s" if count_missing > 1 else "")
Expand All @@ -333,8 +333,8 @@ def _replot(self):
else:
silplot.setScores(
self._silhouette,
numpy.zeros(len(self._silhouette), dtype=int),
[""], numpy.array([[63, 207, 207]])
np.zeros(len(self._silhouette), dtype=int),
[""], np.array([[63, 207, 207]])
)

self.scene.addItem(silplot)
Expand Down Expand Up @@ -379,17 +379,17 @@ def commit(self):
"""
selected = indices = data = None
if self.data is not None:
selectedmask = numpy.full(len(self.data), False, dtype=bool)
selectedmask = np.full(len(self.data), False, dtype=bool)
if self._silplot is not None:
indices = self._silplot.selection()
assert (numpy.diff(indices) > 0).all(), "strictly increasing"
assert (np.diff(indices) > 0).all(), "strictly increasing"
if self._mask is not None:
indices = numpy.flatnonzero(~self._mask)[indices]
indices = np.flatnonzero(~self._mask)[indices]
selectedmask[indices] = True

if self._mask is not None:
scores = numpy.full(shape=selectedmask.shape,
fill_value=numpy.nan)
scores = np.full(shape=selectedmask.shape,
fill_value=np.nan)
scores[~self._mask] = self._silhouette
else:
scores = self._silhouette
Expand All @@ -408,14 +408,14 @@ def commit(self):
domain = self.data.domain
data = self.data

if numpy.count_nonzero(selectedmask):
if np.count_nonzero(selectedmask):
selected = self.data.from_table(
domain, self.data, numpy.flatnonzero(selectedmask))
domain, self.data, np.flatnonzero(selectedmask))

if self.add_scores:
if selected is not None:
selected[:, silhouette_var] = numpy.c_[scores[selectedmask]]
data[:, silhouette_var] = numpy.c_[scores]
selected[:, silhouette_var] = np.c_[scores[selectedmask]]
data[:, silhouette_var] = np.c_[scores]

self.Outputs.selected_data.send(selected)
self.Outputs.annotated_data.send(create_annotated_table(data, indices))
Expand Down Expand Up @@ -456,7 +456,7 @@ def __init__(self, parent=None, **kwargs):
self.__rowNamesVisible = True
self.__barHeight = 3
self.__selectionRect = None
self.__selection = numpy.asarray([], dtype=int)
self.__selection = np.asarray([], dtype=int)
self.__selstate = None
self.__pen = QPen(Qt.NoPen)
self.__layout = QGraphicsGridLayout()
Expand All @@ -482,10 +482,10 @@ def setScores(self, scores, labels, values, colors, rownames=None):
rownames : list of str, optional
A list (len == N) of row names.
"""
scores = numpy.asarray(scores, dtype=float)
labels = numpy.asarray(labels, dtype=int)
scores = np.asarray(scores, dtype=float)
labels = np.asarray(labels, dtype=int)
if rownames is not None:
rownames = numpy.asarray(rownames, dtype=object)
rownames = np.asarray(rownames, dtype=object)

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

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

def setRowNames(self, names):
if names is not None:
names = numpy.asarray(names, dtype=object)
names = np.asarray(names, dtype=object)

layout = self.layout()

Expand Down Expand Up @@ -589,13 +589,15 @@ def clear(self):

def __setup(self):
# Setup the subwidgets/groups/layout
smax = max((numpy.max(g.scores) for g in self.__groups
smax = max((np.nanmax(g.scores) for g in self.__groups
if g.scores.size),
default=1)
smax = 1 if np.isnan(smax) else smax

smin = min((numpy.min(g.scores) for g in self.__groups
smin = min((np.nanmin(g.scores) for g in self.__groups
if g.scores.size),
default=-1)
smin = -1 if np.isnan(smin) else smin
smin = min(smin, 0)

font = self.font()
Expand Down Expand Up @@ -704,7 +706,7 @@ def mousePressEvent(self, event):
rect=None,
)
if saction & SelectAction.Clear:
self.__selstate.selection = numpy.array([], dtype=int)
self.__selstate.selection = np.array([], dtype=int)
self.setSelection(self.__selstate.selection)
event.accept()

Expand Down Expand Up @@ -761,9 +763,9 @@ def mouseReleaseEvent(self, event):
self.__selstate = None

def __move_selection(self, selection, offset):
ids = numpy.asarray([pi.data(0) for pi in self.__plotItems()]).ravel()
indices = [numpy.where(ids == i)[0] for i in selection]
indices = numpy.asarray(indices) + offset
ids = np.asarray([pi.data(0) for pi in self.__plotItems()]).ravel()
indices = [np.where(ids == i)[0] for i in selection]
indices = np.asarray(indices) + offset
if min(indices) >= 0 and max(indices) < len(ids):
self.setSelection(ids[indices])

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

if action & SelectAction.Toogle:
selection = numpy.setxor1d(selection, indices)
selection = np.setxor1d(selection, indices)
elif action & SelectAction.Deselect:
selection = numpy.setdiff1d(selection, indices)
selection = np.setdiff1d(selection, indices)
elif action & SelectAction.Select:
selection = numpy.union1d(selection, indices)
selection = np.union1d(selection, indices)

self.setSelection(selection)

def __selectionIndices(self, rect):
items = [item for item in self.__plotItems()
if item.geometry().intersects(rect)]
selection = [numpy.array([], dtype=int)]
selection = [np.array([], dtype=int)]
for item in items:
indices = item.data(0)
itemrect = item.geometry().intersected(rect)
Expand All @@ -806,10 +808,10 @@ def __selectionIndices(self, rect):
.intersected(crect))
assert itemrect.top() >= 0
rowh = crect.height() / item.count()
indextop = numpy.floor(itemrect.top() / rowh)
indexbottom = numpy.ceil(itemrect.bottom() / rowh)
indextop = np.floor(itemrect.top() / rowh)
indexbottom = np.ceil(itemrect.bottom() / rowh)
selection.append(indices[int(indextop): int(indexbottom)])
return numpy.hstack(selection)
return np.hstack(selection)

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

assert pos.x() >= 0
rowh = crect.height() / item.count()
index = int(numpy.floor(pos.y() / rowh))
index = int(np.floor(pos.y() / rowh))
index = min(index, item.count() - 1)
if index >= 0:
return item.items()[index]
Expand All @@ -840,7 +842,7 @@ def indexAtPos(self, pos):
else:
item = items[0]
indices = item.data(0)
assert (isinstance(indices, numpy.ndarray) and
assert (isinstance(indices, np.ndarray) and
indices.shape == (item.count(),))
crect = item.contentsRect()
pos = item.mapFromParent(pos)
Expand All @@ -849,7 +851,7 @@ def indexAtPos(self, pos):

assert pos.x() >= 0
rowh = crect.height() / item.count()
index = numpy.floor(pos.y() / rowh)
index = np.floor(pos.y() / rowh)
index = min(index, indices.size - 1)

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

def __selectionChanged(self, selected, deselected):
for item, grp in zip(self.__plotItems(), self.__groups):
select = numpy.flatnonzero(
numpy.in1d(grp.indices, selected, assume_unique=True))
select = np.flatnonzero(
np.in1d(grp.indices, selected, assume_unique=True))
items = item.items()
if select.size:
for i in select:
color = numpy.hstack((grp.color, numpy.array([130])))
color = np.hstack((grp.color, np.array([130])))
items[i].setBrush(QBrush(QColor(*color)))

deselect = numpy.flatnonzero(
numpy.in1d(grp.indices, deselected, assume_unique=True))
deselect = np.flatnonzero(
np.in1d(grp.indices, deselected, assume_unique=True))
if deselect.size:
for i in deselect:
items[i].setBrush(QBrush(QColor(*grp.color)))
Expand All @@ -888,9 +890,9 @@ def __textItems(self):
yield item

def setSelection(self, indices):
indices = numpy.unique(numpy.asarray(indices, dtype=int))
select = numpy.setdiff1d(indices, self.__selection)
deselect = numpy.setdiff1d(self.__selection, indices)
indices = np.unique(np.asarray(indices, dtype=int))
select = np.setdiff1d(indices, self.__selection)
deselect = np.setdiff1d(self.__selection, indices)

self.__selectionChanged(select, deselect)

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

def selection(self):
return numpy.asarray(self.__selection, dtype=int)
return np.asarray(self.__selection, dtype=int)


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

def count(self):
Expand Down Expand Up @@ -961,7 +963,7 @@ def brush(self):
return QBrush(self.__brush)

def setPlotData(self, values):
self.__data = numpy.array(values, copy=True)
self.__data = np.array(values, copy=True)
self.__update()
self.updateGeometry()

Expand Down
20 changes: 20 additions & 0 deletions Orange/widgets/visualize/tests/test_owsilhouetteplot.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pylint: disable=protected-access
# Test methods with long descriptive names can omit docstrings
# pylint: disable=missing-docstring
import random
Expand All @@ -6,6 +7,7 @@
import numpy as np

import Orange.data
from Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable
from Orange.widgets.utils.annotated_data import ANNOTATED_DATA_SIGNAL_NAME
from Orange.widgets.visualize.owsilhouetteplot import OWSilhouettePlot
from Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin
Expand Down Expand Up @@ -105,3 +107,21 @@ def test_memory_error(self):
self.widget._effective_data = data
self.widget._update()
self.assertTrue(self.widget.Error.memory_error.is_shown())

def test_bad_data_range(self):
"""
Silhouette Plot now sets axis range properly.
GH-2377
"""
nan = np.NaN
table = Table(
Domain(
[ContinuousVariable("a"), ContinuousVariable("b"), ContinuousVariable("c")],
[DiscreteVariable("d", values=["y", "n"])]),
list(zip([4, nan, nan],
[15, nan, nan],
[16, nan, nan],
"nyy"))
)
self.widget.controls.add_scores.setChecked(1)
self.send_signal(self.widget.Inputs.data, table)