Repository: biolab/orange3

Issue:
Nomogram: IndexError when changing target class
<!-- 
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 (```). -->
Nomogram fails with the below error when changing the target class on renamed categories.

```
Traceback (most recent call last):
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/orangecanvas/scheme/signalmanager.py", line 1024, in __process_next
    if self.__process_next_helper(use_max_active=True):
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/orangecanvas/scheme/signalmanager.py", line 1062, in __process_next_helper
    self.process_node(selected_node)
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/orangecanvas/scheme/signalmanager.py", line 690, in process_node
    self.send_to_node(node, signals_in)
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/orangewidget/workflow/widgetsscheme.py", line 806, in send_to_node
    self.process_signals_for_widget(node, widget, signals)
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/orangewidget/workflow/widgetsscheme.py", line 820, in process_signals_for_widget
    process_signals_for_widget(widget, signals, workflow)
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/functools.py", line 875, in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/orangewidget/workflow/widgetsscheme.py", line 921, in process_signals_for_widget
    process_signal_input(input_meta, widget, signal, workflow)
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/functools.py", line 875, in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/orangewidget/workflow/widgetsscheme.py", line 884, in process_signal_input_default
    notify_input_helper(
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/functools.py", line 875, in wrapper
    return dispatch(args[0].__class__)(*args, **kw)
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/orangewidget/utils/signals.py", line 650, in set_input_helper
    handler(*args)
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/orangewidget/utils/signals.py", line 201, in summarize_wrapper
    method(widget, value)
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/Orange/widgets/visualize/ownomogram.py", line 886, in set_classifier
    self.update_scene()
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/Orange/widgets/visualize/ownomogram.py", line 950, in update_scene
    attr_inds, attributes = zip(*self.get_ordered_attributes()[:n_attrs])
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/Orange/widgets/visualize/ownomogram.py", line 1145, in get_ordered_attributes
    return sorted(enumerate(attrs), key=key)
  File "/Applications/Orange3.app/Contents/Frameworks/Python.framework/Versions/3.8/lib/python3.8/site-packages/Orange/widgets/visualize/ownomogram.py", line 1123, in key
    coef = np.abs(self.log_reg_coeffs_orig[i][class_value]).mean()
IndexError: index 2 is out of bounds for axis 0 with size 1
```

**How can we reproduce the problem?** 
<!-- Upload a zip with the .ows file and data. -->
<!-- Describe the steps (open this widget, click there, then add this...) -->
File (iris) - Edit Domain. Rename iris-versicolor to foo. Select Rows: iris is not `foo`. Logistic Regression - Nomogram. Try to change target class.


**What's your environment?**
<!-- To find your Orange version, see "Help → About → Version" or `Orange.version.full_version` in code -->
- Operating system: OSX
- Orange version: 3.32
- How you installed Orange: dmg (but the same issue appears on master)



Discussion:

Resolving patch:
diff --git a/Orange/widgets/visualize/ownomogram.py b/Orange/widgets/visualize/ownomogram.py
--- a/Orange/widgets/visualize/ownomogram.py
+++ b/Orange/widgets/visualize/ownomogram.py
@@ -910,8 +910,7 @@ def calculate_log_reg_coefficients(self):
         if not isinstance(self.classifier, LogisticRegressionClassifier):
             return
 
-        self.domain = self.reconstruct_domain(self.classifier.original_domain,
-                                              self.domain)
+        self.domain = self.reconstruct_domain(self.classifier, self.domain)
         self.data = self.classifier.original_data.transform(self.domain)
         attrs, ranges, start = self.domain.attributes, [], 0
         for attr in attrs:
@@ -1273,8 +1272,9 @@ def send_report(self):
         self.report_plot()
 
     @staticmethod
-    def reconstruct_domain(original, preprocessed):
+    def reconstruct_domain(classifier: Model, preprocessed: Domain) -> Domain:
         # abuse dict to make "in" comparisons faster
+        original = classifier.original_domain
         attrs = OrderedDict()
         for attr in preprocessed.attributes:
             cv = attr._compute_value.variable._compute_value
@@ -1284,7 +1284,13 @@ def reconstruct_domain(original, preprocessed):
                 continue
             attrs[var] = None   # we only need keys
         attrs = list(attrs.keys())
-        return Domain(attrs, original.class_var, original.metas)
+
+        orig_clv = original.class_var
+        orig_data = classifier.original_data
+        values = (orig_clv.values[int(i)] for i in
+                  np.unique(orig_data.get_column_view(orig_clv)[0]))
+        class_var = DiscreteVariable(original.class_var.name, values)
+        return Domain(attrs, class_var, original.metas)
 
     @staticmethod
     def get_ruler_values(start, stop, max_width, round_to_nearest=True):


Test patch:
diff --git a/Orange/widgets/visualize/tests/test_ownomogram.py b/Orange/widgets/visualize/tests/test_ownomogram.py
--- a/Orange/widgets/visualize/tests/test_ownomogram.py
+++ b/Orange/widgets/visualize/tests/test_ownomogram.py
@@ -14,6 +14,7 @@
 from Orange.preprocess import Scale, Continuize
 from Orange.tests import test_filename
 from Orange.widgets.tests.base import WidgetTest
+from Orange.widgets.tests.utils import simulate
 from Orange.widgets.visualize.ownomogram import (
     OWNomogram, DiscreteFeatureItem, ContinuousFeatureItem, ProbabilitiesDotItem,
     MovableToolTip
@@ -293,25 +294,52 @@ def test_dots_stop_flashing(self):
     def test_reconstruct_domain(self):
         data = Table("heart_disease")
         cls = LogisticRegressionLearner()(data)
-        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)
+        domain = OWNomogram.reconstruct_domain(cls, cls.domain)
         transformed_data = cls.original_data.transform(domain)
         self.assertEqual(transformed_data.X.shape, data.X.shape)
         self.assertFalse(np.isnan(transformed_data.X[0]).any())
 
         scaled_data = Scale()(data)
         cls = LogisticRegressionLearner()(scaled_data)
-        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)
+        domain = OWNomogram.reconstruct_domain(cls, cls.domain)
         transformed_data = cls.original_data.transform(domain)
         self.assertEqual(transformed_data.X.shape, scaled_data.X.shape)
         self.assertFalse(np.isnan(transformed_data.X[0]).any())
 
         disc_data = Continuize()(data)
         cls = LogisticRegressionLearner()(disc_data)
-        domain = OWNomogram.reconstruct_domain(cls.original_domain, cls.domain)
+        domain = OWNomogram.reconstruct_domain(cls, cls.domain)
         transformed_data = cls.original_data.transform(domain)
         self.assertEqual(transformed_data.X.shape, disc_data.X.shape)
         self.assertFalse(np.isnan(transformed_data.X[0]).any())
 
+    def test_missing_class_value(self):
+        iris = Table("iris")
+        iris_set_ver = iris[:100]
+        target_cb = self.widget.controls.target_class_index
+
+        lr = LogisticRegressionLearner()(iris)
+        self.send_signal(self.widget.Inputs.classifier, lr)
+        simulate.combobox_activate_index(target_cb, 2)
+        self.assertEqual(target_cb.currentIndex(), 2)
+        self.assertEqual(target_cb.count(), 3)
+
+        lr = LogisticRegressionLearner()(iris_set_ver)
+        self.send_signal(self.widget.Inputs.classifier, lr)
+        self.assertEqual(target_cb.currentIndex(), 0)
+        self.assertEqual(target_cb.count(), 2)
+
+        nb = NaiveBayesLearner()(iris)
+        self.send_signal(self.widget.Inputs.classifier, nb)
+        simulate.combobox_activate_index(target_cb, 2)
+        self.assertEqual(target_cb.currentIndex(), 2)
+        self.assertEqual(target_cb.count(), 3)
+
+        nb = NaiveBayesLearner()(iris_set_ver)
+        self.send_signal(self.widget.Inputs.classifier, nb)
+        self.assertEqual(target_cb.currentIndex(), 2)
+        self.assertEqual(target_cb.count(), 3)
+
 
 if __name__ == "__main__":
     unittest.main()