Skip to content

Commit be714f1

Browse files
committed
OWSOM: Selection groups and minor fixes
1 parent 99ffcb5 commit be714f1

File tree

2 files changed

+226
-165
lines changed

2 files changed

+226
-165
lines changed

Orange/widgets/unsupervised/owsom.py

Lines changed: 114 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
from Orange.widgets.utils.itemmodels import DomainModel
2323
from Orange.widgets.utils.widgetpreview import WidgetPreview
2424
from Orange.widgets.utils.annotated_data import \
25-
create_annotated_table, ANNOTATED_DATA_SIGNAL_NAME
26-
from Orange.widgets.utils.colorpalette import ContinuousPaletteGenerator
25+
create_annotated_table, create_groups_table, ANNOTATED_DATA_SIGNAL_NAME
26+
from Orange.widgets.utils.colorpalette import \
27+
ContinuousPaletteGenerator, ColorPaletteGenerator
2728
from Orange.widgets.visualize.utils import CanvasRectangle, CanvasText
2829
from Orange.widgets.visualize.utils.plotutils import wrap_legend_items
2930

@@ -32,11 +33,11 @@
3233

3334

3435
class SomView(QGraphicsView):
35-
SelectionClear, SelectionAdd, SelectionRemove, SelectionToggle = 1, 2, 4, 8
36-
SelectionSet = SelectionClear | SelectionAdd
37-
selection_changed = Signal(set, int)
36+
SelectionSet, SelectionNewGroup, SelectionAddToGroup, SelectionRemove \
37+
= range(4)
38+
selection_changed = Signal(np.ndarray, int)
3839
selection_moved = Signal(QKeyEvent)
39-
selection_mark_changed = Signal(set)
40+
selection_mark_changed = Signal(np.ndarray)
4041

4142
def __init__(self, scene):
4243
super().__init__(scene)
@@ -56,32 +57,28 @@ def _get_marked_cells(self, event):
5657
x0, x1 = sorted((x0, x1))
5758
y0, y1 = sorted((y0, y1))
5859

60+
selection = np.zeros((self.size_x, self.size_y), dtype=bool)
5961
if self.hexagonal:
6062
y0 = max(0, int(y0 / sqrt3_2 + 0.5))
6163
y1 = min(self.size_y, int(np.ceil(y1 / sqrt3_2 + 0.5)))
62-
selection = set()
6364
for y in range(y0, y1):
6465
x0_ = max(0, int(x0 + 0.5 - (y % 2) / 2))
6566
x1_ = min(self.size_x - y % 2,
6667
int(np.ceil(x1 + 0.5 - (y % 2) / 2)))
67-
selection |= {(x, y) for x in range(x0_, x1_)}
68-
return selection
68+
selection[x0_:x1_, y] = True
69+
elif not(x1 < -0.5 or x0 > self.size_x - 0.5
70+
or y1 < -0.5 or y0 > self.size_y - 0.5):
6971

70-
else:
7172
def roundclip(z, zmax):
7273
return int(np.clip(np.round(z), 0, zmax - 1))\
7374

74-
if x1 < -0.5 or x0 > self.size_x - 0.5 \
75-
or y1 < -0.5 or y0 > self.size_y - 0.5:
76-
return set()
77-
7875
x0 = roundclip(x0, self.size_x)
7976
y0 = roundclip(y0, self.size_y)
8077
x1 = roundclip(x1, self.size_x)
8178
y1 = roundclip(y1, self.size_y)
82-
return {(x, y)
83-
for x in range(x0, x1 + 1)
84-
for y in range(y0, y1 + 1)}
79+
selection[x0:x1 + 1, y0:y1 + 1] = True
80+
81+
return selection
8582

8683
def mousePressEvent(self, event):
8784
if event.button() != Qt.LeftButton:
@@ -101,14 +98,15 @@ def mouseReleaseEvent(self, event):
10198
if event.button() != Qt.LeftButton:
10299
return
103100

