Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cirrocumulus/anndata_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ def layer_names(dataset):
mapping directly yields a layer that is not one.
"""
return [key for key in dataset.layers.keys() if key is not None]


def is_encoded_element(node):
"""Test whether a node of a backed store (h5py or zarr) carries an anndata encoding.

Expand Down Expand Up @@ -393,6 +395,8 @@ def dataset_schema(dataset, n_features=10):
if isinstance(dataset, anndata.AnnData):
shape = dataset.shape
else: # X is either a dense array or a group holding a sparse matrix
shape = dataset.X.attrs["shape"] if "shape" in dataset.X.attrs else dataset.X.shape
shape = (
dataset.X.attrs["shape"] if "shape" in dataset.X.attrs else dataset.X.shape
)
schema_dict["shape"] = shape
return schema_dict
10 changes: 6 additions & 4 deletions cirrocumulus/dotplot_aggregator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ def _to_frame(self, adata):
for d in self.dimensions:
names.extend(d if isinstance(d, list) else [d])

columns = {name: adata.obs[name] for name in dict.fromkeys(names) if name in adata.obs}
columns = {
name: adata.obs[name] for name in dict.fromkeys(names) if name in adata.obs
}

var_index = list(adata.var.index)
for measure in self.var_measures:
Expand Down Expand Up @@ -65,9 +67,9 @@ def percent_expressed(x):
continue
# aggregate only the measures: with several dimensions the frame also holds
# the individual categorical columns, which have no mean
agg_result = df.groupby(dimension_name, observed=True)[list(var_measures)].agg(
[mean, percent_expressed]
)
agg_result = df.groupby(dimension_name, observed=True)[
list(var_measures)
].agg([mean, percent_expressed])

