From a9bc428f98a3532a72397e726375fe88fef4bc3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 10:21:26 +0000 Subject: [PATCH 1/4] Initial plan From ddec9982dedeae1b51f5896593b4386fad588794 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 10:24:39 +0000 Subject: [PATCH 2/4] Support cell metadata inputs Co-authored-by: stemangiola <7232890+stemangiola@users.noreply.github.com> --- README.md | 14 +++++++++++++ sccompPy/methods.py | 15 ++++++++++++++ tests/test_metadata_input.py | 38 ++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 tests/test_metadata_input.py diff --git a/README.md b/README.md index e7f7f4d..3bd3e2a 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,20 @@ estimate_res = sccompPy.sccomp_estimate( count = 'count', verbose = False ) +``` + +Cell-level metadata can also be supplied directly. When the selected `count` +column is absent, `sccompPy` derives counts by grouping rows by `sample` and +`cell_group`, while retaining the other metadata columns for use in formulas: + +```python +estimate_res = sccompPy.sccomp_estimate( + data = cell_metadata, + formula_composition = '~ 0 + type', + sample = 'sample', + cell_group = 'cell_group', + verbose = False +) ``` 17:05:31 - cmdstanpy - INFO - CmdStan start processing diff --git a/sccompPy/methods.py b/sccompPy/methods.py index 14a28d3..976864c 100644 --- a/sccompPy/methods.py +++ b/sccompPy/methods.py @@ -3,6 +3,14 @@ from .utilities import * from .functions_multi_beta_binomia import * +def _aggregate_cell_metadata(data, sample, cell_group, count): + """Aggregate one row per cell into the count table expected by sccomp.""" + groups = data.groupby([sample, cell_group], sort=False, observed=True) + aggregated = groups.first().reset_index() + aggregated[count] = groups.size().to_numpy() + return aggregated + + def sccomp_estimate( data, formula_composition="~1", @@ -37,6 +45,13 @@ def sccomp_estimate( variational_inference=None, **kwargs ): + sample = sample or "sample" + cell_group = cell_group or "cell_group" + count = count or count_col or "count" + + if count not in data.columns: + data = _aggregate_cell_metadata(data, sample, cell_group, count) + res = sccomp_glm_data_frame_counts( data, formula_composition=formula_composition, diff --git a/tests/test_metadata_input.py b/tests/test_metadata_input.py new file mode 100644 index 0000000..8869737 --- /dev/null +++ b/tests/test_metadata_input.py @@ -0,0 +1,38 @@ +import pandas as pd +from pandas.testing import assert_frame_equal + +from sccompPy import methods + + +def test_sccomp_estimate_aggregates_cell_metadata(monkeypatch): + cell_metadata = pd.DataFrame( + { + "sample": ["s1", "s1", "s1", "s2"], + "cell_group": ["A", "A", "B", "A"], + "type": ["control", "control", "control", "treated"], + } + ) + captured = {} + + def fake_sccomp_glm_data_frame_counts(data, **kwargs): + captured["data"] = data + return {} + + monkeypatch.setattr(methods, "sccomp_glm_data_frame_counts", fake_sccomp_glm_data_frame_counts) + + methods.sccomp_estimate( + cell_metadata, + sample="sample", + cell_group="cell_group", + verbose=False, + ) + + expected = pd.DataFrame( + { + "sample": ["s1", "s1", "s2"], + "cell_group": ["A", "B", "A"], + "type": ["control", "control", "treated"], + "count": [2, 1, 1], + } + ) + assert_frame_equal(captured["data"], expected) From 6d0140abf6ca70870524ca57431236c367507016 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 10:25:33 +0000 Subject: [PATCH 3/4] Avoid ambiguous metadata aggregation Co-authored-by: stemangiola <7232890+stemangiola@users.noreply.github.com> --- README.md | 2 +- sccompPy/methods.py | 15 +++++++++++++-- tests/test_metadata_input.py | 3 ++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3bd3e2a..22bd346 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ estimate_res = sccompPy.sccomp_estimate( Cell-level metadata can also be supplied directly. When the selected `count` column is absent, `sccompPy` derives counts by grouping rows by `sample` and -`cell_group`, while retaining the other metadata columns for use in formulas: +`cell_group`, while retaining sample-level metadata columns for use in formulas: ```python estimate_res = sccompPy.sccomp_estimate( diff --git a/sccompPy/methods.py b/sccompPy/methods.py index 976864c..1f057d6 100644 --- a/sccompPy/methods.py +++ b/sccompPy/methods.py @@ -6,8 +6,19 @@ def _aggregate_cell_metadata(data, sample, cell_group, count): """Aggregate one row per cell into the count table expected by sccomp.""" groups = data.groupby([sample, cell_group], sort=False, observed=True) - aggregated = groups.first().reset_index() - aggregated[count] = groups.size().to_numpy() + aggregated = groups.size().rename(count).reset_index() + + metadata_columns = [] + for column in data.columns: + if column in (sample, cell_group, count): + continue + if data.groupby(sample, observed=True)[column].nunique(dropna=False).le(1).all(): + metadata_columns.append(column) + + if metadata_columns: + sample_metadata = data[[sample] + metadata_columns].drop_duplicates(sample) + aggregated = aggregated.merge(sample_metadata, on=sample, how="left", sort=False) + return aggregated diff --git a/tests/test_metadata_input.py b/tests/test_metadata_input.py index 8869737..35cd25a 100644 --- a/tests/test_metadata_input.py +++ b/tests/test_metadata_input.py @@ -10,6 +10,7 @@ def test_sccomp_estimate_aggregates_cell_metadata(monkeypatch): "sample": ["s1", "s1", "s1", "s2"], "cell_group": ["A", "A", "B", "A"], "type": ["control", "control", "control", "treated"], + "cell_id": ["c1", "c2", "c3", "c4"], } ) captured = {} @@ -31,8 +32,8 @@ def fake_sccomp_glm_data_frame_counts(data, **kwargs): { "sample": ["s1", "s1", "s2"], "cell_group": ["A", "B", "A"], - "type": ["control", "control", "treated"], "count": [2, 1, 1], + "type": ["control", "control", "treated"], } ) assert_frame_equal(captured["data"], expected) From a72e79aa2428f545249c04d5b73fe279d54ba9da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Aug 2026 10:26:17 +0000 Subject: [PATCH 4/4] Clarify count column handling Co-authored-by: stemangiola <7232890+stemangiola@users.noreply.github.com> --- sccompPy/methods.py | 4 +++- tests/test_metadata_input.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sccompPy/methods.py b/sccompPy/methods.py index 1f057d6..a0d2643 100644 --- a/sccompPy/methods.py +++ b/sccompPy/methods.py @@ -58,7 +58,9 @@ def sccomp_estimate( ): sample = sample or "sample" cell_group = cell_group or "cell_group" - count = count or count_col or "count" + if count is None: + count = count_col + count = count or "count" if count not in data.columns: data = _aggregate_cell_metadata(data, sample, cell_group, count) diff --git a/tests/test_metadata_input.py b/tests/test_metadata_input.py index 35cd25a..6e44c80 100644 --- a/tests/test_metadata_input.py +++ b/tests/test_metadata_input.py @@ -10,6 +10,7 @@ def test_sccomp_estimate_aggregates_cell_metadata(monkeypatch): "sample": ["s1", "s1", "s1", "s2"], "cell_group": ["A", "A", "B", "A"], "type": ["control", "control", "control", "treated"], + "batch": [1, 1, 1, 2], "cell_id": ["c1", "c2", "c3", "c4"], } ) @@ -34,6 +35,8 @@ def fake_sccomp_glm_data_frame_counts(data, **kwargs): "cell_group": ["A", "B", "A"], "count": [2, 1, 1], "type": ["control", "control", "treated"], + "batch": [1, 1, 2], } ) + assert captured["data"].columns.tolist() == expected.columns.tolist() assert_frame_equal(captured["data"], expected)