104-
if event.modifiers() & Qt.ControlModifier:
105-
action = self.SelectionToggle
101+
if event.modifiers() & Qt.ShiftModifier:
102+
if event.modifiers() & Qt.ControlModifier:
103+
action = self.SelectionAddToGroup
104+
else:
105+
action = self.SelectionNewGroup
106106
elif event.modifiers() & Qt.AltModifier:
107107
action = self.SelectionRemove
108-
elif event.modifiers() & Qt.ShiftModifier:
109-
action = self.SelectionAdd
110108
else:
111-
action = self.SelectionClear | self.SelectionAdd
109+
action = self.SelectionSet
112110
selection = self._get_marked_cells(event)
113111
self.selection_changed.emit(selection, action)
114112
event.accept()
@@ -151,7 +149,7 @@ def paint(self, painter, _option, _index):
151149

152150

153151
class OWSOM(OWWidget):
154-
name = "Self-organizing Map"
152+
name = "Self-Organizing Map"
155153
description = "Computation of self-organizing map."
156154
icon = "icons/SOM.svg"
157155
keywords = ["SOM"]
@@ -173,7 +171,7 @@ class Outputs:
173171
attr_color = ContextSetting(None)
174172
size_by_instances = Setting(True)
175173
pie_charts = Setting(False)
176-
selection = Setting(set(), schema_only=True)
174+
selection = Setting(None, schema_only=True)
177175

178176
graph_name = "view"
179177

@@ -203,7 +201,7 @@ def __init__(self):
203201

204202
self.data = self.cont_x = None
205203
self.cells = self.member_data = None
206-
self.selection = set()
204+
self.selection = None
207205
self.colors = self.thresholds = None
208206

209207
box = gui.vBox(self.controlArea, box="SOM")
@@ -214,7 +212,7 @@ def __init__(self):
214212
box2 = gui.indentedBox(box, 10)
215213
auto_dim = gui.checkBox(
216214
box2, self, "auto_dimension", "Set dimensions automatically",
217-
callback=self.recompute_dimensions)
215+
callback=self.on_auto_dimension_changed)
218216
self.manual_box = box3 = gui.hBox(box2)
219217
spinargs = dict(
220218
value="", widget=box3, master=self, minv=5, maxv=100, step=5,
@@ -225,11 +223,12 @@ def __init__(self):
225223
spin_y = gui.spin(**spinargs)
226224
spin_x.setValue(self.size_y)
227225
gui.rubber(box3)
226+
self.manual_box.setEnabled(not self.auto_dimension)
228227

229228
initialization = gui.comboBox(
230229
box, self, "initialization",
231230
items=("Initialize with PCA", "Random initialization",
232-
"Replicable random"))
231+
"Replicable random"))
233232

234233
start = gui.button(
235234
box, self, "Restart", callback=self.restart_som_pressed,
@@ -299,7 +298,7 @@ def set_data(self, data):
299298
else:
300299
if np.all(mask):
301300
self.data = data
302-
self.cont_x = x
301+
self.cont_x = x.copy()
303302
else:
304303
self.data = data[mask]
305304
self.cont_x = x[mask]
@@ -350,15 +349,23 @@ def clear(self):
350349
self.Error.clear()
351350

352351
def recompute_dimensions(self):
352+
if not self.auto_dimension or self.cont_x is None:
353+
return
354+
dim = max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))
355+
self.opt_controls.spin_x.setValue(dim)
356+
self.opt_controls.spin_y.setValue(dim)
357+
358+
def on_auto_dimension_changed(self):
353359
self.manual_box.setEnabled(not self.auto_dimension)
354-
if not self.auto_dimension and self.cont_x is not None:
355-
dimx = dimy = \
356-
max(5, int(np.ceil(np.sqrt(5 * np.sqrt(self.cont_x.shape[0])))))
360+
if self.auto_dimension:
361+
self.recompute_dimensions()
357362
else:
358-
dimx = int(5 * np.round(self.size_x / 5))
359-
dimy = int(5 * np.round(self.size_y / 5))
360-
self.opt_controls.spin_x.setValue(dimx)
361-
self.opt_controls.spin_y.setValue(dimy)
363+
spin_x = self.opt_controls.spin_x
364+
spin_y = self.opt_controls.spin_y
365+
dimx = int(5 * np.round(spin_x.value() / 5))
366+
dimy = int(5 * np.round(spin_y.value() / 5))
367+
spin_x.setValue(dimx)
368+
spin_y.setValue(dimy)
362369

363370
def on_attr_color_change(self):
364371
self.controls.pie_charts.setEnabled(self.attr_color is not None)
@@ -374,33 +381,35 @@ def on_pie_chart_change(self):
374381
self._redraw()
375382