values = []
dotplot_result = {
Expand Down
6 changes: 5 additions & 1 deletion cirrocumulus/prepare_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
from pandas import CategoricalDtype

from cirrocumulus.anndata_dataset import read_adata
from cirrocumulus.anndata_util import dataset_schema, get_scanpy_marker_keys, layer_names
from cirrocumulus.anndata_util import (
dataset_schema,
get_scanpy_marker_keys,
layer_names,
)
from cirrocumulus.io_util import SPATIAL_HELP, filter_markers, get_markers, unique_id
from cirrocumulus.util import get_fs, open_file, to_json

Expand Down
1 change: 0 additions & 1 deletion scripts/check_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import sys
import zipfile


INDEX = "cirrocumulus/client/index.html"
BUNDLE = "cirrocumulus/client/static/js/*.js"

Expand Down
4 changes: 3 additions & 1 deletion tests/test_de.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ def test_de_4_groups(sparse):
adata2 = get_example_data(sparse)
# rename_categories, not replace: pandas 3 refuses to set a value outside the
# existing categories on a categorical Series
adata2.obs["sc_groups"] = adata2.obs["sc_groups"].cat.rename_categories({0: 2, 1: 3})
adata2.obs["sc_groups"] = adata2.obs["sc_groups"].cat.rename_categories(
{0: 2, 1: 3}
)
adata = anndata.concat((adata1, adata2))
adata.obs_names_make_unique()
batch_size = 3
Expand Down
33 changes: 25 additions & 8 deletions tests/test_dotplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ def dotplot_data(request, test_data):

def _expected(adata, measure, by):
values = adata[:, measure].X
values = np.asarray(values.todense() if scipy.sparse.issparse(values) else values).ravel()
grouped = pd.Series(values, index=adata.obs.index).groupby(adata.obs[by].values, observed=True)
values = np.asarray(
values.todense() if scipy.sparse.issparse(values) else values
).ravel()
grouped = pd.Series(values, index=adata.obs.index).groupby(
adata.obs[by].values, observed=True
)
return grouped.mean(), grouped.apply(lambda x: 100 * (x.values != 0).sum() / len(x))


def test_dotplot_accepts_anndata(dotplot_data, measures, by):
results = DotPlotAggregator(var_measures=measures, dimensions=[by]).execute(dotplot_data)
results = DotPlotAggregator(var_measures=measures, dimensions=[by]).execute(
dotplot_data
)
assert len(results) == 1
result = results[0]
assert result["name"] == by
Expand All @@ -37,21 +43,32 @@ def test_dotplot_accepts_anndata(dotplot_data, measures, by):


def test_dotplot_values_match_manual_groupby(dotplot_data, measures, by):
result = DotPlotAggregator(var_measures=measures, dimensions=[by]).execute(dotplot_data)[0]
result = DotPlotAggregator(var_measures=measures, dimensions=[by]).execute(
dotplot_data
)[0]
categories = list(result["categories"])
for value in result["values"]:
expected_mean, expected_pct = _expected(dotplot_data, value["name"], by)
np.testing.assert_allclose(
np.asarray(value["mean"]), expected_mean.loc[categories].values, rtol=1e-4, atol=1e-5
np.asarray(value["mean"]),
expected_mean.loc[categories].values,
rtol=1e-4,
atol=1e-5,
)
np.testing.assert_allclose(
np.asarray(value["percentExpressed"]), expected_pct.loc[categories].values, rtol=1e-4
np.asarray(value["percentExpressed"]),
expected_pct.loc[categories].values,
rtol=1e-4,
)


def test_dotplot_multiple_dimensions(dotplot_data, measures, by):
adata = dotplot_data
adata.obs["half"] = pd.Categorical(np.where(np.arange(adata.shape[0]) % 2 == 0, "a", "b"))
results = DotPlotAggregator(var_measures=measures, dimensions=[[by, "half"]]).execute(adata)
adata.obs["half"] = pd.Categorical(
np.where(np.arange(adata.shape[0]) % 2 == 0, "a", "b")
)
results = DotPlotAggregator(
var_measures=measures, dimensions=[[by, "half"]]
).execute(adata)
assert len(results) == 1
assert results[0]["name"] == "{}-half".format(by)
27 changes: 22 additions & 5 deletions tests/test_prepare_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,24 @@ def test_prepare(


@pytest.mark.parametrize("zarr_format", [2, 3])
def test_prepare_zarr_format(test_data, measures, dimensions, continuous_obs, basis, tmp_path, zarr_format):
def test_prepare_zarr_format(
test_data, measures, dimensions, continuous_obs, basis, tmp_path, zarr_format
):
# anndata >= 0.13 writes zarr v3 by default, so v3 is the shape most stores have on disk
anndata.settings.zarr_write_format = zarr_format
output_dir = str(tmp_path / "test.zarr")
test_data = test_data[:, measures]
test_data.obs = test_data.obs[dimensions + continuous_obs]
PrepareData(datasets=[test_data], output=output_dir, output_format="zarr").execute()
read_and_diff(ZarrDataset(), output_dir, test_data, measures, dimensions, continuous_obs, basis)
read_and_diff(
ZarrDataset(),
output_dir,
test_data,
measures,
dimensions,
continuous_obs,
basis,
)


def test_prepare_jsonl(
Expand All @@ -158,7 +168,9 @@ def test_layer_names_skips_the_x_alias(test_data):
assert layer_names(adata) == ["counts"]


def test_read_anndata_zarr(test_data, measures, dimensions, continuous_obs, basis, tmp_path):
def test_read_anndata_zarr(
test_data, measures, dimensions, continuous_obs, basis, tmp_path
):
# a zarr store written by anndata itself, not by PrepareData: anndata >= 0.8 encodes a
# categorical obs column as a group (codes + categories), not as an array
path = str(tmp_path / "test.zarr")
Expand All @@ -177,7 +189,9 @@ def test_read_anndata_zarr(test_data, measures, dimensions, continuous_obs, basi
read_and_diff(reader, path, test_data, measures, dimensions, continuous_obs, basis)


def test_read_anndata_zarr_group_encoded_elements(test_data, measures, dimensions, tmp_path):
def test_read_anndata_zarr_group_encoded_elements(
test_data, measures, dimensions, tmp_path
):
"""Elements anndata writes as groups rather than arrays must still be readable.

A nullable string index and a DataFrame-valued ``obsm`` both used to raise, because the
Expand All @@ -200,7 +214,10 @@ def test_read_anndata_zarr_group_encoded_elements(test_data, measures, dimension
assert list(schema["shape"]) == list(test_data.shape)

adata = reader.read_dataset(
filesystem=fs, path=path, dataset=dict(id=""), keys=dict(X=measures, obs=dimensions)
filesystem=fs,
path=path,
dataset=dict(id=""),
keys=dict(X=measures, obs=dimensions),
)
assert list(adata.var.index) == list(test_data.var.index)
assert list(adata.obs.columns) == dimensions
21 changes: 16 additions & 5 deletions tests/test_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,24 @@ def test_X_stats_matches_dense(stats_data):
# sparse and dense reductions accumulate in a different order, so float32 sums of
# near-zero scaled values differ in the last bits -- compare with an absolute floor
scale = float(np.abs(dense).max())
np.testing.assert_allclose(df["min"].values, dense.min(axis=0), rtol=1e-5, atol=1e-5 * scale)
np.testing.assert_allclose(df["max"].values, dense.max(axis=0), rtol=1e-5, atol=1e-5 * scale)
np.testing.assert_allclose(
df["sum"].values, dense.sum(axis=0), rtol=1e-4, atol=1e-4 * scale * dense.shape[0] ** 0.5
df["min"].values, dense.min(axis=0), rtol=1e-5, atol=1e-5 * scale
)
np.testing.assert_allclose(
df["max"].values, dense.max(axis=0), rtol=1e-5, atol=1e-5 * scale
)
np.testing.assert_allclose(
df["sum"].values,
dense.sum(axis=0),
rtol=1e-4,
atol=1e-4 * scale * dense.shape[0] ** 0.5,
)
np.testing.assert_allclose(
df["mean"].values, dense.mean(axis=0), rtol=1e-4, atol=1e-5 * scale
)
np.testing.assert_array_equal(
df["numExpressed"].values, np.count_nonzero(dense, axis=0)
)
np.testing.assert_allclose(df["mean"].values, dense.mean(axis=0), rtol=1e-4, atol=1e-5 * scale)
np.testing.assert_array_equal(df["numExpressed"].values, np.count_nonzero(dense, axis=0))


def test_feature_aggregator_summary(stats_data, measures, dimensions):
Expand Down
Loading