From dd82f19252efe9504758da01adf248fd8ff631b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Primo=C5=BE=20Godec?= Date: Mon, 1 Jul 2019 08:21:38 +0200 Subject: [PATCH 1/6] DBSCAN moved from Prototypes --- Orange/widgets/unsupervised/icons/DBSCAN.svg | 53 ++++++ Orange/widgets/unsupervised/owdbscan.py | 151 ++++++++++++++++++ .../unsupervised/tests/test_owdbscan.py | 119 ++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 Orange/widgets/unsupervised/icons/DBSCAN.svg create mode 100644 Orange/widgets/unsupervised/owdbscan.py create mode 100644 Orange/widgets/unsupervised/tests/test_owdbscan.py diff --git a/Orange/widgets/unsupervised/icons/DBSCAN.svg b/Orange/widgets/unsupervised/icons/DBSCAN.svg new file mode 100644 index 00000000000..6efec89a4b7 --- /dev/null +++ b/Orange/widgets/unsupervised/icons/DBSCAN.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Orange/widgets/unsupervised/owdbscan.py b/Orange/widgets/unsupervised/owdbscan.py new file mode 100644 index 00000000000..59b0a306d1f --- /dev/null +++ b/Orange/widgets/unsupervised/owdbscan.py @@ -0,0 +1,151 @@ +import sys + +import numpy as np +from AnyQt.QtWidgets import QLayout, QApplication +from AnyQt.QtCore import Qt + +from Orange.widgets import widget, gui +from Orange.widgets.settings import Setting +from Orange.data import Table, Domain, DiscreteVariable +from Orange.clustering import DBSCAN +from Orange import distance +from Orange.widgets.utils.annotated_data import ANNOTATED_DATA_SIGNAL_NAME +from Orange.widgets.utils.signals import Input, Output +from Orange.widgets.widget import Msg + + +class OWDBSCAN(widget.OWWidget): + name = "DBSCAN" + description = "Density-based spatial clustering." + icon = "icons/DBSCAN.svg" + priority = 2150 + + class Inputs: + data = Input("Data", Table) + + class Outputs: + annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) + + class Error(widget.OWWidget.Error): + not_enough_instances = Msg("Not enough unique data instances. " + "At least two are required.") + + METRICS = [ + ("Euclidean", "euclidean"), + ("Manhattan", "manhattan"), + ("Cosine", distance.Cosine), + ("Jaccard", distance.Jaccard), + # ("Spearman", distance.SpearmanR), + # ("Spearman absolute", distance.SpearmanRAbsolute), + # ("Pearson", distance.PearsonR), + # ("Pearson absolute", distance.PearsonRAbsolute), + ] + + min_samples = Setting(5) + eps = Setting(0.5) + metric_idx = Setting(0) + auto_commit = Setting(True) + + want_main_area = False + + def __init__(self): + super().__init__() + + self.data = None + self.db = None + self.model = None + + box = gui.widgetBox(self.controlArea, "Parameters") + gui.spin(box, self, "min_samples", 1, 100, 1, callback=self._invalidate, + label="Core point neighbors") + gui.doubleSpin(box, self, "eps", 0.1, 10, 0.01, + callback=self._invalidate, + label="Neighborhood distance") + + box = gui.widgetBox(self.controlArea, self.tr("Distance Metric")) + gui.comboBox(box, self, "metric_idx", + items=list(zip(*self.METRICS))[0], + callback=self._invalidate) + + gui.auto_commit(self.controlArea, self, "auto_commit", "Apply", + orientation=Qt.Horizontal) + gui.rubber(self.controlArea) + + self.controlArea.setMinimumWidth(self.controlArea.sizeHint().width()) + self.layout().setSizeConstraint(QLayout.SetFixedSize) + + def adjustSize(self): + self.ensurePolished() + self.resize(self.controlArea.sizeHint()) + + def check_data_size(self): + if len(self.data) < 2: + self.Error.not_enough_instances() + return False + return True + + def commit(self): + self.cluster() + + def cluster(self): + if not self.check_data_size(): + return + self.model = DBSCAN( + eps=self.eps, + min_samples=self.min_samples, + metric=self.METRICS[self.metric_idx][1] + ).get_model(self.data) + self.send_data() + + def send_data(self): + model = self.model + + clusters = [c if c >= 0 else np.nan for c in model.labels] + k = len(set(clusters) - {np.nan}) + clusters = np.array(clusters).reshape(len(self.data), 1) + core_samples = set(model.projector.core_sample_indices_) + in_core = np.array([1 if (i in core_samples) else 0 + for i in range(len(self.data))]) + in_core = in_core.reshape(len(self.data), 1) + + clust_var = DiscreteVariable( + "Cluster", values=["C%d" % (x + 1) for x in range(k)]) + in_core_var = DiscreteVariable("DBSCAN Core", values=["0", "1"]) + + domain = self.data.domain + attributes, classes = domain.attributes, domain.class_vars + meta_attrs = domain.metas + x, y, metas = self.data.X, self.data.Y, self.data.metas + + meta_attrs += (clust_var, ) + metas = np.hstack((metas, clusters)) + meta_attrs += (in_core_var, ) + metas = np.hstack((metas, in_core)) + + domain = Domain(attributes, classes, meta_attrs) + new_table = Table(domain, x, y, metas, self.data.W) + + self.Outputs.annotated_data.send(new_table) + + @Inputs.data + def set_data(self, data): + self.data = data + if self.data is None: + self.Outputs.annotated_data.send(None) + self.Error.clear() + if self.data is None: + return + self.unconditional_commit() + + def _invalidate(self): + self.commit() + + +if __name__ == "__main__": + a = QApplication(sys.argv) + ow = OWDBSCAN() + d = Table("iris.tab") + ow.set_data(d) + ow.show() + a.exec() + ow.saveSettings() diff --git a/Orange/widgets/unsupervised/tests/test_owdbscan.py b/Orange/widgets/unsupervised/tests/test_owdbscan.py new file mode 100644 index 00000000000..c53fe2ef81d --- /dev/null +++ b/Orange/widgets/unsupervised/tests/test_owdbscan.py @@ -0,0 +1,119 @@ +import numpy as np +from scipy.sparse import csr_matrix + +from Orange.data import Table +from Orange.widgets.tests.base import WidgetTest +from Orange.widgets.tests.utils import simulate +from Orange.widgets.unsupervised.owdbscan import OWDBSCAN + + +class TestOWCSVFileImport(WidgetTest): + def setUp(self): + self.widget = self.create_widget(OWDBSCAN) + self.iris = Table("iris") + + def tearDown(self): + self.widgets.remove(self.widget) + self.widget.onDeleteWidget() + self.widget = None + + def test_cluster(self): + w = self.widget + + self.send_signal(w.Inputs.data, self.iris) + + output = self.get_output(w.Outputs.annotated_data) + self.assertIsNotNone(output) + self.assertEqual(len(self.iris), len(output)) + self.assertTupleEqual(self.iris.X.shape, output.X.shape) + self.assertTupleEqual(self.iris.Y.shape, output.Y.shape) + self.assertEqual(2, output.metas.shape[1]) + + self.assertEqual("Cluster", str(output.domain.metas[0])) + self.assertEqual("DBSCAN Core", str(output.domain.metas[1])) + + def test_bad_input(self): + w = self.widget + + self.send_signal(w.Inputs.data, self.iris[:1]) + self.assertTrue(w.Error.not_enough_instances.is_shown()) + + self.send_signal(w.Inputs.data, self.iris[:2]) + self.assertFalse(w.Error.not_enough_instances.is_shown()) + + self.send_signal(w.Inputs.data, self.iris) + self.assertFalse(w.Error.not_enough_instances.is_shown()) + + def test_data_none(self): + w = self.widget + + self.send_signal(w.Inputs.data, self.iris[:5]) + self.send_signal(w.Inputs.data, None) + + output = self.get_output(w.Outputs.annotated_data) + self.assertIsNone(output) + + def test_change_eps(self): + w = self.widget + + self.send_signal(w.Inputs.data, self.iris) + + # change parameters + self.widget.controls.eps.valueChanged.emit(0.5) + output1 = self.get_output(w.Outputs.annotated_data) + self.widget.controls.eps.valueChanged.emit(1) + output2 = self.get_output(w.Outputs.annotated_data) + + # on this data higher eps has greater sum of clusters - less nan + # values + self.assertGreater(np.nansum(output2.metas[:, 0]), + np.nansum(output1.metas[:, 0])) + + def test_change_min_samples(self): + w = self.widget + + self.send_signal(w.Inputs.data, self.iris) + + # change parameters + self.widget.controls.min_samples.valueChanged.emit(5) + output1 = self.get_output(w.Outputs.annotated_data) + self.widget.controls.min_samples.valueChanged.emit(1) + output2 = self.get_output(w.Outputs.annotated_data) + + # on this data lower min_samples has greater sum of clusters - less nan + # values + self.assertGreater(np.nansum(output2.metas[:, 0]), + np.nansum(output1.metas[:, 0])) + + def test_change_metric_idx(self): + w = self.widget + + self.send_signal(w.Inputs.data, self.iris) + + # change parameters + cbox = self.widget.controls.metric_idx + simulate.combobox_activate_index(cbox, 0) # Euclidean + output1 = self.get_output(w.Outputs.annotated_data) + simulate.combobox_activate_index(cbox, 1) # Manhattan + output2 = self.get_output(w.Outputs.annotated_data) + + # Manhattan has more nan clusters + self.assertGreater(np.nansum(output1.metas[:, 0]), + np.nansum(output2.metas[:, 0])) + + def test_sparse_data(self): + self.iris.X = csr_matrix(self.iris.X) + + w = self.widget + + self.send_signal(w.Inputs.data, self.iris) + + output = self.get_output(w.Outputs.annotated_data) + self.assertIsNotNone(output) + self.assertEqual(len(self.iris), len(output)) + self.assertTupleEqual(self.iris.X.shape, output.X.shape) + self.assertTupleEqual(self.iris.Y.shape, output.Y.shape) + self.assertEqual(2, output.metas.shape[1]) + + self.assertEqual("Cluster", str(output.domain.metas[0])) + self.assertEqual("DBSCAN Core", str(output.domain.metas[1])) From 48df8de3231f53e777a7db61e1923a3538fe96f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Primo=C5=BE=20Godec?= Date: Wed, 3 Jul 2019 15:21:37 +0200 Subject: [PATCH 2/6] DBSCAN: eps graph --- Orange/widgets/unsupervised/owdbscan.py | 140 +++++++++++++++--- .../unsupervised/tests/test_owdbscan.py | 106 ++++++++++++- 2 files changed, 222 insertions(+), 24 deletions(-) diff --git a/Orange/widgets/unsupervised/owdbscan.py b/Orange/widgets/unsupervised/owdbscan.py index 59b0a306d1f..f894f201925 100644 --- a/Orange/widgets/unsupervised/owdbscan.py +++ b/Orange/widgets/unsupervised/owdbscan.py @@ -1,19 +1,57 @@ import sys import numpy as np +import pyqtgraph as pg +from scipy import spatial from AnyQt.QtWidgets import QLayout, QApplication from AnyQt.QtCore import Qt +from AnyQt.QtGui import QColor +from Orange.preprocess import Normalize, Continuize from Orange.widgets import widget, gui +from Orange.widgets.gui import SliderGraph from Orange.widgets.settings import Setting from Orange.data import Table, Domain, DiscreteVariable from Orange.clustering import DBSCAN -from Orange import distance from Orange.widgets.utils.annotated_data import ANNOTATED_DATA_SIGNAL_NAME from Orange.widgets.utils.signals import Input, Output from Orange.widgets.widget import Msg +DEFAULT_CUTPOINT = 0.1 +PREPROCESSORS = [Continuize(), Normalize()] +EPS_BOTTOM_LIMIT = 0.01 + + +def get_kth_distances(data, metric, k=5): + """ + The function computes the epsilon parameter for DBSCAN through method + proposed in the paper. + Parameters + ---------- + coordinates : Orange.data.Table + Visualisation coordinates - embeddings + k : int + Number kth observed neighbour + skip : float + Percentage of skipped neighborus. + Returns + ------- + np.ndarray + Epsilon parameter for DBSCAN + """ + x = data.X + if x.shape[0] > 1000: # subsample + x = x[np.random.randint(x.shape[0], size=1000), :] + + dist = spatial.distance.squareform(spatial.distance.pdist(x, metric=metric)) + k = min(k+1, len(data) - 1) # k+1 since first one is item itself + kth_point = np.argpartition(dist, k, axis=1)[:, k] + kth_dist = np.sort(dist[np.arange(0, len(kth_point)), kth_point])[::-1] + + return kth_dist + + class OWDBSCAN(widget.OWWidget): name = "DBSCAN" description = "Density-based spatial clustering." @@ -32,40 +70,36 @@ class Error(widget.OWWidget.Error): METRICS = [ ("Euclidean", "euclidean"), - ("Manhattan", "manhattan"), - ("Cosine", distance.Cosine), - ("Jaccard", distance.Jaccard), - # ("Spearman", distance.SpearmanR), - # ("Spearman absolute", distance.SpearmanRAbsolute), - # ("Pearson", distance.PearsonR), - # ("Pearson absolute", distance.PearsonRAbsolute), + ("Manhattan", "cityblock"), + ("Cosine", "cosine") ] min_samples = Setting(5) eps = Setting(0.5) metric_idx = Setting(0) auto_commit = Setting(True) - - want_main_area = False + k_distances = None + cut_point = None def __init__(self): super().__init__() self.data = None + self.data_normalized = None self.db = None self.model = None box = gui.widgetBox(self.controlArea, "Parameters") gui.spin(box, self, "min_samples", 1, 100, 1, callback=self._invalidate, label="Core point neighbors") - gui.doubleSpin(box, self, "eps", 0.1, 10, 0.01, - callback=self._invalidate, + gui.doubleSpin(box, self, "eps", EPS_BOTTOM_LIMIT, 10, 0.01, + callback=self._eps_changed, label="Neighborhood distance") box = gui.widgetBox(self.controlArea, self.tr("Distance Metric")) gui.comboBox(box, self, "metric_idx", items=list(zip(*self.METRICS))[0], - callback=self._invalidate) + callback=self._metirc_changed) gui.auto_commit(self.controlArea, self, "auto_commit", "Apply", orientation=Qt.Horizontal) @@ -74,12 +108,22 @@ def __init__(self): self.controlArea.setMinimumWidth(self.controlArea.sizeHint().width()) self.layout().setSizeConstraint(QLayout.SetFixedSize) + self.plot = SliderGraph( + x_axis_label="Data items sorted by score", + y_axis_label="Distance to the k-th nearest neighbour", + background="w", callback=self._on_cut_changed + ) + + self.mainArea.layout().addWidget(self.plot) + def adjustSize(self): self.ensurePolished() self.resize(self.controlArea.sizeHint()) - def check_data_size(self): - if len(self.data) < 2: + def check_data_size(self, data): + if data is None: + return False + if len(data) < 2: self.Error.not_enough_instances() return False return True @@ -88,15 +132,41 @@ def commit(self): self.cluster() def cluster(self): - if not self.check_data_size(): + if not self.check_data_size(self.data): return self.model = DBSCAN( eps=self.eps, min_samples=self.min_samples, metric=self.METRICS[self.metric_idx][1] - ).get_model(self.data) + ).get_model(self.data_normalized) self.send_data() + def _compute_and_plot(self): + self._compute_kdistances() + self._compute_cutpoint() + self._plot_graph() + + def _plot_graph(self): + nonzero = np.sum(self.k_distances > EPS_BOTTOM_LIMIT) + self.plot.update(np.arange(len(self.k_distances)), + [self.k_distances], + colors=[QColor('red')], + cutpoint_x=self.cut_point, + selection_limit=(0, nonzero - 1)) + + def _compute_kdistances(self): + self.k_distances = get_kth_distances( + self.data_normalized, metric=self.METRICS[self.metric_idx][1]) + + def _compute_cutpoint(self): + self.cut_point = int(DEFAULT_CUTPOINT * len(self.k_distances)) + self.eps = self.k_distances[self.cut_point] + + if self.eps < EPS_BOTTOM_LIMIT: + self.eps = np.min( + self.k_distances[self.k_distances >= EPS_BOTTOM_LIMIT]) + self.cut_point = self._find_nearest_dist(self.eps) + def send_data(self): model = self.model @@ -129,17 +199,49 @@ def send_data(self): @Inputs.data def set_data(self, data): - self.data = data + self.Error.clear() + if not self.check_data_size(data): + data = None + self.data = self.data_normalized = data if self.data is None: self.Outputs.annotated_data.send(None) - self.Error.clear() + if self.data is None: return + + # preprocess data + for pp in PREPROCESSORS: + self.data_normalized = pp(self.data_normalized) + + self._compute_and_plot() self.unconditional_commit() def _invalidate(self): self.commit() + def _find_nearest_dist(self, value): + array = np.asarray(self.k_distances) + idx = (np.abs(array - value)).argmin() + return idx + + def _eps_changed(self): + # find the closest value to eps + self.cut_point = self._find_nearest_dist(self.eps) + self.plot.set_cutpoint(self.cut_point) + self._invalidate() + + def _metirc_changed(self): + if self.data is not None: + self._compute_and_plot() + self._invalidate() + + def _on_cut_changed(self, value): + # cut changed by means of a cut line over the scree plot. + self.cut_point = value + self.eps = self.k_distances[value] + + self.commit() + if __name__ == "__main__": a = QApplication(sys.argv) diff --git a/Orange/widgets/unsupervised/tests/test_owdbscan.py b/Orange/widgets/unsupervised/tests/test_owdbscan.py index c53fe2ef81d..ccb961f915b 100644 --- a/Orange/widgets/unsupervised/tests/test_owdbscan.py +++ b/Orange/widgets/unsupervised/tests/test_owdbscan.py @@ -1,13 +1,14 @@ import numpy as np -from scipy.sparse import csr_matrix +from scipy.sparse import csr_matrix, csc_matrix from Orange.data import Table +from Orange.distance import Euclidean from Orange.widgets.tests.base import WidgetTest from Orange.widgets.tests.utils import simulate -from Orange.widgets.unsupervised.owdbscan import OWDBSCAN +from Orange.widgets.unsupervised.owdbscan import OWDBSCAN, get_kth_distances -class TestOWCSVFileImport(WidgetTest): +class TestOWDBSCAN(WidgetTest): def setUp(self): self.widget = self.create_widget(OWDBSCAN) self.iris = Table("iris") @@ -47,7 +48,6 @@ def test_bad_input(self): def test_data_none(self): w = self.widget - self.send_signal(w.Inputs.data, self.iris[:5]) self.send_signal(w.Inputs.data, None) output = self.get_output(w.Outputs.annotated_data) @@ -69,6 +69,13 @@ def test_change_eps(self): self.assertGreater(np.nansum(output2.metas[:, 0]), np.nansum(output1.metas[:, 0])) + # try when no data + self.send_signal(w.Inputs.data, None) + self.widget.controls.eps.valueChanged.emit(0.5) + output = self.get_output(w.Outputs.annotated_data) + self.assertIsNone(output) + + def test_change_min_samples(self): w = self.widget @@ -85,6 +92,12 @@ def test_change_min_samples(self): self.assertGreater(np.nansum(output2.metas[:, 0]), np.nansum(output1.metas[:, 0])) + # try when no data + self.send_signal(w.Inputs.data, None) + self.widget.controls.min_samples.valueChanged.emit(3) + output = self.get_output(w.Outputs.annotated_data) + self.assertIsNone(output) + def test_change_metric_idx(self): w = self.widget @@ -101,7 +114,12 @@ def test_change_metric_idx(self): self.assertGreater(np.nansum(output1.metas[:, 0]), np.nansum(output2.metas[:, 0])) - def test_sparse_data(self): + # try when no data + self.send_signal(w.Inputs.data, None) + cbox = self.widget.controls.metric_idx + simulate.combobox_activate_index(cbox, 0) # Euclidean + + def test_sparse_csr_data(self): self.iris.X = csr_matrix(self.iris.X) w = self.widget @@ -117,3 +135,81 @@ def test_sparse_data(self): self.assertEqual("Cluster", str(output.domain.metas[0])) self.assertEqual("DBSCAN Core", str(output.domain.metas[1])) + + def test_sparse_csc_data(self): + self.iris.X = csc_matrix(self.iris.X) + + w = self.widget + + self.send_signal(w.Inputs.data, self.iris) + + output = self.get_output(w.Outputs.annotated_data) + self.assertIsNotNone(output) + self.assertEqual(len(self.iris), len(output)) + self.assertTupleEqual(self.iris.X.shape, output.X.shape) + self.assertTupleEqual(self.iris.Y.shape, output.Y.shape) + self.assertEqual(2, output.metas.shape[1]) + + self.assertEqual("Cluster", str(output.domain.metas[0])) + self.assertEqual("DBSCAN Core", str(output.domain.metas[1])) + + def test_get_kth_distances(self): + dists = get_kth_distances(self.iris, "euclidean", k=5) + self.assertEqual(len(self.iris), len(dists)) + # dists must be sorted + np.testing.assert_array_equal(dists, np.sort(dists)[::-1]) + + # test with different distance - e.g. Orange distance + dists = get_kth_distances(self.iris, Euclidean, k=5) + self.assertEqual(len(self.iris), len(dists)) + # dists must be sorted + np.testing.assert_array_equal(dists, np.sort(dists)[::-1]) + + def test_metric_changed(self): + w = self.widget + + self.send_signal(w.Inputs.data, self.iris) + cbox = w.controls.metric_idx + simulate.combobox_activate_index(cbox, 2) + + output = self.get_output(w.Outputs.annotated_data) + self.assertIsNotNone(output) + self.assertEqual(len(self.iris), len(output)) + self.assertTupleEqual(self.iris.X.shape, output.X.shape) + self.assertTupleEqual(self.iris.Y.shape, output.Y.shape) + + def test_large_data(self): + """ + When data has less than 1000 instances they are subsampled in k-values + computation. + """ + w = self.widget + + data = Table(self.iris.domain, + np.repeat(self.iris.X, 10, axis=0), + np.repeat(self.iris.Y, 10, axis=0)) + + self.send_signal(w.Inputs.data, data) + output = self.get_output(w.Outputs.annotated_data) + + self.assertEqual(len(data), len(output)) + self.assertTupleEqual(data.X.shape, output.X.shape) + self.assertTupleEqual(data.Y.shape, output.Y.shape) + self.assertEqual(2, output.metas.shape[1]) + + def test_titanic(self): + """ + Titanic is a data-set with many 0 in k-nearest neighbours and thus some + manipulation is required to set cut-point. + This test checks whether widget works on those type of data. + """ + w = self.widget + data = Table("titanic") + self.send_signal(w.Inputs.data, data) + + def test_missing_data(self): + w = self.widget + self.iris[1:5, 1] = np.nan + self.send_signal(w.Inputs.data, self.iris) + output = self.get_output(w.Outputs.annotated_data) + self.assertTupleEqual((150, 1), output[:, "Cluster"].metas.shape) From 23e4c98ec347c51783107dd3bb673e9b9dcd3195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Primo=C5=BE=20Godec?= Date: Thu, 4 Jul 2019 15:17:28 +0200 Subject: [PATCH 3/6] Graph that allow selecting the cut-point moved to utils --- Orange/widgets/unsupervised/owdbscan.py | 98 ++++++----- Orange/widgets/utils/slidergraph.py | 210 ++++++++++++++++++++++++ 2 files changed, 264 insertions(+), 44 deletions(-) create mode 100644 Orange/widgets/utils/slidergraph.py diff --git a/Orange/widgets/unsupervised/owdbscan.py b/Orange/widgets/unsupervised/owdbscan.py index f894f201925..392dae4493e 100644 --- a/Orange/widgets/unsupervised/owdbscan.py +++ b/Orange/widgets/unsupervised/owdbscan.py @@ -1,15 +1,15 @@ import sys import numpy as np -import pyqtgraph as pg from scipy import spatial -from AnyQt.QtWidgets import QLayout, QApplication +from AnyQt.QtWidgets import QApplication from AnyQt.QtCore import Qt from AnyQt.QtGui import QColor +from sklearn.metrics import pairwise_distances -from Orange.preprocess import Normalize, Continuize +from Orange.preprocess import Normalize, Continuize, SklImpute from Orange.widgets import widget, gui -from Orange.widgets.gui import SliderGraph +from Orange.widgets.utils.slidergraph import SliderGraph from Orange.widgets.settings import Setting from Orange.data import Table, Domain, DiscreteVariable from Orange.clustering import DBSCAN @@ -18,8 +18,8 @@ from Orange.widgets.widget import Msg -DEFAULT_CUTPOINT = 0.1 -PREPROCESSORS = [Continuize(), Normalize()] +DEFAULT_CUT_POINT = 0.1 +PREPROCESSORS = [Continuize(), Normalize(), SklImpute()] EPS_BOTTOM_LIMIT = 0.01 @@ -29,12 +29,13 @@ def get_kth_distances(data, metric, k=5): proposed in the paper. Parameters ---------- - coordinates : Orange.data.Table + data : Orange.data.Table Visualisation coordinates - embeddings + metric : callable or str + The metric to compute the distance. k : int Number kth observed neighbour - skip : float - Percentage of skipped neighborus. + Returns ------- np.ndarray @@ -44,7 +45,7 @@ def get_kth_distances(data, metric, k=5): if x.shape[0] > 1000: # subsample x = x[np.random.randint(x.shape[0], size=1000), :] - dist = spatial.distance.squareform(spatial.distance.pdist(x, metric=metric)) + dist = pairwise_distances(x, metric=metric) k = min(k+1, len(data) - 1) # k+1 since first one is item itself kth_point = np.argpartition(dist, k, axis=1)[:, k] kth_dist = np.sort(dist[np.arange(0, len(kth_point)), kth_point])[::-1] @@ -74,7 +75,7 @@ class Error(widget.OWWidget.Error): ("Cosine", "cosine") ] - min_samples = Setting(5) + min_samples = Setting(4) eps = Setting(0.5) metric_idx = Setting(0) auto_commit = Setting(True) @@ -90,9 +91,10 @@ def __init__(self): self.model = None box = gui.widgetBox(self.controlArea, "Parameters") - gui.spin(box, self, "min_samples", 1, 100, 1, callback=self._invalidate, + gui.spin(box, self, "min_samples", 1, 100, 1, + callback=self._min_samples_changed, label="Core point neighbors") - gui.doubleSpin(box, self, "eps", EPS_BOTTOM_LIMIT, 10, 0.01, + gui.doubleSpin(box, self, "eps", EPS_BOTTOM_LIMIT, 1000, 0.01, callback=self._eps_changed, label="Neighborhood distance") @@ -105,8 +107,7 @@ def __init__(self): orientation=Qt.Horizontal) gui.rubber(self.controlArea) - self.controlArea.setMinimumWidth(self.controlArea.sizeHint().width()) - self.layout().setSizeConstraint(QLayout.SetFixedSize) + self.controlArea.layout().addStretch() self.plot = SliderGraph( x_axis_label="Data items sorted by score", @@ -116,10 +117,6 @@ def __init__(self): self.mainArea.layout().addWidget(self.plot) - def adjustSize(self): - self.ensurePolished() - self.resize(self.controlArea.sizeHint()) - def check_data_size(self, data): if data is None: return False @@ -141,9 +138,10 @@ def cluster(self): ).get_model(self.data_normalized) self.send_data() - def _compute_and_plot(self): + def _compute_and_plot(self, cut_point=None): self._compute_kdistances() - self._compute_cutpoint() + if cut_point is None: + self._compute_cut_point() self._plot_graph() def _plot_graph(self): @@ -156,10 +154,12 @@ def _plot_graph(self): def _compute_kdistances(self): self.k_distances = get_kth_distances( - self.data_normalized, metric=self.METRICS[self.metric_idx][1]) + self.data_normalized, metric=self.METRICS[self.metric_idx][1], + k=self.min_samples + ) - def _compute_cutpoint(self): - self.cut_point = int(DEFAULT_CUTPOINT * len(self.k_distances)) + def _compute_cut_point(self): + self.cut_point = int(DEFAULT_CUT_POINT * len(self.k_distances)) self.eps = self.k_distances[self.cut_point] if self.eps < EPS_BOTTOM_LIMIT: @@ -167,6 +167,27 @@ def _compute_cutpoint(self): self.k_distances[self.k_distances >= EPS_BOTTOM_LIMIT]) self.cut_point = self._find_nearest_dist(self.eps) + @Inputs.data + def set_data(self, data): + self.Error.clear() + if not self.check_data_size(data): + data = None + self.data = self.data_normalized = data + if self.data is None: + self.Outputs.annotated_data.send(None) + self.plot.clear_plot() + return + + if self.data is None: + return + + # preprocess data + for pp in PREPROCESSORS: + self.data_normalized = pp(self.data_normalized) + + self._compute_and_plot() + self.unconditional_commit() + def send_data(self): model = self.model @@ -197,25 +218,6 @@ def send_data(self): self.Outputs.annotated_data.send(new_table) - @Inputs.data - def set_data(self, data): - self.Error.clear() - if not self.check_data_size(data): - data = None - self.data = self.data_normalized = data - if self.data is None: - self.Outputs.annotated_data.send(None) - - if self.data is None: - return - - # preprocess data - for pp in PREPROCESSORS: - self.data_normalized = pp(self.data_normalized) - - self._compute_and_plot() - self.unconditional_commit() - def _invalidate(self): self.commit() @@ -226,8 +228,10 @@ def _find_nearest_dist(self, value): def _eps_changed(self): # find the closest value to eps + if self.data is None: + return self.cut_point = self._find_nearest_dist(self.eps) - self.plot.set_cutpoint(self.cut_point) + self.plot.set_cut_point(self.cut_point) self._invalidate() def _metirc_changed(self): @@ -242,6 +246,12 @@ def _on_cut_changed(self, value): self.commit() + def _min_samples_changed(self): + if self.data is None: + return + self._compute_and_plot(cut_point=self.cut_point) + self._invalidate() + if __name__ == "__main__": a = QApplication(sys.argv) diff --git a/Orange/widgets/utils/slidergraph.py b/Orange/widgets/utils/slidergraph.py new file mode 100644 index 00000000000..59a4bed1b6b --- /dev/null +++ b/Orange/widgets/utils/slidergraph.py @@ -0,0 +1,210 @@ +import numpy as np +from pyqtgraph import PlotWidget, mkPen, InfiniteLine, PlotCurveItem, \ + TextItem, Point +from AnyQt.QtGui import QColor +from AnyQt.QtCore import Qt + + +class SliderGraph(PlotWidget): + """ + An widget graph element that shows a line plot with more sequences. It + also plot a vertical line that can be moved left and right by a user. When + the line is moved a callback function is called with selected value (on + x axis). + + Attributes + ---------- + x_axis_label : str + A text label for x axis + y_axis_label : str + A text label for y axis + callback : callable + A function which is called when selection is changed. + background : str, optional (default: "w") + Plot background color + """ + + def __init__(self, x_axis_label, y_axis_label, callback, background="w"): + super().__init__(background=background) + + axis = self.getAxis("bottom") + axis.setLabel(x_axis_label) + axis = self.getAxis("left") + axis.setLabel(y_axis_label) + + self.getViewBox().setMenuEnabled(False) + self.getViewBox().setMouseEnabled(False, False) + self.showGrid(True, True, alpha=0.5) + self.setRange(xRange=(0.0, 1.0), yRange=(0.0, 1.0)) + + # tuples to store horisontal lines and labels + self.plot_horlabel = [] + self.plot_horline = [] + self._line = None + self.callback = callback + + # variables to store sequences + self.sequences = None + self.x = None + self.selection_limit = None + self.data_increasing = None # true if data mainly increasing + + def update(self, x, y, colors, cutpoint_x=None, selection_limit=None): + """ + Function replots a graph. + + Parameters + ---------- + x : np.ndarray + One-dimensional array with X coordinates of the points + y : list + List of np.ndarrays that contains an array of Y values for each + sequence. + colors : list + List of Qt colors (eg. Qt.red) for each sequence. + cutpoint_x : int, optional + A starting cutpoint - the location of the vertical line. + selection_limit : tuple + The tuple of two values that limit the range for selection. + """ + self.clear_plot() + self.sequences = y + self.x = x + self.selection_limit = selection_limit + + self.data_increasing = [np.sum(d[1:] - d[:-1]) > 0 for d in y] + + # plot sequence + for s, c in zip(y, colors): + self.plot(x, s, pen=mkPen(QColor(c), width=2), antialias=True) + + self._plot_cutpoint(cutpoint_x) + self.setRange(xRange=(x.min(), x.max()), + yRange=(0, max(yi.max() for yi in y))) + + def clear_plot(self): + """ + This function clears the plot and removes data. + """ + self.clear() + self.plot_horlabel = [] + self.plot_horline = [] + self._line = None + self.sequences = None + + def set_cut_point(self, x): + """ + This function sets the cutpoint (selection line) at the specific + location. + + Parameters + ---------- + x : int + Cutpoint location at the x axis. + """ + self._plot_cutpoint(x) + + def _plot_cutpoint(self, x): + """ + Function plots the cutpoint. + + Parameters + ---------- + x : int + Cutpoint location. + """ + if x is None: + self._line = None + return + if self._line is None: + # plot interactive vertical line + self._line = InfiniteLine( + angle=90, pos=x, movable=True, + bounds=self.selection_limit if self.selection_limit is not None + else (self.x.min(), self.x.max()) + ) + self._line.setCursor(Qt.SizeHorCursor) + self._line.setPen(mkPen(QColor(Qt.black), width=2)) + self._line.sigPositionChanged.connect(self._on_cut_changed) + self.addItem(self._line) + else: + self._line.setValue(x) + + self._update_horizontal_lines() + + def _plot_horizontal_lines(self): + """ + Function plots the vertical dashed lines that points to the selected + sequence values at the y axis. + """ + for _ in range(len(self.sequences)): + self.plot_horline.append(PlotCurveItem( + pen=mkPen(QColor(Qt.blue), style=Qt.DashLine))) + self.plot_horlabel.append(TextItem( + color=QColor(Qt.black), anchor=(0, 1))) + for item in self.plot_horlabel + self.plot_horline: + self.addItem(item) + + def _set_anchor(self, label, cutidx, inc): + """ + This function set the location of the text label around the selected + point at the curve. It place the text such that it is not plotted + at the line. + + Parameters + ---------- + label : TextItem + Text item that needs to have location set. + cutidx : int + The index of the selected element in the list. If index in first + part of the list we put label on the right side else on the left, + such that it does not disappear at the graph edge. + inc : bool + This parameter tels whether the curve value is increasing or + decreasing. + """ + if inc: + label.anchor = Point(0, 0) if cutidx < len(self.x) / 2 \ + else Point(1, 1) + else: + label.anchor = Point(0, 1) if cutidx < len(self.x) / 2 \ + else Point(1, 0) + + def _update_horizontal_lines(self): + """ + This function update the horisontal lines when selection changes. + If lines are present jet it calls the function to init them. + """ + if not self.plot_horline: # init horizontal lines + self._plot_horizontal_lines() + + # in every case set their position + location = int(round(self._line.value())) + cutidx = np.searchsorted(self.x, location) + for s, curve, label, inc in zip( + self.sequences, self.plot_horline, self.plot_horlabel, + self.data_increasing): + y = s[cutidx] + curve.setData([-100, location], 2 * [y]) + self._set_anchor(label, cutidx, inc) + label.setPos(location, y) + label.setPlainText("{:.3f}".format(y)) + + def _on_cut_changed(self, line): + """ + This function is called when selection changes. It extract the selected + value and calls the callback function. + + Parameters + ---------- + line : InfiniteLine + The cutpoint - selection line. + """ + # cut changed by means of a cut line over the scree plot. + value = int(round(line.value())) + + # vertical line can take only int positions + self._line.setValue(value) + + self._update_horizontal_lines() + self.callback(value) From 130f893e6eda9e8c6a42cc1f7965151841df1caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Primo=C5=BE=20Godec?= Date: Wed, 10 Jul 2019 15:01:32 +0200 Subject: [PATCH 4/6] OwPCA: Use a SlierGraph from gui --- Orange/widgets/unsupervised/owpca.py | 105 ++++++--------------------- 1 file changed, 24 insertions(+), 81 deletions(-) diff --git a/Orange/widgets/unsupervised/owpca.py b/Orange/widgets/unsupervised/owpca.py index 413ac2bb561..17ff7508601 100644 --- a/Orange/widgets/unsupervised/owpca.py +++ b/Orange/widgets/unsupervised/owpca.py @@ -1,17 +1,15 @@ import numbers +import numpy from AnyQt.QtWidgets import QFormLayout -from AnyQt.QtGui import QColor from AnyQt.QtCore import Qt -import numpy -import pyqtgraph as pg - from Orange.data import Table, Domain, StringVariable, ContinuousVariable from Orange.data.sql.table import SqlTable, AUTO_DL_LIMIT from Orange.preprocess import preprocess from Orange.projection import PCA from Orange.widgets import widget, gui, settings +from Orange.widgets.utils.slidergraph import SliderGraph from Orange.widgets.utils.widgetpreview import WidgetPreview from Orange.widgets.widget import Input, Output @@ -63,7 +61,6 @@ def __init__(self): self._transformed = None self._variance_ratio = None self._cumulative = None - self._line = False self._init_projector() # Components Selection @@ -106,19 +103,9 @@ def __init__(self): gui.auto_commit(self.controlArea, self, "auto_commit", "Apply", checkbox_label="Apply automatically") - self.plot = pg.PlotWidget(background="w") - - axis = self.plot.getAxis("bottom") - axis.setLabel("Principal Components") - axis = self.plot.getAxis("left") - axis.setLabel("Proportion of variance") - self.plot_horlabels = [] - self.plot_horlines = [] - - self.plot.getViewBox().setMenuEnabled(False) - self.plot.getViewBox().setMouseEnabled(False, False) - self.plot.showGrid(True, True, alpha=0.5) - self.plot.setRange(xRange=(0.0, 1.0), yRange=(0.0, 1.0)) + self.plot = SliderGraph( + "Principal Components", "Proportion of variance", + self._on_cut_changed) self.mainArea.layout().addWidget(self.plot) self._update_normalize() @@ -139,11 +126,11 @@ def set_data(self, data): data_sample.download_data(2000, partial=True) data = Table(data_sample) if isinstance(data, Table): - if len(data.domain.attributes) == 0: + if not data.domain.attributes: self.Error.no_features() self.clear_outputs() return - if len(data) == 0: + if not data: self.Error.no_instances() self.clear_outputs() return @@ -189,10 +176,7 @@ def clear(self): self._transformed = None self._variance_ratio = None self._cumulative = None - self._line = None - self.plot_horlabels = [] - self.plot_horlines = [] - self.plot.clear() + self.plot.clear_plot() def clear_outputs(self): self.Outputs.transformed_data.send(None) @@ -200,72 +184,33 @@ def clear_outputs(self): self.Outputs.pca.send(self._pca_projector) def _setup_plot(self): - self.plot.clear() if self._pca is None: + self.plot.clear_plot() return explained_ratio = self._variance_ratio explained = self._cumulative + cutpos = self._nselected_components() p = min(len(self._variance_ratio), self.maxp) - self.plot.plot(numpy.arange(p), explained_ratio[:p], - pen=pg.mkPen(QColor(Qt.red), width=2), - antialias=True, - name="Variance") - self.plot.plot(numpy.arange(p), explained[:p], - pen=pg.mkPen(QColor(Qt.darkYellow), width=2), - antialias=True, - name="Cumulative Variance") - - cutpos = self._nselected_components() - 1 - self._line = pg.InfiniteLine( - angle=90, pos=cutpos, movable=True, bounds=(0, p - 1)) - self._line.setCursor(Qt.SizeHorCursor) - self._line.setPen(pg.mkPen(QColor(Qt.black), width=2)) - self._line.sigPositionChanged.connect(self._on_cut_changed) - self.plot.addItem(self._line) - - self.plot_horlines = ( - pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine)), - pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine))) - self.plot_horlabels = ( - pg.TextItem(color=QColor(Qt.black), anchor=(1, 0)), - pg.TextItem(color=QColor(Qt.black), anchor=(1, 1))) - for item in self.plot_horlabels + self.plot_horlines: - self.plot.addItem(item) - self._set_horline_pos() - - self.plot.setRange(xRange=(0.0, p - 1), yRange=(0.0, 1.0)) + self.plot.update( + numpy.arange(1, p+1), [explained_ratio[:p], explained[:p]], + [Qt.red, Qt.darkYellow], cutpoint_x=cutpos) + self._update_axis() - def _set_horline_pos(self): - cutidx = self.ncomponents - 1 - for line, label, curve in zip(self.plot_horlines, self.plot_horlabels, - (self._variance_ratio, self._cumulative)): - y = curve[cutidx] - line.setData([-1, cutidx], 2 * [y]) - label.setPos(cutidx, y) - label.setPlainText("{:.3f}".format(y)) - - def _on_cut_changed(self, line): - # cut changed by means of a cut line over the scree plot. - value = int(round(line.value())) - self._line.setValue(value) - current = self._nselected_components() - components = value + 1 + def _on_cut_changed(self, components): if not (self.ncomponents == 0 and components == len(self._variance_ratio)): self.ncomponents = components - self._set_horline_pos() - if self._pca is not None: var = self._cumulative[components - 1] if numpy.isfinite(var): self.variance_covered = int(var * 100) - if current != self._nselected_components(): + if components != self._nselected_components(): self._invalidate_selection() def _update_selection_component_spin(self): @@ -284,9 +229,7 @@ def _update_selection_component_spin(self): if numpy.isfinite(var): self.variance_covered = int(var * 100) - if numpy.floor(self._line.value()) + 1 != cut: - self._line.setValue(cut - 1) - + self.plot.set_cut_point(cut) self._invalidate_selection() def _update_selection_variance_spin(self): @@ -298,8 +241,7 @@ def _update_selection_variance_spin(self): self.variance_covered / 100.0) + 1 cut = min(cut, len(self._cumulative)) self.ncomponents = cut - if numpy.floor(self._line.value()) + 1 != cut: - self._line.setValue(cut - 1) + self.plot.set_cut_point(cut) self._invalidate_selection() def _update_normalize(self): @@ -340,13 +282,13 @@ def _update_axis(self): p = min(len(self._variance_ratio), self.maxp) axis = self.plot.getAxis("bottom") d = max((p-1)//(self.axis_labels-1), 1) - axis.setTicks([[(i, str(i+1)) for i in range(0, p, d)]]) + axis.setTicks([[(i, str(i)) for i in range(1, p + 1, d)]]) def commit(self): transformed = components = None if self._pca is not None: if self._transformed is None: - # Compute the full transform (MAX_COMPONENTS components) only once. + # Compute the full transform (MAX_COMPONENTS components) once. self._transformed = self._pca(self.data) transformed = self._transformed @@ -357,9 +299,10 @@ def commit(self): ) transformed = transformed.from_table(domain, transformed) # prevent caching new features by defining compute_value - dom = Domain([ContinuousVariable(a.name, compute_value=lambda _: None) - for a in self._pca.orig_domain.attributes], - metas=[StringVariable(name='component')]) + dom = Domain( + [ContinuousVariable(a.name, compute_value=lambda _: None) + for a in self._pca.orig_domain.attributes], + metas=[StringVariable(name='component')]) metas = numpy.array([['PC{}'.format(i + 1) for i in range(self.ncomponents)]], dtype=object).T From fc6b4927b61a63cb0d1a13440613470368cb1dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Primo=C5=BE=20Godec?= Date: Thu, 11 Jul 2019 14:34:15 +0200 Subject: [PATCH 5/6] Tests for SliderGraph --- .../widgets/utils/tests/test_slidergraph.py | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 Orange/widgets/utils/tests/test_slidergraph.py diff --git a/Orange/widgets/utils/tests/test_slidergraph.py b/Orange/widgets/utils/tests/test_slidergraph.py new file mode 100644 index 00000000000..7eeafb2c982 --- /dev/null +++ b/Orange/widgets/utils/tests/test_slidergraph.py @@ -0,0 +1,107 @@ +import numpy as np +from AnyQt.QtCore import Qt + +from Orange.widgets import widget +from Orange.widgets.tests.base import WidgetTest +from Orange.widgets.utils.slidergraph import SliderGraph + + +class SimpleWidget(widget.OWWidget): + name = "Simple widget" + + def __init__(self): + super().__init__() + self.plot = SliderGraph(x_axis_label="label1", + y_axis_label="label2", + callback=lambda x: x) + + self.mainArea.layout().addWidget(self.plot) + + +class TestSliderGraph(WidgetTest): + def setUp(self): + self.data = [np.array([1, 2, 3, 4, 5, 6, 7])] + self.widget = self.create_widget(SimpleWidget) + + def test_init(self): + p = self.widget.plot + + # labels set correctly? + self.assertEqual("label1", p.getAxis("bottom").labelText) + self.assertEqual("label2", p.getAxis("left").labelText) + + # pylint: disable=protected-access + self.assertIsNone(p._line) + self.assertIsNone(p.sequences) + self.assertIsNone(p.x) + self.assertIsNone(p.selection_limit) + self.assertIsNone(p.data_increasing) + + self.assertListEqual([], p.plot_horlabel) + self.assertListEqual([], p.plot_horline) + + def test_plot(self): + p = self.widget.plot + x = np.arange(len(self.data[0])) + p.update(x, self.data, [Qt.red], + cutpoint_x=1) + + # labels set correctly? + self.assertEqual("label1", p.getAxis("bottom").labelText) + self.assertEqual("label2", p.getAxis("left").labelText) + + # pylint: disable=protected-access + self.assertIsNotNone(p._line) + np.testing.assert_array_equal(self.data, p.sequences) + np.testing.assert_array_equal(x, p.x) + self.assertTrue(p.data_increasing) + + self.assertEqual(len(p.plot_horlabel), 1) + self.assertEqual(len(p.plot_horline), 1) + # pylint: disable=protected-access + self.assertIsNotNone(p._line) + + def test_plot_selection_limit(self): + p = self.widget.plot + x = np.arange(len(self.data[0])) + p.update(x, self.data, [Qt.red], + cutpoint_x=1, selection_limit=(0, 2)) + + # labels set correctly? + self.assertEqual("label1", p.getAxis("bottom").labelText) + self.assertEqual("label2", p.getAxis("left").labelText) + + # pylint: disable=protected-access + self.assertIsNotNone(p._line) + np.testing.assert_array_equal(self.data, p.sequences) + np.testing.assert_array_equal(x, p.x) + self.assertTrue(p.data_increasing) + self.assertTupleEqual((0, 2), p.selection_limit) + # pylint: disable=protected-access + self.assertEqual((0, 2), p._line.maxRange) + + self.assertEqual(len(p.plot_horlabel), 1) + self.assertEqual(len(p.plot_horline), 1) + # pylint: disable=protected-access + self.assertIsNotNone(p._line) + + def test_plot_no_cutpoint(self): + """ + When no cutpoint provided there must be no cutpoint plotted. + """ + p = self.widget.plot + x = np.arange(len(self.data[0])) + p.update(x, self.data, [Qt.red]) + + # pylint: disable=protected-access + self.assertIsNone(p._line) + + # then it is set + p.update(x, self.data, [Qt.red], cutpoint_x=1) + # pylint: disable=protected-access + self.assertIsNotNone(p._line) + + # and re-ploted without cutpoint again + p.update(x, self.data, [Qt.red]) + # pylint: disable=protected-access + self.assertIsNone(p._line) From 8d1e4c304aba09faedfafe39d8d116bcdb0f8050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Primo=C5=BE=20Godec?= Date: Fri, 12 Jul 2019 12:20:54 +0200 Subject: [PATCH 6/6] OWDBSCAN: Documentation --- doc/visual-programming/source/index.rst | 3 +- .../source/widgets/unsupervised/DBSCAN.md | 46 ++++++++++++++++++ .../unsupervised/images/dbscan-example.png | Bin 0 -> 76061 bytes .../unsupervised/images/dbscan-stamped.png | Bin 0 -> 34553 bytes 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 doc/visual-programming/source/widgets/unsupervised/DBSCAN.md create mode 100644 doc/visual-programming/source/widgets/unsupervised/images/dbscan-example.png create mode 100644 doc/visual-programming/source/widgets/unsupervised/images/dbscan-stamped.png diff --git a/doc/visual-programming/source/index.rst b/doc/visual-programming/source/index.rst index ee0c3080e1b..b63dc8c609f 100644 --- a/doc/visual-programming/source/index.rst +++ b/doc/visual-programming/source/index.rst @@ -122,10 +122,11 @@ Unsupervised widgets/unsupervised/hierarchicalclustering widgets/unsupervised/kmeans widgets/unsupervised/louvainclustering + widgets/unsupervised/DBSCAN widgets/unsupervised/mds widgets/unsupervised/tsne widgets/unsupervised/manifoldlearning - + Evaluation ---------- diff --git a/doc/visual-programming/source/widgets/unsupervised/DBSCAN.md b/doc/visual-programming/source/widgets/unsupervised/DBSCAN.md new file mode 100644 index 00000000000..bb3ed06d287 --- /dev/null +++ b/doc/visual-programming/source/widgets/unsupervised/DBSCAN.md @@ -0,0 +1,46 @@ +DBSCAN +====== + +Groups items using the DBSCAN clustering algorithm. + +**Inputs** + +- Data: input dataset + +**Outputs** + +- Data: dataset with cluster index as a class attribute + +The widget applies the +[DBSCAN clustering](https://en.wikipedia.org/wiki/DBSCAN) algorithm to +the data and outputs a new dataset with cluster indices as a meta +attribute. The widget also shows the sorted graph with distances to +k-th nearest neighbors. With k values set to **Core point neighbors** +as suggested in the +[methods article](https://www.aaai.org/Papers/KDD/1996/KDD96-037.pdf). +This gives the user the idea of an +ideal selection for **Neighborhood distance** setting. As suggested by +authors this parameter should be set to the first value in the first +"valley" in the graph. + +![](images/dbscan-stamped.png) + +1. Set *minimal number of core neighbors* for a cluster and *maximal +neighborhood distance. +2. Set the distance metric that is used in grouping the items. +3. If *Apply Automatically* is ticked, the widget will commit changes +automatically. Alternatively, click *Apply*. +4. The graph shows the distance to the k-th nearest neighbor. *k* is +set by the **Core point neighbor** option. With moving the black slider +left and right you can select the right **Neighbourhood distance**. + +Example +------- + +In the following example, we connected the File widget with selected +Iris dataset to the DBSCAN widget. In the DBSCAN widget, we set +**Core points neighbors** parameter to 5. And select the +**Neighbourhood distance** to the value in the first "valley" in the +graph. We show clusters in the Scatter Plot widget. + +![](images/dbscan-example.png) diff --git a/doc/visual-programming/source/widgets/unsupervised/images/dbscan-example.png b/doc/visual-programming/source/widgets/unsupervised/images/dbscan-example.png new file mode 100644 index 0000000000000000000000000000000000000000..90cfe393e5d8a468fbf1c15d3ebcf952afb32994 GIT binary patch literal 76061 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 literal 0 HcmV?d00001 diff --git a/doc/visual-programming/source/widgets/unsupervised/images/dbscan-stamped.png b/doc/visual-programming/source/widgets/unsupervised/images/dbscan-stamped.png new file mode 100644 index 0000000000000000000000000000000000000000..d6af3d36b15da174eabc0ccba75f79abde9a600e GIT binary patch literal 34553 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 literal 0 HcmV?d00001