Repository: biolab/orange3

Issue:
PCA should work on sparse
<!--
This is an issue template. Please fill in the relevant details in the
sections below.
-->

##### Orange version
<!-- From menu _Help→About→Version_ or code `Orange.version.full_version` -->
3.5.dev

##### Expected behavior
PCA works on sparse.


##### Actual behavior
PCA fails on sparse.


##### Steps to reproduce the behavior
Corpus - Bag of Words - PCA.


##### Additional info (worksheets, data, screenshots, ...)





Discussion:
PCA probably cannot work on sparse data efficiently since it requires the data to be centered before computing the covariance matrix.

Maybe we should introduce the SVD widget, since it doesn't center the data and can hence efficiently work on sparse data. 

Resolving patch:
diff --git a/Orange/widgets/unsupervised/owpca.py b/Orange/widgets/unsupervised/owpca.py
--- a/Orange/widgets/unsupervised/owpca.py
+++ b/Orange/widgets/unsupervised/owpca.py
@@ -55,6 +55,7 @@ class Warning(widget.OWWidget.Warning):
     class Error(widget.OWWidget.Error):
         no_features = widget.Msg("At least 1 feature is required")
         no_instances = widget.Msg("At least 1 data instance is required")
+        sparse_data = widget.Msg("Sparse data is not supported")
 
     def __init__(self):
         super().__init__()
@@ -174,7 +175,11 @@ def start(self):
             self.start_button.setText("Abort remote computation")
 
     def set_data(self, data):
+        self.clear_messages()
+        self.clear()
+        self.start_button.setEnabled(False)
         self.information()
+        self.data = None
         if isinstance(data, SqlTable):
             if data.approx_len() < AUTO_DL_LIMIT:
                 data = Table(data)
@@ -183,36 +188,35 @@ def set_data(self, data):
                 data_sample = data.sample_time(1, no_cache=True)
                 data_sample.download_data(2000, partial=True)
                 data = Table(data_sample)
-        self.data = data
-        self.fit()
-
-    def fit(self):
-        self.clear_messages()
-        self.clear()
-        self.start_button.setEnabled(False)
-        if self.data is None:
-            return
-        data = self.data
-        self._transformed = None
-        if isinstance(data, SqlTable): # data was big and remote available
-            self.sampling_box.setVisible(True)
-            self.start_button.setText("Start remote computation")
-            self.start_button.setEnabled(True)
-        else:
+            else:       # data was big and remote available
+                self.sampling_box.setVisible(True)
+                self.start_button.setText("Start remote computation")
+                self.start_button.setEnabled(True)
+        if not isinstance(data, SqlTable):
             self.sampling_box.setVisible(False)
+        if isinstance(data, Table):
+            if data.is_sparse():
+                self.Error.sparse_data()
+                self.clear_outputs()
+                return
             if len(data.domain.attributes) == 0:
                 self.Error.no_features()
-                self.send("Transformed data", None)
-                self.send("Components", None)
-                self.send("PCA", None)
+                self.clear_outputs()
                 return
             if len(data) == 0:
                 self.Error.no_instances()
-                self.send("Transformed data", None)
-                self.send("Components", None)
-                self.send("PCA", None)
+                self.clear_outputs()
                 return
+        self.data = data
+        self.fit()
 
+    def fit(self):
+        self.clear()
+        self.Warning.trivial_components.clear()
+        if self.data is None:
+            return
+        data = self.data
+        if not isinstance(data, SqlTable):
             pca = self._pca_projector(data)
             variance_ratio = pca.explained_variance_ratio_
             cumulative = numpy.cumsum(variance_ratio)
@@ -237,7 +241,11 @@ def clear(self):
         self.plot_horlabels = []
         self.plot_horlines = []
         self.plot.clear()
-        self.Warning.trivial_components.clear()
+
+    def clear_outputs(self):
+        self.send("Transformed data", None)
+        self.send("Components", None)
+        self.send("PCA", self._pca_projector)
 
     def get_model(self):
         if self.rpca is None:


Test patch:
diff --git a/Orange/widgets/unsupervised/tests/test_owpca.py b/Orange/widgets/unsupervised/tests/test_owpca.py
--- a/Orange/widgets/unsupervised/tests/test_owpca.py
+++ b/Orange/widgets/unsupervised/tests/test_owpca.py
@@ -1,6 +1,7 @@
 # Test methods with long descriptive names can omit docstrings
 # pylint: disable=missing-docstring
 import numpy as np
+import scipy.sparse as sp
 
 from Orange.data import Table, Domain
 from Orange.widgets.tests.base import WidgetTest
@@ -69,3 +70,9 @@ def test_variance_shown(self):
         self.widget._update_selection_component_spin()
         var3 = self.widget.variance_covered
         self.assertGreater(var3, var2)
+
+    def test_error_on_sparse_data(self):
+        data = Table('iris')
+        data.X = sp.csr_matrix(data.X)
+        self.widget.set_data(data)
+        self.assertTrue(self.widget.Error.sparse_data.is_shown())