Repository: biolab/orange3

Issue:
Pythagorean Tree crashes when selecting data with categorical variables
##### Orange version
master


##### Expected behavior
Selecting a node in tree should always send data to output.


##### Actual behavior
For a specific case, this fails.


##### Steps to reproduce the behavior
![image](https://user-images.githubusercontent.com/5758119/48931629-b53ff300-eef7-11e8-98d9-84f1f183f533.png)

In File, select a data set containing categorical variables (e.g. titanic, breast-cancer), select a tree in the random forest visualization and then try to select a node in the Pythagorean forest widget.

The widget crashes with an error like this:
```
Traceback (most recent call last):
  File "/home/pavlin/dev/orange3/Orange/widgets/visualize/owpythagorastree.py", line 309, in commit
    data = self.tree_adapter.get_instances_in_nodes(nodes)
  File "/home/pavlin/dev/orange3/Orange/widgets/visualize/utils/tree/skltreeadapter.py", line 260, in get_instances_in_nodes
    leaf_samples = self.get_samples_in_leaves()
  File "/home/pavlin/dev/orange3/Orange/widgets/visualize/utils/tree/skltreeadapter.py", line 243, in get_samples_in_leaves
    leaf_indices = assign(0, items)
  File "/home/pavlin/dev/orange3/Orange/widgets/visualize/utils/tree/skltreeadapter.py", line 229, in assign
    column = self.instances.X[indices, feature_idx]
IndexError: index 10 is out of bounds for axis 1 with size 9
```


Discussion:
There's another problem with Pythagorean Trees coming from a Random Forest (using the SklTreeAdapter). The root's tooltip for the Iris dataset shows 55/150, but there's 50 instances of each class.
Mistery solved: caused by bootstrap sampling in sklearn's random forest.

Resolving patch:
diff --git a/Orange/widgets/visualize/utils/tree/skltreeadapter.py b/Orange/widgets/visualize/utils/tree/skltreeadapter.py
--- a/Orange/widgets/visualize/utils/tree/skltreeadapter.py
+++ b/Orange/widgets/visualize/utils/tree/skltreeadapter.py
@@ -151,8 +151,9 @@ def short_rule(self, node):
 
     def attribute(self, node):
         feature_idx = self.splitting_attribute(node)
-        if feature_idx != self.FEATURE_UNDEFINED:
-            return self.domain.attributes[self.splitting_attribute(node)]
+        if feature_idx == self.FEATURE_UNDEFINED:
+            return None
+        return self.domain.attributes[self.splitting_attribute(node)]
 
     def splitting_attribute(self, node):
         return self._tree.feature[node]
@@ -236,7 +237,7 @@ def assign(node_id, indices):
                 feature_idx = self._tree.feature[node_id]
                 thresh = self._tree.threshold[node_id]
 
-                column = self.instances.X[indices, feature_idx]
+                column = self.instances_transformed.X[indices, feature_idx]
                 leftmask = column <= thresh
                 leftind = assign(self._tree.children_left[node_id],
                                  indices[leftmask])
diff --git a/Orange/widgets/visualize/utils/tree/treeadapter.py b/Orange/widgets/visualize/utils/tree/treeadapter.py
--- a/Orange/widgets/visualize/utils/tree/treeadapter.py
+++ b/Orange/widgets/visualize/utils/tree/treeadapter.py
@@ -22,6 +22,7 @@ def __init__(self, model):
         self.model = model
         self.domain = model.domain
         self.instances = model.instances
+        self.instances_transformed = self.instances.transform(self.domain)
 
     @abstractmethod
     def weight(self, node):
@@ -40,7 +41,6 @@ def weight(self, node):
             The weight of the node relative to its siblings.
 
         """
-        pass
 
     @abstractmethod
     def num_samples(self, node):
@@ -56,7 +56,6 @@ def num_samples(self, node):
         int
 
         """
-        pass
 
     @abstractmethod
     def parent(self, node):
@@ -71,7 +70,6 @@ def parent(self, node):
         object
 
         """
-        pass
 
     @abstractmethod
     def has_children(self, node):
@@ -86,7 +84,6 @@ def has_children(self, node):
         bool
 
         """
-        pass
 
     def is_leaf(self, node):
         """Check if the given node is a leaf node.
@@ -116,7 +113,6 @@ def children(self, node):
             A iterable object containing the labels of the child nodes.
 
         """
-        pass
 
     def reverse_children(self, node):
         """Reverse children of a given node.
@@ -125,12 +121,10 @@ def reverse_children(self, node):
         ----------
         node : object
         """
-        pass
 
     def shuffle_children(self):
         """Randomly shuffle node's children in the entire tree.
         """
-        pass
 
     @abstractmethod
     def get_distribution(self, node):
@@ -151,7 +145,6 @@ def get_distribution(self, node):
             the number of nodes that belong to a given class inside the node.
 
         """
-        pass
 
     @abstractmethod
     def get_impurity(self, node):
@@ -166,7 +159,6 @@ def get_impurity(self, node):
         object
 
         """
-        pass
 
     @abstractmethod
     def rules(self, node):
@@ -182,7 +174,6 @@ def rules(self, node):
             A list of Rule objects, can be of any type.
 
         """
-        pass
 
     @abstractmethod
     def short_rule(self, node):
@@ -200,7 +191,6 @@ def attribute(self, node):
         -------
 
         """
-        pass
 
     def is_root(self, node):
         """Check if a given node is the root node.
@@ -227,7 +217,6 @@ def leaves(self, node):
         -------
 
         """
-        pass
 
     @abstractmethod
     def get_instances_in_nodes(self, dataset, nodes):
@@ -245,7 +234,6 @@ def get_instances_in_nodes(self, dataset, nodes):
         -------
 
         """
-        pass
 
     @abstractmethod
     def get_indices(self, nodes):
@@ -261,7 +249,6 @@ def max_depth(self):
         int
 
         """
-        pass
 
     @property
     @abstractmethod
@@ -276,7 +263,6 @@ def num_nodes(self):
         int
 
         """
-        pass
 
     @property
     @abstractmethod
@@ -288,7 +274,6 @@ def root(self):
         object
 
         """
-        pass
 
 
 class TreeAdapter(BaseTreeAdapter):


Test patch:
diff --git a/Orange/widgets/visualize/tests/test_owpythagorastree.py b/Orange/widgets/visualize/tests/test_owpythagorastree.py
--- a/Orange/widgets/visualize/tests/test_owpythagorastree.py
+++ b/Orange/widgets/visualize/tests/test_owpythagorastree.py
@@ -4,11 +4,14 @@
 
 from os import path
 
+from Orange.classification.random_forest import RandomForestLearner
 from Orange.data import Table
 from Orange.modelling import TreeLearner
+from Orange.regression.random_forest import RandomForestRegressionLearner
 from Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin
 from Orange.widgets.tests.utils import simulate
 from Orange.widgets.visualize.owpythagorastree import OWPythagorasTree
+from Orange.widgets.visualize.owpythagoreanforest import OWPythagoreanForest
 from Orange.widgets.visualize.pythagorastreeviewer import (
     PythagorasTree,
     Point,
@@ -359,3 +362,24 @@ def _callback():
         # Check that individual squares all have the same color
         colors_same = [self._check_all_same(x) for x in zip(*colors)]
         self.assertTrue(all(colors_same))
+
+    def test_forest_tree_table(self):
+        titanic_data = Table('titanic')[::50]
+        titanic = RandomForestLearner(n_estimators=3)(titanic_data)
+        titanic.instances = titanic_data
+
+        housing_data = Table('housing')[:10]
+        housing = RandomForestRegressionLearner(n_estimators=3)(housing_data)
+        housing.instances = housing_data
+
+        forest_w = self.create_widget(OWPythagoreanForest)
+        for data in (housing, titanic):
+            self.send_signal(forest_w.Inputs.random_forest, data, widget=forest_w)
+            tree = forest_w.forest_model[0].model
+
+            tree_w = self.widget
+            self.send_signal(tree_w.Inputs.tree, tree, widget=tree_w)
+            square = [i for i in tree_w.scene.items() if isinstance(i, SquareGraphicsItem)][-1]
+            square.setSelected(True)
+            tab = self.get_output(tree_w.Outputs.selected_data, widget=tree_w)
+            self.assertGreater(len(tab), 0)