Repository: scverse/scanpy

Issue:
Inconsistent array types from sc.get.aggregate
### Please make sure these conditions are met

- [X] I have checked that this issue has not already been reported.
- [X] I have confirmed this bug exists on the latest version of scanpy.
- [X] (optional) I have confirmed this bug exists on the main branch of scanpy.

### What happened?

cc: @Intron7 

The array types returned for the various aggregations in `sc.get.aggregate` are different (see example)

This can lead to somewhat confusing behavior downstream, especially while we are using the sparse matrix classes.

I would suggest we default to a dense result and consider adding an argument `array_type` that determines the type of the arrays added to `layers`.

### Minimal code sample

```python
import scanpy as sc

adata = sc.datasets.pbmc3k_processed().raw.to_adata()

aggregated = sc.get.aggregate(adata, "louvain", ["sum", "count_nonzero"])
type(aggregated.layers["sum"])
# numpy.ndarray

type(aggregated.layers["count_nonzero"])
# scipy.sparse._csr.csr_matrix
```


### Error output

_No response_

### Versions

<details>

```
-----
anndata     0.10.5.post1
scanpy      1.10.0.dev315+gf6d5ac94
-----
IPython             8.20.0
PIL                 10.2.0
asciitree           NA
asttokens           NA
cloudpickle         3.0.0
cycler              0.12.1
cython_runtime      NA
dask                2024.1.1
dateutil            2.8.2
decorator           5.1.1
executing           2.0.1
fasteners           0.19
h5py                3.10.0
igraph              0.11.3
jedi                0.19.1
jinja2              3.1.3
joblib              1.3.2
kiwisolver          1.4.5
legacy_api_wrap     NA
leidenalg           0.10.2
llvmlite            0.41.1
markupsafe          2.1.4
matplotlib          3.8.2
mpl_toolkits        NA
msgpack             1.0.7
natsort             8.4.0
numba               0.58.1
numcodecs           0.12.1
numpy               1.26.3
packaging           23.2
pandas              2.2.0
parso               0.8.3
pexpect             4.9.0
prompt_toolkit      3.0.43
psutil              5.9.8
ptyprocess          0.7.0
pure_eval           0.2.2
pygments            2.17.2
pyparsing           3.1.1
pytz                2023.4
scipy               1.12.0
session_info        1.0.0
six                 1.16.0
sklearn             1.4.0
sparse              0.15.1
stack_data          0.6.3
tblib               3.0.0
texttable           1.7.0
threadpoolctl       3.2.0
tlz                 0.12.1
toolz               0.12.1
traitlets           5.14.1
wcwidth             0.2.13
yaml                6.0.1
zarr                2.16.1
zipp                NA
-----
Python 3.11.7 | packaged by conda-forge | (main, Dec 23 2023, 14:43:09) [GCC 12.3.0]
Linux-5.15.0-87-generic-x86_64-with-glibc2.35
-----
Session information updated at 2024-03-04 13:41
```

</details>



Discussion:

Resolving patch:
diff --git a/scanpy/get/_aggregated.py b/scanpy/get/_aggregated.py
--- a/scanpy/get/_aggregated.py
+++ b/scanpy/get/_aggregated.py
@@ -71,7 +71,7 @@ def count_nonzero(self) -> NDArray[np.integer]:
         """
         # pattern = self.data._with_data(np.broadcast_to(1, len(self.data.data)))
         # return self.indicator_matrix @ pattern
-        return self.indicator_matrix @ (self.data != 0)
+        return utils.asarray(self.indicator_matrix @ (self.data != 0))
 
     def sum(self) -> Array:
         """\


Test patch:
diff --git a/scanpy/tests/test_aggregated.py b/scanpy/tests/test_aggregated.py
--- a/scanpy/tests/test_aggregated.py
+++ b/scanpy/tests/test_aggregated.py
@@ -378,3 +378,15 @@ def test_combine_categories(label_cols, cols, expected):
         [x.split("_") for x in result], columns=cols, index=result.astype(str)
     ).astype("category")
     pd.testing.assert_frame_equal(reconstructed_df, result_label_df)
+
+
+@pytest.mark.parametrize("array_type", ARRAY_TYPES_MEM)
+@pytest.mark.parametrize("metric", ["sum", "mean", "var", "count_nonzero"])
+def test_aggregate_arraytype(array_type, metric):
+    adata = pbmc3k_processed().raw.to_adata()
+    adata = adata[
+        adata.obs["louvain"].isin(adata.obs["louvain"].cat.categories[:5]), :1_000
+    ].copy()
+    adata.X = array_type(adata.X)
+    aggregate = sc.get.aggregate(adata, ["louvain"], metric)
+    assert isinstance(aggregate.layers[metric], np.ndarray)