Repository: biolab/orange3

Issue:
Discretize: uncaught error and strange binning
<!-- 
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 (```). -->

First issue: Naive Bayes fails with a strange error (AssertionError without context). After investigation, the issue is in Discretize, which tries to discretize to 4 bins where there are only 3 values.
Second issue: Discretize returns strange (likely wrong) bins.


- [ ] **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...) -->

[discretize-error.zip](https://github.com/biolab/orange3/files/5959719/discretize-error.zip)


- [ ] **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.28.dev
- How you installed Orange: conda


- [ ] **Additional information**

First error:
```
Traceback (most recent call last):
  File "/Users/ajda/opt/miniconda3/envs/o3/lib/python3.8/site-packages/orangewidget/gui.py", line 607, in eventFilter
    self.__onEditingFinished()
  File "/Users/ajda/opt/miniconda3/envs/o3/lib/python3.8/site-packages/orangewidget/gui.py", line 551, in __onEditingFinished
    self.__commitValue(value)
  File "/Users/ajda/opt/miniconda3/envs/o3/lib/python3.8/site-packages/orangewidget/gui.py", line 565, in __commitValue
    self.cfunc()
  File "/Users/ajda/opt/miniconda3/envs/o3/lib/python3.8/site-packages/orangewidget/gui.py", line 1923, in __call__
    self.func(**kwds)
  File "/Users/ajda/orange/orange3/Orange/widgets/data/owdiscretize.py", line 662, in _default_disc_changed
    self._update_points()
  File "/Users/ajda/orange/orange3/Orange/widgets/data/owdiscretize.py", line 599, in _update_points
    points, dvar = induce_cuts(state.method, self.data, var)
  File "/Users/ajda/orange/orange3/Orange/widgets/data/owdiscretize.py", line 585, in induce_cuts
    dvar = _dispatch[type(method)](method, data, var)
  File "/Users/ajda/orange/orange3/Orange/widgets/data/owdiscretize.py", line 48, in <lambda>
    lambda m, data, var: _dispatch[type(m.method)](m.method, data, var),
  File "/Users/ajda/orange/orange3/Orange/widgets/data/owdiscretize.py", line 51, in <lambda>
    EqualFreq: lambda m, data, var: disc.EqualFreq(m.k)(data, var),
  File "/Users/ajda/orange/orange3/Orange/preprocess/discretize.py", line 150, in __call__
    return Discretizer.create_discretized_var(
  File "/Users/ajda/orange/orange3/Orange/preprocess/discretize.py", line 71, in create_discretized_var
    values = [
  File "/Users/ajda/orange/orange3/Orange/preprocess/discretize.py", line 72, in <listcomp>
    cls._fmt_interval(low, high, fmt)
  File "/Users/ajda/orange/orange3/Orange/preprocess/discretize.py", line 53, in _fmt_interval
    assert low is None or high is None or low < high
AssertionError
```
The problem is it is badly formatted in Test and Score and unclear in Naive Bayes widget.

Second issue:
Why is this discretized to 3 bins when there are only 2 distinct values?

Var|Discretized
-|-
99.93|< 99.93
99.93|< 99.93
99.93|< 99.93
99.93|< 99.93
99.93|99.93 - 99.94
99.93|99.93 - 99.94
99.93|99.93 - 99.94
99.93|99.93 - 99.94
99.93|99.93 - 99.94
99.93|99.93 - 99.94
99.95|≥ 99.94
99.95|≥ 99.94
99.95|≥ 99.94



Discussion:
> Why is this discretized to 3 bins when there are only 2 distinct values?

Count'em.

```python
Var	Category
continuous	Bad Good
	class
99.93000000000002	Good
99.93000000000002	Good
99.93000000000002	Good
99.93000000000002	Good
99.93000000000004	Bad
99.93000000000004	Bad
99.93000000000004	Good
99.93000000000004	Good
99.93000000000004	Good
99.93000000000004	Good
99.95000000000002	Bad
99.95000000000002	Bad
99.95000000000006	Bad
```
I know about the rounding error, but how does the same float get rounded differently?
I don't understand - what do you mean by same float "rounded differently"? This data is read from file in which it has four distinct values there, hence three ((hum, three?) bins. I think that Orange works correctly regarding the second issue.

The first one is more interesting: differences between thresholds are below precision, so two consecutive thresholds are the same and assertion fails. I added `np.unique` to handle this, now I'm trying to write a test.

Resolving patch:
diff --git a/Orange/preprocess/discretize.py b/Orange/preprocess/discretize.py
--- a/Orange/preprocess/discretize.py
+++ b/Orange/preprocess/discretize.py
@@ -147,6 +147,8 @@ def __call__(self, data, attribute):
         else:
             d = distribution.get_distribution(data, attribute)
             points = _discretize.split_eq_freq(d, self.n)
+            # np.unique handles cases in which differences are below precision
+            points = list(np.unique(points))
         return Discretizer.create_discretized_var(
             data.domain[attribute], points)
 


Test patch:
diff --git a/Orange/tests/test_discretize.py b/Orange/tests/test_discretize.py
--- a/Orange/tests/test_discretize.py
+++ b/Orange/tests/test_discretize.py
@@ -1,6 +1,7 @@
 # Test methods with long descriptive names can omit docstrings
 # pylint: disable=missing-docstring
 
+import sys
 import random
 from unittest import TestCase
 
@@ -43,6 +44,26 @@ def test_equifreq_with_k_instances(self):
         self.assertEqual(len(dvar.values), 4)
         self.assertEqual(dvar.compute_value.points, [1.5, 2.5, 3.5])
 
+    def test_below_precision(self):
+        eps = sys.float_info.epsilon
+
+        # Test with n >= number of distinct values
+        X = np.array([[1], [1 + eps], [1 + 2 * eps], [1 + 3 * eps]])
+        # Test the test: check that these are indeed distinct values
+        assert len(np.unique(X).flatten()) == 4
+        table = data.Table.from_numpy(None, X)
+        var = discretize.EqualFreq(n=4)(table, table.domain[0])
+        points = var.compute_value.points
+        self.assertEqual(len(np.unique(points)), len(points))
+
+        # Test with n < number of distinct values
+        X = np.array([[1 + i * eps] for i in range(10)])
+        # Test the test: check that these are indeed distinct values
+        assert len(np.unique(X).flatten()) == 10
+        table = data.Table.from_numpy(None, X)
+        var = discretize.EqualFreq(n=8)(table, table.domain[0])
+        points = var.compute_value.points
+        self.assertEqual(len(np.unique(points)), len(points))
 
 # noinspection PyPep8Naming
 class TestEqualWidth(TestCase):