cellstate-c2s 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cellstate_c2s/__init__.py +7 -0
- cellstate_c2s/builder.py +154 -0
- cellstate_c2s/c2s_adapter.py +43 -0
- cellstate_c2s-0.1.0.dist-info/METADATA +125 -0
- cellstate_c2s-0.1.0.dist-info/RECORD +8 -0
- cellstate_c2s-0.1.0.dist-info/WHEEL +5 -0
- cellstate_c2s-0.1.0.dist-info/licenses/LICENSE +21 -0
- cellstate_c2s-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Deviation-from-reference gene ordering for Cell2Sentence cell-state prediction."""
|
|
2
|
+
|
|
3
|
+
from .builder import DeviationSentenceBuilder
|
|
4
|
+
from .c2s_adapter import replace_sentences
|
|
5
|
+
|
|
6
|
+
__all__ = ["DeviationSentenceBuilder", "replace_sentences"]
|
|
7
|
+
__version__ = "0.1.0"
|
cellstate_c2s/builder.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Deviation-from-reference cell sentences."""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from scipy import sparse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeviationSentenceBuilder:
|
|
11
|
+
"""
|
|
12
|
+
Build cell sentences by ranking genes on signed deviation from a reference mean.
|
|
13
|
+
|
|
14
|
+
For each cell, (deviation = expression - reference_mean). Genes are sorted
|
|
15
|
+
by deviation (highest first, stable on ties) and the first (top_k) are
|
|
16
|
+
kept. With (mask_unexpressed=True), genes whose value in that cell is
|
|
17
|
+
exactly 0 are dropped, so sentences can be shorter than (top_k).
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
top_k : int
|
|
22
|
+
Maximum number of genes per sentence.
|
|
23
|
+
mask_unexpressed : bool
|
|
24
|
+
Drop genes with zero expression in the cell.
|
|
25
|
+
chunk_size : int
|
|
26
|
+
Number of cells densified at a time in ``transform``.
|
|
27
|
+
|
|
28
|
+
Notes
|
|
29
|
+
-----
|
|
30
|
+
adata.X must hold normalized expression, on the same scale for every
|
|
31
|
+
dataset you convert. The reference mean is taken over every cell passed to
|
|
32
|
+
fit (or the subset selected by mask).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, top_k=200, mask_unexpressed=True, chunk_size=2048):
|
|
36
|
+
if top_k < 1:
|
|
37
|
+
raise ValueError("top_k must be at least 1.")
|
|
38
|
+
if chunk_size < 1:
|
|
39
|
+
raise ValueError("chunk_size must be at least 1.")
|
|
40
|
+
self.top_k = top_k
|
|
41
|
+
self.mask_unexpressed = mask_unexpressed
|
|
42
|
+
self.chunk_size = chunk_size
|
|
43
|
+
self.reference_mean_ = None
|
|
44
|
+
self.gene_names_ = None
|
|
45
|
+
self.n_reference_cells_ = None
|
|
46
|
+
|
|
47
|
+
def fit(self, adata, mask=None):
|
|
48
|
+
"""
|
|
49
|
+
Compute the reference mean.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
adata : anndata.AnnData
|
|
54
|
+
Cells x genes, normalized.
|
|
55
|
+
mask : array-like of bool, optional
|
|
56
|
+
Selects the cells that define the reference (for example AT2 cells
|
|
57
|
+
only). Defaults to all cells.
|
|
58
|
+
"""
|
|
59
|
+
_check_unique_var_names(adata)
|
|
60
|
+
X = adata.X
|
|
61
|
+
if mask is not None:
|
|
62
|
+
mask = np.asarray(mask, dtype=bool)
|
|
63
|
+
if mask.shape != (adata.n_obs,):
|
|
64
|
+
raise ValueError(f"mask must have shape ({adata.n_obs},), got {mask.shape}.")
|
|
65
|
+
if not mask.any():
|
|
66
|
+
raise ValueError("mask selects no cells.")
|
|
67
|
+
X = X[mask]
|
|
68
|
+
if X.shape[0] == 0:
|
|
69
|
+
raise ValueError("Cannot fit a reference on zero cells.")
|
|
70
|
+
self.reference_mean_ = np.asarray(X.mean(axis=0)).ravel().astype(np.float64)
|
|
71
|
+
self.gene_names_ = np.asarray(adata.var_names).astype(str)
|
|
72
|
+
self.n_reference_cells_ = int(X.shape[0])
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def transform(self, adata):
|
|
76
|
+
"""
|
|
77
|
+
Return (cell_names, sentences) for every cell in adata.
|
|
78
|
+
|
|
79
|
+
Only genes present in both the reference and adata are used. A
|
|
80
|
+
warning lists how many reference genes are missing.
|
|
81
|
+
"""
|
|
82
|
+
self._check_fitted()
|
|
83
|
+
_check_unique_var_names(adata)
|
|
84
|
+
idx = pd.Index(adata.var_names).get_indexer(self.gene_names_)
|
|
85
|
+
keep = idx >= 0
|
|
86
|
+
if not keep.any():
|
|
87
|
+
raise ValueError("No genes shared between the reference and this AnnData.")
|
|
88
|
+
if not keep.all():
|
|
89
|
+
warnings.warn(
|
|
90
|
+
f"{int((~keep).sum())} of {len(keep)} reference genes are missing from "
|
|
91
|
+
"this AnnData and will be ignored.",
|
|
92
|
+
stacklevel=2,
|
|
93
|
+
)
|
|
94
|
+
col_idx = idx[keep]
|
|
95
|
+
genes = self.gene_names_[keep]
|
|
96
|
+
ref = self.reference_mean_[keep]
|
|
97
|
+
k = min(self.top_k, len(genes))
|
|
98
|
+
|
|
99
|
+
X = adata.X
|
|
100
|
+
sentences = []
|
|
101
|
+
for start in range(0, X.shape[0], self.chunk_size):
|
|
102
|
+
chunk = X[start:start + self.chunk_size][:, col_idx]
|
|
103
|
+
chunk = chunk.toarray() if sparse.issparse(chunk) else np.asarray(chunk)
|
|
104
|
+
chunk = chunk.astype(np.float64, copy=False)
|
|
105
|
+
dev = chunk - ref
|
|
106
|
+
if self.mask_unexpressed:
|
|
107
|
+
dev = np.where(chunk > 0, dev, -np.inf)
|
|
108
|
+
order = np.argsort(-dev, axis=1, kind="stable")[:, :k]
|
|
109
|
+
for row in range(order.shape[0]):
|
|
110
|
+
o = order[row]
|
|
111
|
+
if self.mask_unexpressed:
|
|
112
|
+
o = o[np.isfinite(dev[row, o])]
|
|
113
|
+
sentences.append(" ".join(genes[o]))
|
|
114
|
+
return [str(n) for n in adata.obs_names], sentences
|
|
115
|
+
|
|
116
|
+
def fit_transform(self, adata, mask=None):
|
|
117
|
+
"""Fit the reference on ``adata`` and transform the same cells."""
|
|
118
|
+
return self.fit(adata, mask=mask).transform(adata)
|
|
119
|
+
|
|
120
|
+
def save(self, path):
|
|
121
|
+
"""Save the reference and settings to a ``.npz`` file."""
|
|
122
|
+
self._check_fitted()
|
|
123
|
+
np.savez(
|
|
124
|
+
path,
|
|
125
|
+
reference_mean=self.reference_mean_,
|
|
126
|
+
gene_names=self.gene_names_,
|
|
127
|
+
n_reference_cells=self.n_reference_cells_,
|
|
128
|
+
top_k=self.top_k,
|
|
129
|
+
mask_unexpressed=self.mask_unexpressed,
|
|
130
|
+
chunk_size=self.chunk_size,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def load(cls, path):
|
|
135
|
+
"""Load a builder saved with :meth:save."""
|
|
136
|
+
with np.load(path, allow_pickle=False) as d:
|
|
137
|
+
obj = cls(
|
|
138
|
+
top_k=int(d["top_k"]),
|
|
139
|
+
mask_unexpressed=bool(d["mask_unexpressed"]),
|
|
140
|
+
chunk_size=int(d["chunk_size"]),
|
|
141
|
+
)
|
|
142
|
+
obj.reference_mean_ = d["reference_mean"]
|
|
143
|
+
obj.gene_names_ = d["gene_names"].astype(str)
|
|
144
|
+
obj.n_reference_cells_ = int(d["n_reference_cells"])
|
|
145
|
+
return obj
|
|
146
|
+
|
|
147
|
+
def _check_fitted(self):
|
|
148
|
+
if self.reference_mean_ is None:
|
|
149
|
+
raise RuntimeError("Call fit() first.")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _check_unique_var_names(adata):
|
|
153
|
+
if not adata.var_names.is_unique:
|
|
154
|
+
raise ValueError("var_names must be unique; call adata.var_names_make_unique().")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Glue for Cell2Sentence arrow datasets.
|
|
3
|
+
|
|
4
|
+
Works on Hugging Face datasets.Dataset, such as the one returned by
|
|
5
|
+
cell2sentence.CSData.adata_to_arrow. This module does not import
|
|
6
|
+
cell2sentence itself.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def replace_sentences(arrow_ds, cell_names, sentences,
|
|
11
|
+
field="cell_sentence", name_field="cell_name"):
|
|
12
|
+
"""Swap the sentence column of an arrow dataset, matching rows by cell name.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
arrow_ds : datasets.Dataset
|
|
17
|
+
Dataset with (field) and (name_field) columns.
|
|
18
|
+
cell_names, sentences : sequence of str
|
|
19
|
+
Output of (DeviationSentenceBuilder.transform).
|
|
20
|
+
field : str
|
|
21
|
+
Column holding the cell sentence.
|
|
22
|
+
name_field : str
|
|
23
|
+
Column holding the cell name, matching (adata.obs_names).
|
|
24
|
+
|
|
25
|
+
Returns
|
|
26
|
+
-------
|
|
27
|
+
datasets.Dataset
|
|
28
|
+
A new dataset; the input is not modified.
|
|
29
|
+
"""
|
|
30
|
+
if len(cell_names) != len(sentences):
|
|
31
|
+
raise ValueError("cell_names and sentences must have the same length.")
|
|
32
|
+
lookup = dict(zip(cell_names, sentences))
|
|
33
|
+
if len(lookup) != len(cell_names):
|
|
34
|
+
raise ValueError("cell_names contains duplicates.")
|
|
35
|
+
for col in (field, name_field):
|
|
36
|
+
if col not in arrow_ds.column_names:
|
|
37
|
+
raise KeyError(f"Column {col!r} not in dataset; columns are {arrow_ds.column_names}.")
|
|
38
|
+
missing = [n for n in arrow_ds[name_field] if n not in lookup]
|
|
39
|
+
if missing:
|
|
40
|
+
raise KeyError(
|
|
41
|
+
f"{len(missing)} cells in the arrow dataset have no sentence, e.g. {missing[:3]}"
|
|
42
|
+
)
|
|
43
|
+
return arrow_ds.map(lambda ex: {field: lookup[ex[name_field]]})
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cellstate_c2s
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deviation-from-reference gene ordering for Cell2Sentence cell-state prediction
|
|
5
|
+
Author-email: impatient_hedgehog <impatienthedgehog@duck.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: single-cell,cell2sentence,llm,scRNA-seq,xenium
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: scipy
|
|
16
|
+
Requires-Dist: pandas
|
|
17
|
+
Requires-Dist: anndata
|
|
18
|
+
Provides-Extra: c2s
|
|
19
|
+
Requires-Dist: cell2sentence; extra == "c2s"
|
|
20
|
+
Requires-Dist: datasets; extra == "c2s"
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest; extra == "dev"
|
|
23
|
+
Requires-Dist: datasets; extra == "dev"
|
|
24
|
+
Requires-Dist: build; extra == "dev"
|
|
25
|
+
Requires-Dist: twine; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# cellstate_c2s
|
|
29
|
+
|
|
30
|
+
Custom cell sentences for [Cell2Sentence](https://github.com/vandijklab/cell2sentence) fine-tuning and prediction.
|
|
31
|
+
|
|
32
|
+
Cell2Sentence normally writes each cell as its genes ordered by expression. This package replaces that with genes ordered by **deviation from a reference mean** (for example, the mean AT2 profile), so the sentence describes how a cell differs from the reference state rather than which genes are simply most abundant. You build the arrow dataset with Cell2Sentence as usual, then swap in the new sentences before creating `CSData`.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install cellstate_c2s # core: numpy, scipy, pandas, anndata
|
|
38
|
+
pip install "cellstate_c2s[c2s]" # plus cell2sentence and datasets
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
From a local clone:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install -e ".[dev]"
|
|
45
|
+
pytest
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import cell2sentence as cs
|
|
52
|
+
from cellstate_c2s import DeviationSentenceBuilder, replace_sentences
|
|
53
|
+
|
|
54
|
+
# 1. Build the arrow dataset with Cell2Sentence as usual
|
|
55
|
+
arrow_ds, vocabulary = cs.CSData.adata_to_arrow(
|
|
56
|
+
adata=adata_train, random_state=SEED, sentence_delimiter=" ",
|
|
57
|
+
label_col_names=["annotation_cell_states", "organism", "tissue"],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# 2. Fit the reference and build deviation sentences
|
|
61
|
+
builder = DeviationSentenceBuilder(top_k=200)
|
|
62
|
+
builder.fit(adata_train, mask=adata_train.obs["cell_type"] == "AT2") # optional mask
|
|
63
|
+
names, sentences = builder.transform(adata_train)
|
|
64
|
+
builder.save("reference.npz")
|
|
65
|
+
|
|
66
|
+
# 3. Swap the sentences in, matched by cell name, then continue with Cell2Sentence
|
|
67
|
+
arrow_ds = replace_sentences(arrow_ds, names, sentences)
|
|
68
|
+
csdata = cs.CSData.csdata_from_arrow(
|
|
69
|
+
arrow_dataset=arrow_ds, vocabulary=vocabulary,
|
|
70
|
+
save_dir=save_dir, save_name=save_name, dataset_backend="arrow",
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Held-out test data (same platform):** reuse the reference fitted on training data. Do not fit on the test set.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
builder = DeviationSentenceBuilder.load("reference.npz")
|
|
78
|
+
names, sentences = builder.transform(adata_test)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Another platform (e.g. Xenium):** fit a separate reference on that dataset, then do the Cell2Sentence vocabulary swap yourself.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
names_x, sents_x = DeviationSentenceBuilder(top_k=200).fit_transform(adata_xenium)
|
|
85
|
+
arrow_ds_xenium = replace_sentences(arrow_ds_xenium, names_x, sents_x)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## How sentences are built
|
|
89
|
+
|
|
90
|
+
For each cell, `deviation = expression - reference_mean`. Genes are sorted by deviation, highest first, and the first `top_k` are kept. Ties keep the order of `var_names` (stable sort), so the same input always gives the same sentence.
|
|
91
|
+
|
|
92
|
+
| Option | Default | Effect |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `top_k` | 200 | Maximum genes per sentence. Capped at the number of shared genes. |
|
|
95
|
+
| `mask_unexpressed` | `True` | Drops genes whose value in that cell is exactly 0, so sentences can be shorter than `top_k`. Set `False` for a pure ranking of all genes. |
|
|
96
|
+
| `chunk_size` | 2048 | Cells densified at a time. Lower it if memory is tight. |
|
|
97
|
+
| `fit(..., mask=)` | all cells | Boolean array selecting the cells that define the reference. |
|
|
98
|
+
|
|
99
|
+
`transform` uses only the genes shared by the reference and the AnnData passed in, warns about missing reference genes, and ignores extra genes.
|
|
100
|
+
|
|
101
|
+
## Before you use this
|
|
102
|
+
|
|
103
|
+
The code cannot check most of these, so they are on you. The first four are the most likely to cause silent errors.
|
|
104
|
+
|
|
105
|
+
1. **`adata.X` is normalized, the same way in every dataset.** Deviation is in expression units, so raw counts in one dataset and log-normalized values in another give meaningless ranks. The package never reads `layers` or `.raw`.
|
|
106
|
+
2. **Zero means "not expressed."** With `mask_unexpressed=True`, any gene with a value of exactly 0 in a cell is dropped.
|
|
107
|
+
3. **The cells passed to `fit` define the reference.** Pass only the cells you want (e.g. AT2), or use `mask=`.
|
|
108
|
+
4. **Fit on training data only for evaluation.** `fit_transform` on a test set computes the reference from test cells.
|
|
109
|
+
5. **Gene names are unique and use one naming system** (symbols or Ensembl IDs, not a mix). Duplicates raise an error; mismatched naming just shrinks the shared gene set.
|
|
110
|
+
6. **Cell names (`obs_names`) are unique and match the arrow dataset's `cell_name` column.** Field names are parameters of `replace_sentences` if your Cell2Sentence version differs.
|
|
111
|
+
7. **The datasets are already subset to the genes you want.** The package does not select genes.
|
|
112
|
+
|
|
113
|
+
## What is checked
|
|
114
|
+
|
|
115
|
+
| Checked in code | Not checked |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `fit` called before `transform` | Normalization |
|
|
118
|
+
| Unique gene names | Gene naming system |
|
|
119
|
+
| Shared genes exist; warns on missing ones | Whether the reference cells are the right ones |
|
|
120
|
+
| Every arrow row has a sentence; no duplicate cell names | Train/test separation |
|
|
121
|
+
| Required arrow columns exist | Meaning of zeros |
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
cellstate_c2s/__init__.py,sha256=hCwNcO2njGiLis8Zhbb_FKWn6Qo6aExAYzWtNutVe4E,259
|
|
2
|
+
cellstate_c2s/builder.py,sha256=q7MUO8h3brUP2RBBHpBpF1xhz0JJjEyQ4XnvM_dt-1A,5723
|
|
3
|
+
cellstate_c2s/c2s_adapter.py,sha256=kfHe6GiYga3f90WGlDcyYnF0-NW-SztJT9ar5vfI87Y,1604
|
|
4
|
+
cellstate_c2s-0.1.0.dist-info/licenses/LICENSE,sha256=ZPVnK694kpNR2D_ji4dqIcHxv4gZoS6glCJO26Yi_9w,1075
|
|
5
|
+
cellstate_c2s-0.1.0.dist-info/METADATA,sha256=TUyIVQpVpm0X9LAncsOqgtgKinVUkMZ17ellKqaYRoI,5665
|
|
6
|
+
cellstate_c2s-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
+
cellstate_c2s-0.1.0.dist-info/top_level.txt,sha256=MoF-soUO4Bt7jNeMwTzMPwFA1Gkeya9fbDU3HzPfrk0,14
|
|
8
|
+
cellstate_c2s-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 impatient_hedgehog
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cellstate_c2s
|