Repository: biolab/orange3

Issue:
SOM Error
<!-- 
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 (```). -->
Error when changing input data to SOM
```
Traceback (most recent call last):
  File "C:\Users\aljaz\AppData\Local\Programs\Orange\lib\site-packages\Orange\widgets\unsupervised\owsom.py", line 694, in run
    self.som.fit(self.data, N_ITERATIONS,
  File "C:\Users\aljaz\AppData\Local\Programs\Orange\lib\site-packages\Orange\projection\som.py", line 54, in fit
    self.init_weights_pca(x)
  File "C:\Users\aljaz\AppData\Local\Programs\Orange\lib\site-packages\Orange\projection\som.py", line 27, in init_weights_pca
    pc_length, pc = np.linalg.eig(np.cov(x.T))
  File "<__array_function__ internals>", line 5, in eig
  File "C:\Users\aljaz\AppData\Local\Programs\Orange\lib\site-packages\numpy\linalg\linalg.py", line 1318, in eig
    _assert_finite(a)
  File "C:\Users\aljaz\AppData\Local\Programs\Orange\lib\site-packages\numpy\linalg\linalg.py", line 209, in _assert_finite
    raise LinAlgError("Array must not contain infs or NaNs")
numpy.linalg.LinAlgError: Array must not contain infs or NaNs
```




**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...) -->
[err.zip](https://github.com/biolab/orange3/files/10195313/err.zip)
Paint Data -> SOM(PCA, network 10x10) -> Data Table
Open Paint Data, add exactly 1 (put) point, SOM throws an error.




**What's your environment?**
<!-- To find your Orange version, see "Help → About → Version" or `Orange.version.full_version` in code -->
- Operating system: Windows 11 
- Orange version:  3.31.1
- How you installed Orange: installer



Discussion:
You've find yourself a perfect task for your introduction to coding Orange. :)

I suggest you do the following:

- Fork this repository if you haven't done so already.

1. Add a test in which SOM gets a single data point. See the existing tests and modify one of them (say `test_missing_one_row_data`) to pass data with a single row to the widget. (If you have some table, say `self.iris`, use `self.iris[:1]` to get a table that only contains the first row.) Such tests should fail. (I tried and it does.)
2. Then write another test in which you have multiple rows, but every row except has some missing value. I suppose this will fail as well.
3. Then fix the widget. See `class Error` and search for `self.Error` within code to understand how errors work. There is, in fact, a place that already reports a similar error (no rows without missing values). I suppose that you could simply modify this error message (and the corresponding check that triggers it) to something like `"SOM needs at least two data rows without missing values"`. Also rename the message from `no_defined_rows` to `not_enough_data`.
4. The tests should no longer fail.
5. Now improve the tests: they should check that the error is shown. See how other tests do it. Also check that error messages disappear if the widget then receives proper data. Continue by triggering the error again (by using faulty data) and then check that the error disappears when the widget is given `None` as data. (This shouldn't be a problem if you're fix is indeed going to consist just of minor modification of the existing code.

- Make a pull request.

If you need any help, ask.



Resolving patch:
diff --git a/Orange/widgets/unsupervised/owsom.py b/Orange/widgets/unsupervised/owsom.py
--- a/Orange/widgets/unsupervised/owsom.py
+++ b/Orange/widgets/unsupervised/owsom.py
@@ -220,7 +220,7 @@ class Warning(OWWidget.Warning):
 
     class Error(OWWidget.Error):
         no_numeric_variables = Msg("Data contains no numeric columns.")
-        no_defined_rows = Msg("All rows contain at least one undefined value.")
+        not_enough_data = Msg("SOM needs at least two data rows without missing values.")
 
     def __init__(self):
         super().__init__()
@@ -318,8 +318,8 @@ def prepare_data():
                 self.cont_x = x.tocsr()
             else:
                 mask = np.all(np.isfinite(x), axis=1)
-                if not np.any(mask):
-                    self.Error.no_defined_rows()
+                if np.sum(mask) <= 1:
+                    self.Error.not_enough_data()
                 else:
                     if np.all(mask):
                         self.data = data


Test patch:
diff --git a/Orange/widgets/unsupervised/tests/test_owsom.py b/Orange/widgets/unsupervised/tests/test_owsom.py
--- a/Orange/widgets/unsupervised/tests/test_owsom.py
+++ b/Orange/widgets/unsupervised/tests/test_owsom.py
@@ -86,7 +86,7 @@ def test_missing_all_data(self):
                 self.iris.X[i, i % 4] = np.nan
 
         self.send_signal(widget.Inputs.data, self.iris)
-        self.assertTrue(widget.Error.no_defined_rows.is_shown())
+        self.assertTrue(widget.Error.not_enough_data.is_shown())
         self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())
         self.assertIsNone(widget.data)
         self.assertIsNone(widget.cont_x)
@@ -100,7 +100,7 @@ def test_missing_some_data(self):
             self.iris.X[:50, 0] = np.nan
 
         self.send_signal(widget.Inputs.data, self.iris)
-        self.assertFalse(widget.Error.no_defined_rows.is_shown())
+        self.assertFalse(widget.Error.not_enough_data.is_shown())
         self.assertTrue(widget.Warning.missing_values.is_shown())
         np.testing.assert_almost_equal(
             widget.data.Y.flatten(), [1] * 50 + [2] * 50)
@@ -115,12 +115,32 @@ def test_missing_one_row_data(self):
             self.iris.X[5, 0] = np.nan
 
         self.send_signal(widget.Inputs.data, self.iris)
-        self.assertFalse(widget.Error.no_defined_rows.is_shown())
+        self.assertFalse(widget.Error.not_enough_data.is_shown())
         self.assertTrue(widget.Warning.missing_values.is_shown())
 
         self.send_signal(widget.Inputs.data, None)
         self.assertFalse(widget.Warning.missing_values.is_shown())
 
+    @_patch_recompute_som
+    def test_single_row_data(self):
+        widget = self.widget
+        with self.iris.unlocked():
+            self.iris.X[:-1] = np.nan
+
+        self.send_signal(widget.Inputs.data, self.iris)
+        self.assertTrue(widget.Error.not_enough_data.is_shown())
+
+        self.send_signal(widget.Inputs.data, Table("heart_disease"))
+        self.assertFalse(widget.Error.not_enough_data.is_shown())
+        self.assertTrue(widget.Warning.ignoring_disc_variables.is_shown())
+
+        self.send_signal(widget.Inputs.data, self.iris)
+        self.assertTrue(widget.Error.not_enough_data.is_shown())
+        self.assertFalse(widget.Warning.ignoring_disc_variables.is_shown())
+
+        self.send_signal(widget.Inputs.data, None)
+        self.assertFalse(widget.Error.not_enough_data.is_shown())
+
     @_patch_recompute_som
     def test_sparse_data(self):
         widget = self.widget