Repository: biolab/orange3

Issue:
Boxplot: Ordering crashes when statistics cannot be computed for some attributes
https://sentry.io/biolab/orange3/issues/196744182/

```
TypeError:  unorderable types: float() < NoneType()
(1 additional frame(s) were not displayed)
...
  Module "Orange.widgets.visualize.owboxplot", line 281, in set_data

    self.grouping_changed()
  Module "Orange.widgets.visualize.owboxplot", line 346, in grouping_changed

    self.attr_changed()
  Module "Orange.widgets.visualize.owboxplot", line 358, in attr_changed

    self.layout_changed()
  Module "Orange.widgets.visualize.owboxplot", line 438, in layout_changed

    self.display_changed()
  Module "Orange.widgets.visualize.owboxplot", line 451, in display_changed

    self.order, key=lambda i: getattr(self.stats[i], criterion))
```


Discussion:

Resolving patch:
diff --git a/Orange/widgets/visualize/owboxplot.py b/Orange/widgets/visualize/owboxplot.py
--- a/Orange/widgets/visualize/owboxplot.py
+++ b/Orange/widgets/visualize/owboxplot.py
@@ -447,8 +447,11 @@ def display_changed(self):
         self.order = list(range(len(self.stats)))
         criterion = self._sorting_criteria_attrs[self.compare]
         if criterion:
-            self.order = sorted(
-                self.order, key=lambda i: getattr(self.stats[i], criterion))
+            vals = [getattr(stat, criterion) for stat in self.stats]
+            overmax = max((val for val in vals if val is not None), default=0) \
+                      + 1
+            vals = [val if val is not None else overmax for val in vals]
+            self.order = sorted(self.order, key=vals.__getitem__)
 
         heights = 90 if self.show_annotations else 60
 
@@ -553,6 +556,8 @@ def compute_tests(self):
         # The non-parametric tests can't do this, so we use statistics.tests
         def stat_ttest():
             d1, d2 = self.stats
+            if d1.n == 0 or d2.n == 0:
+                return np.nan, np.nan
             pooled_var = d1.var / d1.n + d2.var / d2.n
             df = pooled_var ** 2 / \
                 ((d1.var / d1.n) ** 2 / (d1.n - 1) +
@@ -566,6 +571,8 @@ def stat_ttest():
         # TODO: Check this function
         # noinspection PyPep8Naming
         def stat_ANOVA():
+            if any(stat.n == 0 for stat in self.stats):
+                return np.nan, np.nan
             n = sum(stat.n for stat in self.stats)
             grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n
             var_between = sum(stat.n * (stat.mean - grand_avg) ** 2
@@ -892,8 +899,11 @@ def line(y0, y1):
         y_up = -len(self.stats) * height + 10
         for pos, box_index in enumerate(self.order):
             stat = self.stats[box_index]
-            x = getattr(stat, crit_line) * self.scale_x
-            xs.append(x)
+            x = getattr(stat, crit_line)
+            if x is None:
+                continue
+            x *= self.scale_x
+            xs.append(x * self.scale_x)
             by = y_up + pos * height
             line(by + 12, 3)
             line(by - 12, by - 25)


Test patch:
diff --git a/Orange/widgets/visualize/tests/test_owboxplot.py b/Orange/widgets/visualize/tests/test_owboxplot.py
--- a/Orange/widgets/visualize/tests/test_owboxplot.py
+++ b/Orange/widgets/visualize/tests/test_owboxplot.py
@@ -128,6 +128,11 @@ def select_group(i):
                           'rest ECG', 'cholesterol',
                           'fasting blood sugar > 120', 'diameter narrowing'])
 
+    def test_box_order_when_missing_stats(self):
+        self.widget.compare = 1
+        # The widget can't do anything smart here, but shouldn't crash
+        self.send_signal("Data", self.iris[49:51])
+
     def test_saved_selection(self):
         self.send_signal("Data", self.data)
         selected_indices = self._select_data()