Repository: biolab/orange3

Issue:
EntropyMDL._normalize does not account if X is 0's
<!-- 
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 (```). -->


[`EntropyMDL._normalize()`](https://github.com/biolab/orange3/blob/71b790662cfb5c712be16c067060eaddc671b034/Orange/preprocess/discretize.py#L691) divides by the scale which is the sum.
When X (across the axis) is 0's scale is 0 and therefore the denominator is 0 and you get a warning and a nan back.

`python3.9/site-packages/Orange/preprocess/discretize.py:551: RuntimeWarning: invalid value encountered in true_divide
  return X / scale`

I'm not sure if we'd want to enforce a warning, or just return 0 for those.

**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...) -->


```python
from Orange.preprocess import EntropyMDL
EntropyMDL._normalize( [[1., 0.], [0., 0.]], axis=1)
```


**What's your environment?**
<!-- To find your Orange version, see "Help → About → Version" or `Orange.version.full_version` in code -->
- Operating system: Ubuntu/Linux
- Orange version: 3.32.0
- How you installed Orange: mamba/conda



Discussion:
Thanks for reporting this.

This function normalizes rows/columns to have a sum of 1, therefore I think that all-zero rows/columns should be replaced by 1 divided by the number of columns/rows.
I saw your "bio" on github. :)

I obliged and added type hints to function definition (#6102). Happy?
Thanks for the fixes and for the type hints 😆  I appreciate it!

Resolving patch:
diff --git a/Orange/preprocess/discretize.py b/Orange/preprocess/discretize.py
--- a/Orange/preprocess/discretize.py
+++ b/Orange/preprocess/discretize.py
@@ -1,7 +1,8 @@
 import calendar
 import re
 import time
-from typing import NamedTuple, List, Union, Callable
+from numbers import Number
+from typing import NamedTuple, List, Union, Callable, Optional
 import datetime
 from itertools import count
 
@@ -672,7 +673,10 @@ def __call__(self, data, attribute):
             data.domain[attribute], points)
 
     @classmethod
-    def _normalize(cls, X, axis=None, out=None):
+    def _normalize(cls,
+                   X: Union[List[List[Number]], np.ndarray],
+                   axis: Optional[int] = None,
+                   out: Optional[np.ndarray] = None) -> np.ndarray:
         """
         Normalize `X` array so it sums to 1.0 over the `axis`.
 
@@ -687,6 +691,7 @@ def _normalize(cls, X, axis=None, out=None):
         """
         X = np.asarray(X, dtype=float)
         scale = np.sum(X, axis=axis, keepdims=True)
+        scale[scale == 0] = 1
         if out is None:
             return X / scale
         else:
@@ -716,6 +721,8 @@ def _entropy_normalized(cls, D, axis=None):
         # req: np.all(np.abs(np.sum(D, axis=axis) - 1) < 1e-9)
 
         D = np.asarray(D)
+        if np.sum(D) == 0:
+            return 0
         Dc = np.clip(D, np.finfo(D.dtype).eps, 1.0)
         return - np.sum(D * np.log2(Dc), axis=axis)
 


Test patch: