Repository: scverse/scanpy

Issue:
sc.pl.paga_path not working with TypeError
<!-- Please give a clear and concise description of what the bug is: -->

Hi,
I run scanpy in Python 3.7, matplotlib=3.1.1 - `sc.pl.paga_path` gives me the following error(s).

<!-- Put a minimal reproducible example that reproduces the bug in the code block below: -->
```python
import scanpy as sc
adata = sc.datasets.paul15()
sc.pp.pca(adata)
sc.pp.neighbors(adata)
sc.tl.dpt(adata)
sc.tl.paga(adata, groups='paul15_clusters')
sc.pl.paga_path(adata, nodes=['1Ery'], keys=['Gata2', 'Btg2', 'Btg1'])
```

<!-- Put your Error output in this code block (if applicable, else delete the block): -->
```pytb
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-5-a9471349c389> in <module>
----> 1 sc.pl.paga_path(adata, nodes=['1Ery'], keys=['Gata2', 'Btg2', 'Btg1'])

~/Documents/Python/scanpy/scanpy/plotting/_tools/paga.py in paga_path(adata, nodes, keys, use_raw, annotations, color_map, color_maps_annotations, palette_groups, n_avg, groups_key, xlim, title, left_margin, ytick_fontsize, title_fontsize, show_node_names, show_yticks, show_colorbar, legend_fontsize, legend_fontweight, normalize_to_zero_one, as_heatmap, return_data, show, save, ax)
   1057     if as_heatmap:
   1058         img = ax.imshow(
-> 1059             X, aspect='auto', interpolation='nearest', cmap=color_map
   1060         )
   1061         if show_yticks:

~/anaconda3/lib/python3.7/site-packages/matplotlib/__init__.py in inner(ax, data, *args, **kwargs)
   1599     def inner(ax, *args, data=None, **kwargs):
   1600         if data is None:
-> 1601             return func(ax, *map(sanitize_sequence, args), **kwargs)
   1602 
   1603         bound = new_sig.bind(ax, *args, **kwargs)

~/anaconda3/lib/python3.7/site-packages/matplotlib/cbook/deprecation.py in wrapper(*args, **kwargs)
    367                 f"%(removal)s.  If any parameter follows {name!r}, they "
    368                 f"should be pass as keyword, not positionally.")
--> 369         return func(*args, **kwargs)
    370 
    371     return wrapper

~/anaconda3/lib/python3.7/site-packages/matplotlib/cbook/deprecation.py in wrapper(*args, **kwargs)
    367                 f"%(removal)s.  If any parameter follows {name!r}, they "
    368                 f"should be pass as keyword, not positionally.")
--> 369         return func(*args, **kwargs)
    370 
    371     return wrapper

~/anaconda3/lib/python3.7/site-packages/matplotlib/axes/_axes.py in imshow(self, X, cmap, norm, aspect, interpolation, alpha, vmin, vmax, origin, extent, shape, filternorm, filterrad, imlim, resample, url, **kwargs)
   5669                               resample=resample, **kwargs)
   5670 
-> 5671         im.set_data(X)
   5672         im.set_alpha(alpha)
   5673         if im.get_clip_path() is None:

~/anaconda3/lib/python3.7/site-packages/matplotlib/image.py in set_data(self, A)
    688                 or self._A.ndim == 3 and self._A.shape[-1] in [3, 4]):
    689             raise TypeError("Invalid shape {} for image data"
--> 690                             .format(self._A.shape))
    691 
    692         if self._A.ndim == 3:

TypeError: Invalid shape (3, 43, 1) for image data
```
If I convert the `adata.X` to sparse matrix format, I have the following error:
```python
adata.X = sci.sparse.csr_matrix(adata.X)
sc.pl.paga_path(adata, nodes=['1Ery'], keys=['Gata2', 'Btg2', 'Btg1'])
```

```pytb
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-29-a9471349c389> in <module>
----> 1 sc.pl.paga_path(adata, nodes=['1Ery'], keys=['Gata2', 'Btg2', 'Btg1'])

~/Documents/Python/scanpy/scanpy/plotting/_tools/paga.py in paga_path(adata, nodes, keys, use_raw, annotations, color_map, color_maps_annotations, palette_groups, n_avg, groups_key, xlim, title, left_margin, ytick_fontsize, title_fontsize, show_node_names, show_yticks, show_colorbar, legend_fontsize, legend_fontweight, normalize_to_zero_one, as_heatmap, return_data, show, save, ax)
   1057     if as_heatmap:
   1058         img = ax.imshow(
-> 1059             X, aspect='auto', interpolation='nearest', cmap=color_map
   1060         )
   1061         if show_yticks:

~/anaconda3/lib/python3.7/site-packages/matplotlib/__init__.py in inner(ax, data, *args, **kwargs)
   1599     def inner(ax, *args, data=None, **kwargs):
   1600         if data is None:
-> 1601             return func(ax, *map(sanitize_sequence, args), **kwargs)
   1602 
   1603         bound = new_sig.bind(ax, *args, **kwargs)

~/anaconda3/lib/python3.7/site-packages/matplotlib/cbook/deprecation.py in wrapper(*args, **kwargs)
    367                 f"%(removal)s.  If any parameter follows {name!r}, they "
    368                 f"should be pass as keyword, not positionally.")
--> 369         return func(*args, **kwargs)
    370 
    371     return wrapper

~/anaconda3/lib/python3.7/site-packages/matplotlib/cbook/deprecation.py in wrapper(*args, **kwargs)
    367                 f"%(removal)s.  If any parameter follows {name!r}, they "
    368                 f"should be pass as keyword, not positionally.")
--> 369         return func(*args, **kwargs)
    370 
    371     return wrapper

~/anaconda3/lib/python3.7/site-packages/matplotlib/axes/_axes.py in imshow(self, X, cmap, norm, aspect, interpolation, alpha, vmin, vmax, origin, extent, shape, filternorm, filterrad, imlim, resample, url, **kwargs)
   5669                               resample=resample, **kwargs)
   5670 
-> 5671         im.set_data(X)
   5672         im.set_alpha(alpha)
   5673         if im.get_clip_path() is None:

~/anaconda3/lib/python3.7/site-packages/matplotlib/image.py in set_data(self, A)
    683                 not np.can_cast(self._A.dtype, float, "same_kind")):
    684             raise TypeError("Image data of dtype {} cannot be converted to "
--> 685                             "float".format(self._A.dtype))
    686 
    687         if not (self._A.ndim == 2

TypeError: Image data of dtype object cannot be converted to float
```
Plotting a heatmap with `sc.pl.heatmap` works.

#### Versions:
<!-- Output of scanpy.logging.print_versions() -->
```
scanpy==1.4.5.dev137+ge46f89b anndata==0.6.22.post2.dev73+g00b4b91 umap==0.3.8 numpy==1.17.3 scipy==1.2.1 pandas==0.25.2 scikit-learn==0.20.3 statsmodels==0.10.1 python-igraph==0.7.1 louvain==0.6.1
```
> 



Discussion:
+1, I still have the same issue (the 2nd traceback)- did you find a solution?
I've found a workaround, when I downgrade to `anndata=0.6.22.post1` (still with `scanpy==1.4.5.post2`), it generates an output with `paga_path` but also this warning:

```
FutureWarning: In anndata v0.7+, arrays contained within an AnnData object will maintain their dimensionality. For example, prior to v0.7 `adata[0, 0].X` returned a scalar and `adata[0, :]` returned a 1d array, post v0.7 they will return two dimensional arrays. If you would like to get a one dimensional array from your AnnData object, consider using the `adata.obs_vector`, `adata.var_vector` methods or accessing the array directly.
```
+1 issue found with scanpy <Version('1.4.5.post3')>

Resolving patch:
diff --git a/scanpy/plotting/_tools/paga.py b/scanpy/plotting/_tools/paga.py
--- a/scanpy/plotting/_tools/paga.py
+++ b/scanpy/plotting/_tools/paga.py
@@ -13,6 +13,7 @@
 from matplotlib import patheffects
 from matplotlib.axes import Axes
 from matplotlib.colors import is_color_like, Colormap
+from scipy.sparse import issparse
 
 from .. import _utils
 from .._utils import matrix, _IGraphLayout, _FontWeight, _FontSize
@@ -1097,12 +1098,12 @@ def moving_average(a):
                 ]
             )
             idcs = idcs[idcs_group]
-            if key in adata.obs_keys():
-                x += list(adata.obs[key].values[idcs])
-            else:
-                x += list(adata_X[:, key].X[idcs])
+            values = (
+                adata.obs[key].values if key in adata.obs_keys() else adata_X[:, key].X
+            )[idcs]
+            x += (values.A if issparse(values) else values).tolist()
             if ikey == 0:
-                groups += [group for i in range(len(idcs))]
+                groups += [group] * len(idcs)
                 x_tick_locs.append(len(x))
                 for anno in annotations:
                     series = adata.obs[anno]
@@ -1128,7 +1129,7 @@ def moving_average(a):
                 else:
                     label = group
                 x_tick_labels.append(label)
-    X = np.array(X)
+    X = np.asarray(X).squeeze()
     if as_heatmap:
         img = ax.imshow(X, aspect='auto', interpolation='nearest', cmap=color_map)
         if show_yticks:


Test patch:
diff --git a/scanpy/tests/_images/master_paga_path.png b/scanpy/tests/_images/master_paga_path.png
new file mode 100644
Binary files /dev/null and b/scanpy/tests/_images/master_paga_path.png differ
diff --git a/scanpy/tests/test_plotting.py b/scanpy/tests/test_plotting.py
--- a/scanpy/tests/test_plotting.py
+++ b/scanpy/tests/test_plotting.py
@@ -999,6 +999,23 @@ def test_paga(image_comparer):
     save_and_compare_images('master_paga_pie')
 
 
+def test_paga_path(image_comparer):
+    save_and_compare_images = image_comparer(ROOT, FIGS, tol=15)
+
+    pbmc = sc.datasets.pbmc68k_reduced()
+    sc.tl.paga(pbmc, groups='bulk_labels')
+
+    pbmc.uns['iroot'] = 0
+    sc.tl.dpt(pbmc)
+    sc.pl.paga_path(
+        pbmc,
+        nodes=['Dendritic'],
+        keys=['HES4', 'SRM', 'CSTB'],
+        show=False,
+    )
+    save_and_compare_images('master_paga_path')
+
+
 def test_no_copy():
     # https://github.com/theislab/scanpy/issues/1000
     # Tests that plotting functions don't make a copy from a view unless they