Skip to content

Commit 979d399

Browse files
authored
Merge pull request #3572 from pavlin-policar/louvain-num-clusters
[ENH] Louvain show number of clusters
2 parents 5eba886 + 40762e2 commit 979d399

File tree

1 file changed

+47
-38
lines changed

1 file changed

+47
-38
lines changed

Orange/widgets/unsupervised/owlouvainclustering.py

Lines changed: 47 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from AnyQt.QtCore import (
1313
Qt, QObject, QTimer, pyqtSignal as Signal, pyqtSlot as Slot
1414
)
15-
from AnyQt.QtWidgets import QSlider, QCheckBox, QWidget
15+
from AnyQt.QtWidgets import QSlider, QCheckBox, QWidget, QLabel
1616

1717
from Orange.clustering.louvain import table_to_knn_graph, Louvain
1818
from Orange.data import Table, DiscreteVariable
@@ -40,26 +40,26 @@
4040
_DEFAULT_K_NEIGHBORS = 30
4141

4242

43-
METRICS = [('Euclidean', 'l2'), ('Manhattan', 'l1')]
43+
METRICS = [("Euclidean", "l2"), ("Manhattan", "l1")]
4444

4545

4646
class OWLouvainClustering(widget.OWWidget):
47-
name = 'Louvain Clustering'
48-
description = 'Detects communities in a network of nearest neighbors.'
49-
icon = 'icons/LouvainClustering.svg'
47+
name = "Louvain Clustering"
48+
description = "Detects communities in a network of nearest neighbors."
49+
icon = "icons/LouvainClustering.svg"
5050
priority = 2110
5151

5252
want_main_area = False
5353

5454
settingsHandler = DomainContextHandler()
5555

5656
class Inputs:
57-
data = Input('Data', Table, default=True)
57+
data = Input("Data", Table, default=True)
5858

5959
if Graph is not None:
6060
class Outputs:
6161
annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table, default=True)
62-
graph = Output('Network', Graph)
62+
graph = Output("Network", Graph)
6363
else:
6464
class Outputs:
6565
annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table, default=True)
@@ -75,8 +75,7 @@ class Information(widget.OWWidget.Information):
7575
modified = Msg("Press commit to recompute clusters and send new data")
7676

7777
class Error(widget.OWWidget.Error):
78-
empty_dataset = Msg('No features in data')
79-
general_error = Msg('Error occured during clustering\n{}')
78+
empty_dataset = Msg("No features in data")
8079

8180
def __init__(self):
8281
super().__init__()
@@ -98,40 +97,44 @@ def __init__(self):
9897
self.__commit_timer = QTimer(self, singleShot=True)
9998
self.__commit_timer.timeout.connect(self.commit)
10099

101-
pca_box = gui.vBox(self.controlArea, 'PCA Preprocessing')
100+
# Set up UI
101+
info_box = gui.vBox(self.controlArea, "Info")
102+
self.info_label = gui.widgetLabel(info_box, "No data on input.") # type: QLabel
103+
104+
pca_box = gui.vBox(self.controlArea, "PCA Preprocessing")
102105
self.apply_pca_cbx = gui.checkBox(
103-
pca_box, self, 'apply_pca', label='Apply PCA preprocessing',
106+
pca_box, self, "apply_pca", label="Apply PCA preprocessing",
104107
callback=self._invalidate_graph,
105108
) # type: QCheckBox
106109
self.pca_components_slider = gui.hSlider(
107-
pca_box, self, 'pca_components', label='Components: ', minValue=2,
110+
pca_box, self, "pca_components", label="Components: ", minValue=2,
108111
maxValue=_MAX_PCA_COMPONENTS,
109112
callback=self._invalidate_pca_projection, tracking=False
110113
) # type: QSlider
111114

112-
graph_box = gui.vBox(self.controlArea, 'Graph parameters')
115+
graph_box = gui.vBox(self.controlArea, "Graph parameters")
113116
self.metric_combo = gui.comboBox(
114-
graph_box, self, 'metric_idx', label='Distance metric',
117+
graph_box, self, "metric_idx", label="Distance metric",
115118
items=[m[0] for m in METRICS], callback=self._invalidate_graph,
116119
orientation=Qt.Horizontal,
117120
) # type: gui.OrangeComboBox
118121
self.k_neighbors_spin = gui.spin(
119-
graph_box, self, 'k_neighbors', minv=1, maxv=_MAX_K_NEIGBOURS,
120-
label='k neighbors', controlWidth=80, alignment=Qt.AlignRight,
122+
graph_box, self, "k_neighbors", minv=1, maxv=_MAX_K_NEIGBOURS,
123+
label="k neighbors", controlWidth=80, alignment=Qt.AlignRight,
121124
callback=self._invalidate_graph,
122125
) # type: gui.SpinBoxWFocusOut
123126
self.resolution_spin = gui.hSlider(
124-
graph_box, self, 'resolution', minValue=0, maxValue=5., step=1e-1,
125-
label='Resolution', intOnly=False, labelFormat='%.1f',
127+
graph_box, self, "resolution", minValue=0, maxValue=5., step=1e-1,
128+
label="Resolution", intOnly=False, labelFormat="%.1f",
126129
callback=self._invalidate_partition, tracking=False,
127130
) # type: QSlider
128131
self.resolution_spin.parent().setToolTip(
129-
'The resolution parameter affects the number of clusters to find. '
130-
'Smaller values tend to produce more clusters and larger values '
131-
'retrieve less clusters.'
132+
"The resolution parameter affects the number of clusters to find. "
133+
"Smaller values tend to produce more clusters and larger values "
134+
"retrieve less clusters."
132135
)
133136
self.apply_button = gui.auto_commit(
134-
self.controlArea, self, 'auto_commit', 'Apply', box=None,
137+
self.controlArea, self, "auto_commit", "Apply", box=None,
135138
commit=lambda: self.commit(),
136139
callback=lambda: self._on_auto_commit_changed(),
137140
) # type: QWidget
@@ -248,6 +251,7 @@ def commit(self):
248251
run_on_graph, graph, resolution=self.resolution, state=state
249252
)
250253