376383
def clear_selection(self):
377-
self.selection.clear()
384+
self.selection = None
378385
self.redraw_selection()
379386

380387
def on_selection_change(self, selection, action=SomView.SelectionSet):
381-
if action & SomView.SelectionClear:
382-
self.selection.clear()
383-
if action & SomView.SelectionAdd:
384-
self.selection |= selection
388+
if self.selection is None:
389+
self.selection = np.zeros(self.grid_cells.T.shape, dtype=np.int16)
390+
if action == SomView.SelectionSet:
391+
self.selection[:] = 0
392+
self.selection[selection] = 1
393+
elif action == SomView.SelectionAddToGroup:
394+
self.selection[selection] = max(1, np.max(self.selection))
395+
elif action == SomView.SelectionNewGroup:
396+
self.selection[selection] = 1 + np.max(self.selection)
385397
elif action & SomView.SelectionRemove:
386-
self.selection -= selection
387-
elif action & SomView.SelectionToggle:
388-
self.selection ^= selection
398+
self.selection[selection] = 0
389399
self.redraw_selection()
390400
self.update_output()
391401

392402
def on_selection_move(self, event: QKeyEvent):
393-
if len(self.selection) > 1:
394-
return
395-
396-
if not self.selection:
403+
if self.selection is None or not np.any(self.selection):
397404
if event.key() in (Qt.Key_Right, Qt.Key_Down):
398405
x = y = 0
399406
else:
400407
x = self.size_x - 1
401408
y = self.size_y - 1
402409
else:
403-
x, y = next(iter(self.selection))
410+
x, y = np.nonzero(self.selection)
411+
if len(x) > 1:
412+
return
404413
if event.key() == Qt.Key_Up and y > 0:
405414
y -= 1
406415
if event.key() == Qt.Key_Down and y < self.size_y - 1:
@@ -409,34 +418,45 @@ def on_selection_move(self, event: QKeyEvent):
409418
x -= 1
410419
if event.key() == Qt.Key_Right and x < self.size_x - 1:
411420
x += 1
421+
x -= self.hexagonal and x == self.size_x - 1 and y % 2
412422

413-
x -= self.hexagonal and x == self.size_x - 1 and y % 2
414-
if {(x, y)} != self.selection:
415-
self.on_selection_change({(x, y)})
423+
if self.selection is not None and self.selection[x, y]:
424+
return
425+
selection = np.zeros(self.grid_cells.shape, dtype=bool)
426+
selection[x, y] = True
427+
self.on_selection_change(selection)
416428

417429
def on_selection_mark_change(self, marks):
418430
self.redraw_selection(marks=marks)
419431

420432
def redraw_selection(self, marks=None):
421433
if self.grid_cells is None:
422434
return
423-
mark_brush = QBrush(QColor(224, 255, 255))
424-
brushes = [[QBrush(Qt.NoBrush), QBrush(QColor(240, 240, 255))],
425-
[mark_brush, mark_brush]]
435+
426436
sel_pen = QPen(QBrush(QColor(128, 128, 128)), 2)
427437
sel_pen.setCosmetic(True)
428438
mark_pen = QPen(QBrush(QColor(128, 128, 128)), 4)
429439
mark_pen.setCosmetic(True)
430-
pens = [[self._grid_pen, sel_pen],
431-
[mark_pen, mark_pen]]
440+
pens = [self._grid_pen, sel_pen]
441+
442+
mark_brush = QBrush(QColor(224, 255, 255))
443+
sels = self.selection is not None and np.max(self.selection)
444+
palette = ColorPaletteGenerator(number_of_colors=sels + 1)
445+
brushes = [QBrush(Qt.NoBrush)] + \
446+
[QBrush(palette[i].lighter(165)) for i in range(sels)]
447+
432448
for y in range(self.size_y):
433449
for x in range(self.size_x - (y % 2) * self.hexagonal):
434450
cell = self.grid_cells[y, x]
435-
selected = (x, y) in self.selection
436-
marked = bool(marks) and (x, y) in marks
437-
cell.setBrush(brushes[marked][selected])
438-
cell.setPen(pens[marked][selected])
439-
cell.setZValue(marked or selected)
451+
marked = marks is not None and marks[x, y]
452+
sel_group = self.selection is not None and self.selection[x, y]
453+
if marked:
454+
cell.setBrush(mark_brush)
455+
cell.setPen(mark_pen)
456+
else:
457+
cell.setBrush(brushes[sel_group])
458+
cell.setPen(pens[bool(sel_group)])
459+
cell.setZValue(marked or sel_group)
440460

