Repository: dask/dask-ml

Issue:
Allow lazily evaluated TruncateSVD results
I don't see any reason why the pca-specific parameters like these need to be computed immediately rather than leaving them as dask arrays: https://github.com/dask/dask-ml/blob/b94c587abae3f5667eff131b0616ad8f91966e7f/dask_ml/decomposition/truncated_svd.py#L181-L187

Is that true @TomAugspurger?  I'd like to add a `compute` flag if that's ok.  I'm guessing there is nothing in scikit-learn that would ever rely on those specific attributes.

Discussion:
TomAugspurger: Thanks. Could you add a small sanity check for this? Just something like

```python
svd = TruncatedSVD(comptue=False)
svd.fit(X)
assert isinstance(self.components_, da.Array)
```
eric-czech: > Could you add a small sanity check for this? Just something like

Ah whoops, yes added one at https://github.com/dask/dask-ml/pull/743/commits/78b69fa855e7d0191c3c4905006aafa49109167b#diff-151bb55aba85377c700e75a189fa2256R72-R83.
eric-czech: Hey @TomAugspurger, this is ready if you get a chance.  Between this and https://github.com/dask/dask-ml/pull/742, we'd also be ready to use a new dask-ml release if you find a convenient time to cut one.

Patch:
diff --git a/dask_ml/decomposition/truncated_svd.py b/dask_ml/decomposition/truncated_svd.py
index 4491278d..83cdb43e 100644
--- a/dask_ml/decomposition/truncated_svd.py
+++ b/dask_ml/decomposition/truncated_svd.py
@@ -8,7 +8,13 @@ from ..utils import svd_flip
 
 class TruncatedSVD(BaseEstimator, TransformerMixin):
     def __init__(
-        self, n_components=2, algorithm="tsqr", n_iter=5, random_state=None, tol=0.0
+        self,
+        n_components=2,
+        algorithm="tsqr",
+        n_iter=5,
+        random_state=None,
+        tol=0.0,
+        compute=True,
     ):
         """Dimensionality reduction using truncated SVD (aka LSA).
 
@@ -45,6 +51,10 @@ class TruncatedSVD(BaseEstimator, TransformerMixin):
         tol : float, optional
             Ignored.
 
+        compute : bool
+            Whether or not SVD results should be computed
+            eagerly, by default True.
+
         Attributes
         ----------
         components_ : array, shape (n_components, n_features)
@@ -115,6 +125,7 @@ class TruncatedSVD(BaseEstimator, TransformerMixin):
         self.n_iter = n_iter
         self.random_state = random_state
         self.tol = tol
+        self.compute = compute
 
     def fit(self, X, y=None):
         """Fit truncated SVD on training data X
@@ -162,7 +173,11 @@ class TruncatedSVD(BaseEstimator, TransformerMixin):
         """
         X = self._check_array(X)
         if self.algorithm not in {"tsqr", "randomized"}:
-            raise ValueError()
+            raise ValueError(
+                "`algorithm` must be 'tsqr' or 'randomzied', not '{}'".format(
+                    self.algorithm
+                )
+            )
         if self.algorithm == "tsqr":
             u, s, v = da.linalg.svd(X)
             u = u[:, : self.n_components]
@@ -180,11 +195,14 @@ class TruncatedSVD(BaseEstimator, TransformerMixin):
         full_var = X.var(axis=0).sum()
         explained_variance_ratio = explained_var / full_var
 
-        components, ev, evr, sv = compute(v, explained_var, explained_variance_ratio, s)
-        self.components_ = components
-        self.explained_variance_ = ev
-        self.explained_variance_ratio_ = evr
-        self.singular_values_ = sv
+        if self.compute:
+            v, explained_var, explained_variance_ratio, s = compute(
+                v, explained_var, explained_variance_ratio, s
+            )
+        self.components_ = v
+        self.explained_variance_ = explained_var
+        self.explained_variance_ratio_ = explained_variance_ratio
+        self.singular_values_ = s
         self.n_features_in_ = X.shape[1]
         return X_transformed
 


Test patch:
diff --git a/tests/test_svd.py b/tests/test_svd.py
index 9da10a6c..043e83a3 100644
--- a/tests/test_svd.py
+++ b/tests/test_svd.py
@@ -69,6 +69,18 @@ def test_attributes():
         assert tsvd.components_.shape == (n_components, n_features)
 
 
+@pytest.mark.parametrize("algorithm", ["tsqr", "randomized"])
+@pytest.mark.parametrize("compute", [True, False])
+def test_compute(algorithm, compute):
+    est = dd.TruncatedSVD(random_state=0, algorithm=algorithm, compute=compute)
+    est.fit(dXdense)
+    array_class = np.ndarray if compute else da.Array
+    assert isinstance(est.components_, array_class)
+    assert isinstance(est.explained_variance_, array_class)
+    assert isinstance(est.explained_variance_ratio_, array_class)
+    assert isinstance(est.singular_values_, array_class)
+
+
 def test_too_many_components():
     for n_components in (n_features, n_features + 1):
         tsvd = dd.TruncatedSVD(n_components=n_components)