Repository: biolab/orange3

Issue:
Test and Score: move Target Class and Model Comparison closer the results they influence
Someone wrote me an email asking me why this happens to their data. I tried on `iris` and this is what I get:

![Screenshot_2022-01-05_16-50-27](https://user-images.githubusercontent.com/552182/148247474-1510a858-32c2-483f-91e5-00584aba69e5.png)

Is anyone aware of any theory why would this happen so regularly?

On `titanic`, I only see CA = Recall, Precision returns something else.


Discussion:
Got the same mail, literally just looking into this. 😅 
For me, Recall seems off. Logistic regression on heart_disease. If I understand Recall correctly, it should be
146/173 for class = 0
and
112/130 for class = 1
Hence the average of the two values would be 0.8527. Actual Recall as reported in T&S is 0.851. Not far off, but not the same.

<img width="305" alt="Screen Shot 2022-01-05 at 16 59 17" src="https://user-images.githubusercontent.com/12524972/148248843-533f95d0-4fe5-4e6d-bf4c-8eb230a6ccc4.png">


I think that you should have divided TP with the number of positives in the data. And averages are weighted averages. So, CA and recall, respectively:

```
>>> (146+112)/303
0.8514851485148515
>>> 146/164*164/303 + 112/139*139/303
0.8514851485148515
```

To compute a useful recall the users have to select the target class. If they do not, the (weighted) average is used. And in (weighted) averaged recall, the P part cancels out and we get CA. Fun, isn't it? So I'd say that recall seems OK.

To get useful recall scores, users have to select the target class.

The same is then true for F1. But don't we just take one class as target?! Like in, say, nomogram?
Yea, there is a combo in the UI which allows you to do just that. :)
The Predictions widget is missing the target class value selector box though.
We could move "Target class combo" above "Evaluation Results" and "Model Comparison" above "Model Comparison".

Predictions needs combo for choosing the target class: issue #5743.
Would this look OK? I removed box titles not only because they're redundant but mostly because they made everything look messy (they use a smaller font and are placed outside the flow).

I like what this change does to the left side.

<img width="805" alt="Screenshot 2022-01-08 at 10 32 16" src="https://user-images.githubusercontent.com/2387315/148639290-09c99dac-2222-4c1f-8bc0-31df035a7890.png">


I like it! Thanks!

Resolving patch:
diff --git a/Orange/widgets/evaluate/owtestandscore.py b/Orange/widgets/evaluate/owtestandscore.py
--- a/Orange/widgets/evaluate/owtestandscore.py
+++ b/Orange/widgets/evaluate/owtestandscore.py
@@ -22,7 +22,8 @@
 from AnyQt.QtCore import Qt, QSize, QThread
 from AnyQt.QtCore import pyqtSlot as Slot
 from AnyQt.QtGui import QStandardItem, QDoubleValidator
-from AnyQt.QtWidgets import QHeaderView, QTableWidget, QLabel
+from AnyQt.QtWidgets import \
+    QHeaderView, QTableWidget, QLabel, QComboBox, QSizePolicy
 
 from Orange.base import Learner
 import Orange.classification
@@ -189,7 +190,7 @@ class Outputs:
     rope = settings.Setting(0.1)
     comparison_criterion = settings.Setting(0, schema_only=True)
 
-    TARGET_AVERAGE = "(Average over classes)"
+    TARGET_AVERAGE = "(None, show average over classes)"
     class_selection = settings.ContextSetting(TARGET_AVERAGE)
 
     class Error(OWWidget.Error):
@@ -244,7 +245,7 @@ def __init__(self):
         self.__task = None  # type: Optional[TaskState]
         self.__executor = ThreadExecutor()
 
-        sbox = gui.vBox(self.controlArea, "Sampling")
+        sbox = gui.vBox(self.controlArea, box=True)
         rbox = gui.radioButtons(
             sbox, self, "resampling", callback=self._param_changed)
 
@@ -287,37 +288,41 @@ def __init__(self):
         gui.appendRadioButton(rbox, "Test on train data")
         gui.appendRadioButton(rbox, "Test on test data")
 
-        self.cbox = gui.vBox(self.controlArea, "Target Class")
+        gui.rubber(self.controlArea)
+
+        self.score_table = ScoreTable(self)
+        self.score_table.shownScoresChanged.connect(self.update_stats_model)
+        view = self.score_table.view
+        view.setSizeAdjustPolicy(view.AdjustToContents)
+
+        self.results_box = gui.vBox(self.mainArea, box=True)
+        self.cbox = gui.hBox(self.results_box)
         self.class_selection_combo = gui.comboBox(
             self.cbox, self, "class_selection", items=[],
-            sendSelectedValue=True, contentsLength=8, searchable=True,
+            label="Evaluation results for target", orientation=Qt.Horizontal,
+            sendSelectedValue=True, searchable=True, contentsLength=25,
             callback=self._on_target_class_changed
         )