254+
self.info_label.setText("Running...")
251255
self.__set_state_busy()
252256
self.__start_task(task, state)
253257

@@ -269,7 +273,7 @@ def __set_partial_results(self, result):
269273

270274
@Slot(object)
271275
def __on_done(self, future):
272-
# type: (Future['Results']) -> None
276+
# type: (Future["Results"]) -> None
273277
assert future.done()
274278
assert self.__task is not None
275279
assert self.__task.future is future
@@ -278,12 +282,9 @@ def __on_done(self, future):
278282
task.deleteLater()
279283

280284
self.__set_state_ready()
281-
try:
282-
result = future.result()
283-
except Exception as err: # pylint: disable=broad-except
284-
self.Error.general_error(str(err), exc_info=True)
285-
else:
286-
self.__set_results(result)
285+
286+
result = future.result()
287+
self.__set_results(result)
287288

288289
@Slot(str)
289290
def setStatusMessage(self, text):
@@ -330,7 +331,7 @@ def __cancel_task(self, wait=True):
330331
w.done.connect(state.deleteLater)
331332

332333
def __set_results(self, results):
333-
# type: ('Results') -> None
334+
# type: ("Results") -> None
334335
# NOTE: All of these have already been set by __set_partial_results,
335336
# we double check that they are aliases
336337
if results.pca_projection is not None:
@@ -346,6 +347,11 @@ def __set_results(self, results):
346347
assert results.resolution == self.resolution
347348
assert self.partition is results.partition
348349
self.partition = results.partition
350+
351+
# Display the number of found clusters in the UI
352+
num_clusters = len(np.unique(self.partition))
353+
self.info_label.setText("%d clusters found." % num_clusters)
354+
349355
self._send_data()
350356

351357
def _send_data(self):
@@ -359,8 +365,8 @@ def _send_data(self):
359365
new_partition = list(map(index_map.get, self.partition))
360366

361367
cluster_var = DiscreteVariable(
362-
get_unique_names(domain, 'Cluster'),
363-
values=['C%d' % (i + 1) for i, _ in enumerate(np.unique(new_partition))]
368+
get_unique_names(domain, "Cluster"),
369+
values=["C%d" % (i + 1) for i, _ in enumerate(np.unique(new_partition))]
364370
)
365371

366372
new_domain = add_columns(domain, metas=[cluster_var])
@@ -406,6 +412,8 @@ def set_data(self, data):
406412
self.k_neighbors_spin.setMaximum(min(_MAX_K_NEIGBOURS, len(data) - 1))
407413
self.k_neighbors_spin.setValue(min(_DEFAULT_K_NEIGHBORS, len(data) - 1))
408414

415+
self.info_label.setText("Clustering not yet run.")
416+
409417
self.commit()
410418

411419
def clear(self):
@@ -416,6 +424,7 @@ def clear(self):
416424
self.partition = None
417425
self.Error.clear()
418426
self.Information.modified.clear()
427+
self.info_label.setText("No data on input.")
419428

420429
def onDeleteWidget(self):
421430
self.__cancel_task(wait=True)
@@ -427,13 +436,13 @@ def onDeleteWidget(self):
427436
def send_report(self):
428437
pca = report.bool_str(self.apply_pca)
429438
if self.apply_pca:
430-
pca += report.plural(', {number} component{s}', self.pca_components)
439+
pca += report.plural(", {number} component{s}", self.pca_components)
431440

432441
self.report_items((
433-
('PCA preprocessing', pca),
434-
('Metric', METRICS[self.metric_idx][0]),
435-
('k neighbors', self.k_neighbors),
436-
('Resolution', self.resolution),
442+
("PCA preprocessing", pca),
443+
("Metric", METRICS[self.metric_idx][0]),
444+
("k neighbors", self.k_neighbors),
445+
("Resolution", self.resolution),
437446
))
438447

439448

@@ -614,5 +623,5 @@ def run_on_graph(graph, resolution, state):
614623
return res
615624

616625

617-
if __name__ == '__main__': # pragma: no cover
626+
if __name__ == "__main__": # pragma: no cover
618627
WidgetPreview(OWLouvainClustering).run(Table("iris"))

0 commit comments

Comments
 (0)