441461
def restart_som_pressed(self):
442462
if self._optimizer_thread is not None:
@@ -445,11 +465,14 @@ def restart_som_pressed(self):
445465
self.start_som()
446466

447467
def start_som(self):
448-
self.read_controls()
468+
self.read_controls()
469+
self.update_layout()
470+
self.clear_selection()
471+
if self.cont_x is not None:
449472
self.enable_controls(False)
450-
self.update_layout()
451-
self.clear_selection()
452473
self._recompute_som()
474+
else:
475+
self.update_output()
453476

454477
def read_controls(self):
455478
c = self.opt_controls
@@ -502,7 +525,7 @@ def _grid_factors(self):
502525

503526
def _draw_same_color(self, sizes):
504527
fx, fy = self._grid_factors
505-
pen = QPen(QBrush(Qt.black), 4)
528+
pen = QPen(QBrush(Qt.black), 2)
506529
pen.setCosmetic(True)
507530
brush = QBrush(QColor(192, 192, 192))
508531
for y in range(self.size_y):
@@ -566,8 +589,8 @@ def _draw_colored_circles(self, sizes):
566589
self.Warning.missing_colors(self.attr_color.name)
567590
bc = np.bincount(color_dist, minlength=len(self.colors))
568591
color = self.colors[np.argmax(bc)]
569-
pen = QPen(QBrush(color), 4)
570-
brush = QBrush(color.lighter(200 - 100 * np.max(bc) / len(members)))
592+
pen = QPen(QBrush(color), 2)
593+
brush = QBrush(color.lighter(200 - 80 * np.max(bc) / len(members)))
571594
pen.setCosmetic(True)
572595
ellipse = QGraphicsEllipseItem()
573596
ellipse.setRect(x + (y % 2) * fx - r / 2, y * fy - r / 2, r, r)
@@ -723,19 +746,33 @@ def rescale(self):
723746
self.size_y - 0.5 + leg_height / scale)
724747

725748
def update_output(self):
726-
indices = []
727-
if self.data is not None:
728-
for (x, y) in self.selection:
729-
indices.extend(self.get_member_indices(x, y))
730-
if indices:
731-
self.Outputs.selected_data.send(self.data[indices])
732-
self.info.set_output_summary(str(len(indices)))
749+
if self.data is None:
750+
self.Outputs.selected_data.send(None)
751+
self.Outputs.annotated_data.send(None)
752+
self.info.set_output_summary(self.info.NoOutput)
753+
return
754+
755+
indices = np.zeros(len(self.data), dtype=int)
756+
if self.selection is not None and np.any(self.selection):
757+
for y in range(self.size_y):
758+
for x in range(self.size_x):
759+
rows = self.get_member_indices(x, y)
760+
indices[rows] = self.selection[x, y]
761+
762+
if np.any(indices):
763+
sel_data = create_groups_table(self.data, indices, False, "Group")
764+
self.Outputs.selected_data.send(sel_data)
765+
self.info.set_output_summary(str(len(sel_data)))
733766
else:
734767
self.Outputs.selected_data.send(None)
735768
self.info.set_output_summary(self.info.NoOutput)
736769

737-
self.Outputs.annotated_data.send(
738-
create_annotated_table(self.data, indices or None))
770+
if np.max(indices) > 1:
771+
annotated = create_groups_table(self.data, indices)
772+
else:
773+
annotated = create_annotated_table(
774+
self.data, np.flatnonzero(indices))
775+
self.Outputs.annotated_data.send(annotated)
739776

740777
def set_color_bins(self):
741778
if self.attr_color is None:
@@ -818,6 +855,4 @@ def _draw_hexagon():
818855

819856

820857
if __name__ == "__main__": # pragma: no cover
821-
WidgetPreview(OWSOM).run(Table("heart_disease"))
822-
# If run on sparse data, the widget core dumps if the user tries resizing it?!
823-
# WidgetPreview(OWSOM).run(Table("/Users/janez/Downloads/deerwester.pkl"))
858+
WidgetPreview(OWSOM).run(Table("iris"))

0 commit comments

Comments
 (0)