Repository: biolab/orange3

Issue:
Models Can't Handle Sparse Class Variables
<!--
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.0.dev0+8f814e4

##### Expected behavior
Models could handle data where Y is in sparse format.


##### Actual behavior
Models report incorrect error message `Data contains a single target value` despite the fact that target variable has multiple values.


##### Steps to reproduce the behavior
Create bag of words data and set one BoW feature as the class. Pass to some model.
![screen shot 2017-07-26 at 14 54 06](https://user-images.githubusercontent.com/713026/28621866-a99094b4-7212-11e7-8174-acd90b897ae3.png)


##### Proposed solution
1. To remove the incorrect message, use `unique` from `Orange.statistics.util` instead of the one from `numpy` (`owlearnerwidget.py`, L220).
2. Either make models handle sparse data or transform sparse data to dense.


##### Additional info (worksheets, data, screenshots, ...)




Discussion:

Resolving patch:
diff --git a/Orange/data/table.py b/Orange/data/table.py
--- a/Orange/data/table.py
+++ b/Orange/data/table.py
@@ -204,6 +204,8 @@ def Y(self, value):
             value = value[:, None]
         if sp.issparse(value) and len(self) != value.shape[0]:
             value = value.T
+        if sp.issparse(value):
+            value = value.toarray()
         self._Y = value
 
     def __new__(cls, *args, **kwargs):
diff --git a/Orange/widgets/utils/owlearnerwidget.py b/Orange/widgets/utils/owlearnerwidget.py
--- a/Orange/widgets/utils/owlearnerwidget.py
+++ b/Orange/widgets/utils/owlearnerwidget.py
@@ -1,12 +1,11 @@
 from copy import deepcopy
 
-import numpy as np
-
 from AnyQt.QtCore import QTimer, Qt
 
 from Orange.data import Table
 from Orange.modelling import Fitter, Learner, Model
 from Orange.preprocess.preprocess import Preprocess
+from Orange.statistics import util as ut
 from Orange.widgets import gui
 from Orange.widgets.settings import Setting
 from Orange.widgets.utils import getmembers
@@ -133,6 +132,7 @@ def set_data(self, data):
         """Set the input train dataset."""
         self.Error.data_error.clear()
         self.data = data
+
         if data is not None and data.domain.class_var is None:
             self.Error.data_error("Data has no target variable.")
             self.data = None
@@ -181,7 +181,7 @@ def check_data(self):
                 self.Error.data_error(self.learner.learner_adequacy_err_msg)
             elif not len(self.data):
                 self.Error.data_error("Dataset is empty.")
-            elif len(np.unique(self.data.Y)) < 2:
+            elif len(ut.unique(self.data.Y)) < 2:
                 self.Error.data_error("Data contains a single target value.")
             elif self.data.X.size == 0:
                 self.Error.data_error("Data has no features to learn from.")


Test patch:
diff --git a/Orange/tests/test_sparse_table.py b/Orange/tests/test_sparse_table.py
--- a/Orange/tests/test_sparse_table.py
+++ b/Orange/tests/test_sparse_table.py
@@ -67,7 +67,7 @@ def test_Y_setter_1d(self):
         assert iris.Y.shape == (150,)
         iris.Y = csr_matrix(iris.Y)
         # We expect the Y shape to match the X shape, which is (150, 4) in iris
-        self.assertEqual(iris.Y.shape, (150, 1))
+        self.assertEqual(iris.Y.shape, (150,))
 
     def test_Y_setter_2d(self):
         iris = Table('iris')
diff --git a/Orange/tests/test_table.py b/Orange/tests/test_table.py
--- a/Orange/tests/test_table.py
+++ b/Orange/tests/test_table.py
@@ -2810,7 +2810,7 @@ def test_sparse_dense_transformation(self):
 
         iris_sparse = iris.to_sparse(sparse_attributes=True, sparse_class=True)
         self.assertTrue(sp.issparse(iris_sparse.X))
-        self.assertTrue(sp.issparse(iris_sparse.Y))
+        self.assertFalse(sp.issparse(iris_sparse.Y))
         self.assertFalse(sp.issparse(iris_sparse.metas))
 
         dense_iris = iris_sparse.to_dense()
diff --git a/Orange/widgets/utils/tests/test_owlearnerwidget.py b/Orange/widgets/utils/tests/test_owlearnerwidget.py
--- a/Orange/widgets/utils/tests/test_owlearnerwidget.py
+++ b/Orange/widgets/utils/tests/test_owlearnerwidget.py
@@ -1,9 +1,12 @@
+import scipy.sparse as sp
+
 # pylint: disable=missing-docstring
 from Orange.base import Learner, Model
 from Orange.classification import KNNLearner
 from Orange.data import Table, Domain
 from Orange.modelling import TreeLearner
-from Orange.regression import MeanLearner
+from Orange.preprocess import continuize
+from Orange.regression import MeanLearner, LinearRegressionLearner
 from Orange.widgets.utils.owlearnerwidget import OWBaseLearner
 from Orange.widgets.tests.base import WidgetTest
 from Orange.widgets.utils.signals import Output
@@ -108,3 +111,24 @@ class WidgetA(OWBaseLearner):
         settings = w1.settingsHandler.pack_data(w1)
         w2 = self.create_widget(WidgetA, settings)
         self.assertEqual(w2.learner_name, w1.learner_name)
+
+    def test_converts_sparse_targets_to_dense(self):
+        class WidgetLR(OWBaseLearner):
+            name = "lr"
+            LEARNER = LinearRegressionLearner
+
+        w = self.create_widget(WidgetLR)
+
+        # Orange will want do do one-hot encoding when continuizing discrete variable
+        pp = continuize.DomainContinuizer(
+            multinomial_treatment=continuize.Continuize.AsOrdinal,
+            transform_class=True,
+        )
+        data = self.iris.transform(pp(self.iris))
+        data.Y = sp.csr_matrix(data.Y)
+
+        self.send_signal(w.Inputs.data, data, widget=w)
+        self.assertFalse(any(w.Error.active))
+
+        model = self.get_output(w.Outputs.model, widget=w)
+        self.assertIsNotNone(model)