Repository: biolab/orange3

Issue:
Hierarchical Clustering by Column Does not Output Cluster Numbers
<!-- 
Thanks for taking the time to report a bug!
If you're raising an issue about an add-on (i.e., installed via Options > Add-ons), raise an issue in the relevant add-on's issue tracker instead. See: https://github.com/biolab?q=orange3
To fix the bug, we need to be able to reproduce it. Please answer the following questions to the best of your ability.
-->


- [ ] **What's wrong?**
<!-- Be specific, clear, and concise. Include screenshots if relevant. -->
<!-- If you're getting an error message, copy it, and enclose it with three backticks (```). -->
Performing a hierarchical clustering of data by column does not output the cluster number of the columns. HCA of same data by row does output cluster number.




- [ ] **How can we reproduce the problem?** 
<!-- Upload a zip with the .ows file and data. -->
[Archive.zip](https://github.com/biolab/orange3/files/6595014/Archive.zip)
!-- Describe the steps (open this widget, click there, then add this...) -->
Perform HCA on data, selecting columns. Select all clusters individually. Output from HCA does not contain cluster numbers when data are clustered by column. Similar workflow in same file clustering by row works as expected. Attached file has workflow and primary data. 




- [ ] **What's your environment?**
<!-- To find your Orange version, see "Help → About → Version" or `Orange.version.full_version` in code -->
- Operating system: MacOS 11.4
- Orange version: 3.29.1
- How you installed Orange: Downloaded the install package from orangedatamining



Discussion:
It can't. The Hiearchical clustering widget outputs a data table whose rows represent data instances. When clustering by rows, each row belongs to a cluster, so the widget adds a column that shows the cluster. When clustering the columns, each column belongs to a cluster. The widget cannot add a row that would represent a cluster (because we have the same data type for the entire column, so this row would contain cells of different data types).

Cluster number could be indicated as a meta-information for each variable, but I don't know whether and how would this be useful. If anyone knows otherwise, please correct me.
Similarly to #5468, the cluster index could indeed be added as an attribute of variable, which would be useful if the Transpose widget turns this into a column. (Does it?)
It does. For example, "bow-feature" and "hidden" from Text add-on become meta attributes.

Resolving patch:
diff --git a/Orange/widgets/unsupervised/owhierarchicalclustering.py b/Orange/widgets/unsupervised/owhierarchicalclustering.py
--- a/Orange/widgets/unsupervised/owhierarchicalclustering.py
+++ b/Orange/widgets/unsupervised/owhierarchicalclustering.py
@@ -610,16 +610,29 @@ def commit(self):
                 selected_data.domain = Domain(
                     attrs, classes, metas + (clust_var, ))
 
+            annotated_data = create_annotated_table(data, selected_indices)
+
         elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0:
             # Select columns
+            attrs = []
+            for clust, indices in chain(enumerate(maps, start=1),
+                                        [(0, unselected_indices)]):
+                for i in indices:
+                    attr = items.domain[i].copy()
+                    attr.attributes["cluster"] = clust
+                    attrs.append(attr)
             domain = Orange.data.Domain(
-                [items.domain[i] for i in selected_indices],
+                # len(unselected_indices) can be 0
+                attrs[:len(attrs) - len(unselected_indices)],
                 items.domain.class_vars, items.domain.metas)
             selected_data = items.from_table(domain, items)
-            data = None
+
+            domain = Orange.data.Domain(
+                attrs,
+                items.domain.class_vars, items.domain.metas)
+            annotated_data = items.from_table(domain, items)
 
         self.Outputs.selected_data.send(selected_data)
-        annotated_data = create_annotated_table(data, selected_indices)
         self.Outputs.annotated_data.send(annotated_data)
 
     def eventFilter(self, obj, event):


Test patch:
diff --git a/Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py b/Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py
--- a/Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py
+++ b/Orange/widgets/unsupervised/tests/test_owhierarchicalclustering.py
@@ -26,6 +26,8 @@ def setUpClass(cls):
         cls.signal_data = cls.distances
         cls.same_input_output_domain = False
 
+        cls.distances_cols = Euclidean(cls.data, axis=0)
+
     def setUp(self):
         self.widget = self.create_widget(OWHierarchicalClustering)
 
@@ -35,6 +37,11 @@ def _select_data(self):
         self.widget.dendrogram.set_selected_items([cluster])
         return [14, 15, 32, 33]
 
+    def _select_data_columns(self):
+        items = self.widget.dendrogram._items
+        cluster = items[sorted(list(items.keys()))[5]]
+        self.widget.dendrogram.set_selected_items([cluster])
+
     def _compare_selected_annotated_domains(self, selected, annotated):
         self.assertEqual(annotated.domain.variables,
                          selected.domain.variables)
@@ -170,3 +177,17 @@ def test_restore_state(self):
         self.send_signal(w.Inputs.distances, self.distances, widget=w)
         ids_2 = self.get_output(w.Outputs.selected_data, widget=w).ids
         self.assertSequenceEqual(list(ids_1), list(ids_2))
+
+    def test_column_distances(self):
+        self.send_signal(self.widget.Inputs.distances, self.distances_cols)
+        self._select_data_columns()
+        o = self.get_output(self.widget.Outputs.annotated_data)
+        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]
+        self.assertEqual(annotated, [('sepal width', 1), ('petal length', 1),
+                                     ('sepal length', 0), ('petal width', 0)])
+
+        self.widget.selection_box.buttons[2].click()  # top N
+        o = self.get_output(self.widget.Outputs.annotated_data)
+        annotated = [(a.name, a.attributes['cluster']) for a in o.domain.attributes]
+        self.assertEqual(annotated, [('sepal length', 1), ('petal width', 2),
+                                     ('sepal width', 3), ('petal length', 3)])