-
-        self.modcompbox = box = gui.vBox(self.controlArea, "Model Comparison")
-        gui.comboBox(
-            box, self, "comparison_criterion",
-            callback=self.update_comparison_table)
-
-        hbox = gui.hBox(box)
-        gui.checkBox(hbox, self, "use_rope",
-                     "Negligible difference: ",
+        self.cbox.layout().addStretch(100)
+        self.class_selection_combo.setMaximumContentsLength(30)
+        self.results_box.layout().addWidget(self.score_table.view)
+
+        gui.separator(self.mainArea, 16)
+        self.compbox = box = gui.vBox(self.mainArea, box=True)
+        cbox = gui.comboBox(
+            box, self, "comparison_criterion", label="Compare models by:",
+            sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon,
+            sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed),
+            orientation=Qt.Horizontal, callback=self.update_comparison_table).box
+
+        gui.separator(cbox, 8)
+        gui.checkBox(cbox, self, "use_rope", "Negligible diff.: ",
                      callback=self._on_use_rope_changed)
-        gui.lineEdit(hbox, self, "rope", validator=QDoubleValidator(),
-                     controlWidth=70, callback=self.update_comparison_table,
+        gui.lineEdit(cbox, self, "rope", validator=QDoubleValidator(),
+                     controlWidth=50, callback=self.update_comparison_table,
                      alignment=Qt.AlignRight)
         self.controls.rope.setEnabled(self.use_rope)
 
-        gui.rubber(self.controlArea)
-        self.score_table = ScoreTable(self)
-        self.score_table.shownScoresChanged.connect(self.update_stats_model)
-        view = self.score_table.view
-        view.setSizeAdjustPolicy(view.AdjustToContents)
-
-        box = gui.vBox(self.mainArea, "Evaluation Results")
-        box.layout().addWidget(self.score_table.view)
-
-        self.compbox = box = gui.vBox(self.mainArea, box="Model comparison")
         table = self.comparison_table = QTableWidget(
             wordWrap=False, editTriggers=QTableWidget.NoEditTriggers,
             selectionMode=QTableWidget.NoSelection)
@@ -530,15 +535,6 @@ def _update_scorers(self):
             if self.__pending_comparison_criterion < len(self.scorers):
                 self.comparison_criterion = self.__pending_comparison_criterion
             self.__pending_comparison_criterion = None
-        self._update_compbox_title()
-
-    def _update_compbox_title(self):
-        criterion = self.comparison_criterion
-        if criterion < len(self.scorers):
-            scorer = self.scorers[criterion]()
-            self.compbox.setTitle(f"Model Comparison by {scorer.name}")
-        else:
-            self.compbox.setTitle(f"Model Comparison")
 
     @Inputs.preprocessor
     def set_preprocessor(self, preproc):
@@ -570,13 +566,12 @@ def shuffle_split_changed(self):
         self._param_changed()
 
     def _param_changed(self):
-        self.modcompbox.setEnabled(self.resampling == OWTestAndScore.KFold)
         self._update_view_enabled()
         self._invalidate()
         self.__update()
 
     def _update_view_enabled(self):
-        self.comparison_table.setEnabled(
+        self.compbox.setEnabled(
             self.resampling == OWTestAndScore.KFold
             and len(self.learners) > 1
             and self.data is not None)
@@ -719,7 +714,6 @@ def _set_comparison_headers(self, names):
 
     def _scores_by_folds(self, slots):
         scorer = self.scorers[self.comparison_criterion]()
-        self._update_compbox_title()
         if scorer.is_binary:
             if self.class_selection != self.TARGET_AVERAGE:
                 class_var = self.data.domain.class_var


Test patch:
diff --git a/Orange/widgets/evaluate/tests/test_owtestandscore.py b/Orange/widgets/evaluate/tests/test_owtestandscore.py
--- a/Orange/widgets/evaluate/tests/test_owtestandscore.py
+++ b/Orange/widgets/evaluate/tests/test_owtestandscore.py
@@ -513,24 +513,21 @@ def test_comparison_requires_cv(self):
         rbs[OWTestAndScore.KFold].click()
         self.get_output(self.widget.Outputs.evaluations_results, wait=5000)
         self.assertIsNotNone(w.comparison_table.cellWidget(0, 1))
-        self.assertTrue(w.modcompbox.isEnabled())
-        self.assertTrue(w.comparison_table.isEnabled())
+        self.assertTrue(w.compbox.isEnabled())
         baycomp.two_on_single.assert_called()
         baycomp.two_on_single.reset_mock()
 
         rbs[OWTestAndScore.LeaveOneOut].click()
         self.get_output(self.widget.Outputs.evaluations_results, wait=5000)
         self.assertIsNone(w.comparison_table.cellWidget(0, 1))
-        self.assertFalse(w.modcompbox.isEnabled())
-        self.assertFalse(w.comparison_table.isEnabled())
+        self.assertFalse(w.compbox.isEnabled())
         baycomp.two_on_single.assert_not_called()
         baycomp.two_on_single.reset_mock()
 
         rbs[OWTestAndScore.KFold].click()
         self.get_output(self.widget.Outputs.evaluations_results, wait=5000)
         self.assertIsNotNone(w.comparison_table.cellWidget(0, 1))
-        self.assertTrue(w.modcompbox.isEnabled())
-        self.assertTrue(w.comparison_table.isEnabled())
+        self.assertTrue(w.compbox.isEnabled())
         baycomp.two_on_single.assert_called()
         baycomp.two_on_single.reset_mock()