Repository: biolab/orange3

Issue:
DBSCAN changes instance ids
The output of DBSCAN looses traceability of data instances. The following workflow should explain the problem:

![image](https://user-images.githubusercontent.com/726604/117261710-c1626580-ae50-11eb-89b7-ff92dd6576a2.png)

t-SNE should mark all data instances, as the data subset is the same as the data, but instead reports that "No subset data instances appear in input data". 

![image](https://user-images.githubusercontent.com/726604/117261871-eeaf1380-ae50-11eb-8cba-267dfede1551.png)

Similar is of course true with any selection of the data that comes from DBSCAN. My original intention was to send the DBSCAN data to the boxplot and then show the selection from the box plot, sat, the clusters with interesting class-distribution, as a data subset to the t-SNE. But then I get, of course, the same warning in t-SNE, reporting the mismatch of the data subset and data provided to this widget.


Discussion:

Resolving patch:
diff --git a/Orange/widgets/unsupervised/owdbscan.py b/Orange/widgets/unsupervised/owdbscan.py
--- a/Orange/widgets/unsupervised/owdbscan.py
+++ b/Orange/widgets/unsupervised/owdbscan.py
@@ -10,7 +10,7 @@
 from Orange.widgets import widget, gui
 from Orange.widgets.utils.slidergraph import SliderGraph
 from Orange.widgets.settings import Setting
-from Orange.data import Table, Domain, DiscreteVariable
+from Orange.data import Table, DiscreteVariable
 from Orange.data.util import get_unique_names
 from Orange.clustering import DBSCAN
 from Orange.widgets.utils.annotated_data import ANNOTATED_DATA_SIGNAL_NAME
@@ -197,11 +197,10 @@ def send_data(self):
 
         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)
+        clusters = np.array(clusters)
         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)
 
         domain = self.data.domain
         attributes, classes = domain.attributes, domain.class_vars
@@ -215,15 +214,8 @@ def send_data(self):
         u_in_core = get_unique_names(names + [u_clust_var], "DBSCAN Core")
         in_core_var = DiscreteVariable(u_in_core, values=("0", "1"))
 
-        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)
+        new_table = self.data.add_column(clust_var, clusters, to_metas=True)
+        new_table = new_table.add_column(in_core_var, in_core, to_metas=True)
 
         self._set_output_summary(new_table)
         self.Outputs.annotated_data.send(new_table)


Test patch:
diff --git a/Orange/widgets/unsupervised/tests/test_owdbscan.py b/Orange/widgets/unsupervised/tests/test_owdbscan.py
--- a/Orange/widgets/unsupervised/tests/test_owdbscan.py
+++ b/Orange/widgets/unsupervised/tests/test_owdbscan.py
@@ -218,6 +218,11 @@ def test_titanic(self):
         data = Table("titanic")
         self.send_signal(w.Inputs.data, data)
 
+    def test_data_retain_ids(self):
+        self.send_signal(self.widget.Inputs.data, self.iris)
+        output = self.get_output(self.widget.Outputs.annotated_data)
+        np.testing.assert_array_equal(self.iris.ids, output.ids)
+
     def test_missing_data(self):
         w = self.widget
         self.iris[1:5, 1] = np.nan