Skip to content

Commit b5cd347

Browse files
features on 1.5.1: leiden + gating on scatter
1 parent 1fa2d34 commit b5cd347

File tree

8 files changed

+175
-54
lines changed

8 files changed

+175
-54
lines changed

docs/api/tools.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22
options:
33
show_root_heading: true
44

5-
::: scyan.tools.palette_level
5+
::: scyan.tools.leiden
66
options:
77
show_root_heading: true
88

99
::: scyan.tools.subcluster
1010
options:
1111
show_root_heading: true
1212

13+
::: scyan.tools.palette_level
14+
options:
15+
show_root_heading: true
16+
1317
::: scyan.tools.cell_type_ratios
1418
options:
1519
show_root_heading: true
@@ -25,3 +29,13 @@
2529
- __init__
2630
- select
2731
- save_selection
32+
- extract_adata
33+
34+
::: scyan.tools.PolygonGatingScatter
35+
options:
36+
show_root_heading: true
37+
members:
38+
- __init__
39+
- select
40+
- save_selection
41+
- extract_adata

pyproject.toml

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "scyan"
3-
version = "1.5.0"
3+
version = "1.5.1"
44
description = "Single-cell Cytometry Annotation Network"
55
documentation = "https://mics-lab.github.io/scyan/"
66
homepage = "https://mics-lab.github.io/scyan/"
@@ -14,11 +14,9 @@ classifiers = [
1414
"Operating System :: POSIX :: Linux",
1515
"Operating System :: Microsoft :: Windows",
1616
"Programming Language :: Python :: 3",
17-
"Topic :: Scientific/Engineering"
18-
]
19-
packages = [
20-
{ include = "scyan" },
17+
"Topic :: Scientific/Engineering",
2118
]
19+
packages = [{ include = "scyan" }]
2220

2321
[tool.poetry.dependencies]
2422
python = ">=3.8,<3.11"
@@ -32,25 +30,35 @@ FlowUtils = "^1.0.0"
3230
fcsparser = "^0.2.4"
3331
fcswrite = "^0.6.2"
3432

35-
wandb = {version = "0.13.7", optional = true}
36-
hydra-core = {version = "^1.2.0", optional = true}
37-
hydra-colorlog = {version = "^1.2.0", optional = true}
38-
hydra-optuna-sweeper = {version = "^1.2.0", optional = true}
33+
wandb = { version = "0.13.7", optional = true }
34+
hydra-core = { version = "^1.2.0", optional = true }
35+
hydra-colorlog = { version = "^1.2.0", optional = true }
36+
hydra-optuna-sweeper = { version = "^1.2.0", optional = true }
3937

40-
pytest = {version = "^7.1.2", optional = true}
41-
ipykernel = {version = "^6.15.0", optional = true}
42-
ipywidgets = {version = "^7.7.1", optional = true}
43-
isort = {version = "^5.10.1", optional = true}
44-
black = {version = "^22.6.0", optional = true}
45-
mkdocs-material = {version = "^8.5.0", optional = true}
46-
mkdocstrings = {version = "^0.19.0", optional = true}
47-
mkdocstrings-python = {version = "^0.7.1", optional = true}
48-
mkdocs-jupyter = {version = "^0.21.0", optional = true}
38+
pytest = { version = "^7.1.2", optional = true }
39+
ipykernel = { version = "^6.15.0", optional = true }
40+
ipywidgets = { version = "^7.7.1", optional = true }
41+
isort = { version = "^5.10.1", optional = true }
42+
black = { version = "^22.6.0", optional = true }
43+
mkdocs-material = { version = "^8.5.0", optional = true }
44+
mkdocstrings = { version = "^0.19.0", optional = true }
45+
mkdocstrings-python = { version = "^0.7.1", optional = true }
46+
mkdocs-jupyter = { version = "^0.21.0", optional = true }
4947

50-
leidenalg = {version = "^0.8.10", optional = true}
48+
leidenalg = { version = "^0.8.10", optional = true }
5149

5250
[tool.poetry.extras]
53-
dev = ["pytest", "ipykernel", "ipywidgets", "isort", "black", "mkdocs-material", "mkdocstrings", "mkdocstrings-python", "mkdocs-jupyter"]
51+
dev = [
52+
"pytest",
53+
"ipykernel",
54+
"ipywidgets",
55+
"isort",
56+
"black",
57+
"mkdocs-material",
58+
"mkdocstrings",
59+
"mkdocstrings-python",
60+
"mkdocs-jupyter",
61+
]
5462
hydra = ["wandb", "hydra-core", "hydra-colorlog", "hydra-optuna-sweeper"]
5563
discovery = ["leidenalg"]
5664

@@ -65,7 +73,7 @@ python_files = "test_*.py"
6573
filterwarnings = [
6674
"ignore::DeprecationWarning",
6775
"ignore::UserWarning",
68-
"ignore:::.*anndata*"
76+
"ignore:::.*anndata*",
6977
]
7078

7179
[tool.black]
@@ -88,4 +96,4 @@ exclude = '''
8896

8997
[tool.isort]
9098
profile = "black"
91-
skip_glob = ["*/__init__.py"]
99+
skip_glob = ["*/__init__.py"]

scyan/plot/density.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def kde(
2727
"""Plot Kernel-Density-Estimation for each provided population and for multiple markers.
2828
2929
Args:
30-
adata: An `anndata` object.
30+
adata: An `AnnData` object.
3131
population: One population, or a list of population to be analyzed, or `None`. If not `None`, the population name(s) has to be in `adata.obs[key]`.
3232
markers: List of markers to plot. If `None`, the list is chosen automatically.
3333
key: Key to look for populations in `adata.obs`. By default, uses the model predictions.
@@ -97,7 +97,7 @@ def log_prob_threshold(adata: AnnData, show: bool = True):
9797
To use this function, you first need to fit a `scyan.Scyan` model and use the `model.predict()` method.
9898
9999
Args:
100-
adata: The `anndata` object used during the model training.
100+
adata: The `AnnData` object used during the model training.
101101
show: Whether or not to display the figure.
102102
"""
103103
assert (

scyan/plot/dot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def scatter(
2727
One scatter plot is displayed for each pair of markers.
2828
2929
Args:
30-
adata: An `anndata` object.
30+
adata: An `AnnData` object.
3131
population: One population, or a list of population to be colored, or `None`. If not `None`, the population name(s) has to be in `adata.obs[key]`.
3232
markers: List of markers to plot. If `None`, the list is chosen automatically.
3333
n_markers: Number of markers to choose automatically if `markers is None`.
@@ -79,7 +79,7 @@ def umap(
7979
If you trained your UMAP with [scyan.tools.umap][] on a subset of cells, it will only display the desired subset of cells.
8080
8181
Args:
82-
adata: An `anndata` object.
82+
adata: An `AnnData` object.
8383
color: Marker(s) or `obs` name(s) to color. It can be either just one string, or a list (it will plot one UMAP per element in the list).
8484
vmax: `scanpy.pl.umap` vmax argument.
8585
vmin: `scanpy.pl.umap` vmin argument.

scyan/preprocess.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def auto_logicle_transform(
1818
We recommend it for flow cytometry or spectral flow cytometry data.
1919
2020
Args:
21-
adata: An `anndata` object.
21+
adata: An `AnnData` object.
2222
q: See logicle article. Defaults to 0.05.
2323
m: See logicle article. Defaults to 4.5.
2424
"""
@@ -73,7 +73,7 @@ def asinh_transform(adata: AnnData, translation: float = 0, cofactor: float = 5)
7373
"""Asinh transformation for cell-expressions: $asinh((x - translation)/cofactor)$.
7474
7575
Args:
76-
adata: An `anndata` object.
76+
adata: An `AnnData` object.
7777
translation: Constant substracted to the marker expression before division by the cofactor.
7878
cofactor: Scaling factor before computing the asinh.
7979
"""
@@ -93,7 +93,7 @@ def inverse_transform(
9393
If you scaled your data, the complete inverse consists in running [scyan.preprocess.unscale][] first, and then this function.
9494
9595
Args:
96-
adata: An `anndata` object.
96+
adata: An `AnnData` object.
9797
obsm: Name of the anndata obsm to consider. If `None`, use `adata.X`.
9898
obsm_names: Names of the ordered markers from obsm. It is required if obsm is not `None`, if there are less markers than in `adata.X`, and if the transformation to reverse is `logicle`. Usually, it corresponds to `model.var_names`.
9999
transformation: Name of the transformation to inverse: one of `['logicle', 'asinh', None]`. By default, it chooses automatically depending on which transformation was previously run.
@@ -148,7 +148,7 @@ def scale(adata: AnnData, max_value: float = 10, center: Optional[bool] = None)
148148
"""Tranforms the data such as (i) `std=1`, and (ii) either `0` is sent to `-1` (for CyTOF data) or `means=0` (for flow or spectral flow data); except if `center` is set (which overwrites the default behavior).
149149
150150
Args:
151-
adata: An `anndata` object.
151+
adata: An `AnnData` object.
152152
max_value: Clip to this value after scaling.
153153
center: If `None`, data is only centered for spectral or flow cytometry data (recommended), else, it is centered or not according to the value given.
154154
"""
@@ -175,7 +175,7 @@ def unscale(
175175
"""Reverse standardisation. It requires to have run [scyan.preprocess.scale][] before.
176176
177177
Args:
178-
adata: An `anndata` object.
178+
adata: An `AnnData` object.
179179
obsm: Name of the adata obsm to consider. If `None`, use `adata.X`.
180180
obsm_names: Names of the ordered markers from obsm. It is required if obsm is not `None`, and if there are less markers than in `adata.X`. Usually, it corresponds to `model.var_names`.
181181

scyan/tools/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .representation import umap, subcluster
1+
from .representation import umap, subcluster, leiden
22
from .biomarkers import cell_type_ratios, mean_intensities
3-
from .gating import PolygonGatingUMAP
3+
from .gating import PolygonGatingUMAP, PolygonGatingScatter
44
from .colors import palette_level

scyan/tools/gating.py

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@
1010

1111

1212
class _SelectFromCollection:
13-
"""From https://matplotlib.org/stable/gallery/widgets/polygon_selector_demo.html"""
13+
"""Updated from https://matplotlib.org/stable/gallery/widgets/polygon_selector_demo.html"""
1414

15-
def __init__(self, ax, collection, alpha_other=0.3):
15+
def __init__(self, ax, collection, xy: np.ndarray, alpha_other: float = 0.3):
1616
from matplotlib.widgets import PolygonSelector
1717

1818
self.canvas = ax.figure.canvas
1919
self.collection = collection
20+
self.xy = xy
2021
self.alpha_other = alpha_other
2122

22-
self.xys = collection.get_offsets()
23-
self.n_obs = len(self.xys)
23+
self.n_obs = len(self.xy)
2424

2525
self.fc = collection.get_facecolors()
2626
self.fc = np.tile(self.fc, (self.n_obs, 1))
@@ -32,41 +32,37 @@ def onselect(self, verts):
3232
from matplotlib.path import Path
3333

3434
path = Path(verts)
35-
self.ind = np.nonzero(path.contains_points(self.xys))[0]
35+
self.ind = np.nonzero(path.contains_points(self.xy))[0]
3636
self.fc[:, -1] = self.alpha_other
3737
self.fc[self.ind, -1] = 1
3838
self.collection.set_facecolors(self.fc)
3939
self.canvas.draw_idle()
4040

4141
def disconnect(self):
4242
self.poly.disconnect_events()
43-
# TODO: understand why it crashes
44-
# self.fc[:, -1] = 1
45-
# self.collection.set_facecolors(self.fc)
46-
# self.canvas.draw_idle()
4743

4844

4945
class PolygonGatingUMAP:
5046
"""Class used to select cells on a UMAP using polygons.
5147
5248
!!! note
5349
54-
We recommend using it on Jupyter Notebooks. To be able to select the cells, you should first run `%matplotlib tk` on a blank jupyter cell. After the selection, you can run `%matplotlib inline` to retrieve the default behavior.
50+
If used on a Jupyter Notebook, you should first run `%matplotlib tk`. After the selection, you can run `%matplotlib inline` to retrieve the default behavior.
5551
5652
```py
57-
# Usage example (to be run on a jupyter notebook) (`%matplotlib tk` is required for the cell selection)
53+
# Usage example (`%matplotlib tk` is required for the cell selection on jupyter notebooks)
5854
>>> %matplotlib tk
5955
>>> selector = scyan.tools.PolygonGatingUMAP(adata)
6056
>>> selector.select() # select the cells
61-
>>> selector.save_selection() # save the selected cells in adata.obs
62-
>>> %matplotlib inline # to retrieve the default behavior
57+
58+
>>> sub_adata = selector.extract_adata() # on a notebook, this has to be on a new jupyter cell
6359
```
6460
"""
6561

6662
def __init__(self, adata: AnnData) -> None:
6763
"""
6864
Args:
69-
adata: An `anndata` object.
65+
adata: An `AnnData` object.
7066
"""
7167
self.adata = adata
7268
self.has_umap = _has_umap(adata)
@@ -88,7 +84,7 @@ def select(self, s: float = 0.05) -> None:
8884
s=s,
8985
)
9086

91-
self.selector = _SelectFromCollection(ax, pts)
87+
self.selector = _SelectFromCollection(ax, pts, self.x_umap[self.has_umap])
9288

9389
log.info(
9490
f"Enclose cells within a polygon. Helper:\n - Click on the plot to add a polygon vertex\n - Press the 'esc' key to start a new polygon\n - Try holding the 'ctrl' key to move a single vertex\n - Once the polygon is finished and overlaid in red, you can close the window"
@@ -112,3 +108,95 @@ def save_selection(self, key_added: str = "scyan_selected"):
112108
log.info(
113109
f"Selected {len(self.selector.ind)} cells and saved the selection in adata.obs['{key_added}']"
114110
)
111+
112+
def extract_adata(self) -> AnnData:
113+
"""Returns an anndata objects whose cells where inside the polygon"""
114+
log.info(f"Selected {len(self.selector.ind)} cells")
115+
self.selector.disconnect()
116+
117+
return self.adata[np.where(self.has_umap)[0][self.selector.ind]]
118+
119+
120+
class PolygonGatingScatter:
121+
"""Class used to select cells on a scatterplot using polygons.
122+
123+
!!! note
124+
125+
If used on a Jupyter Notebook, you should first run `%matplotlib tk` on a blank jupyter cell. After the selection, you can run `%matplotlib inline` to retrieve the default behavior.
126+
127+
```py
128+
# Usage example (`%matplotlib tk` is required for the cell selection on jupyter notebooks)
129+
>>> %matplotlib tk
130+
>>> selector = scyan.tools.PolygonGatingScatter(adata)
131+
>>> selector.select() # select the cells
132+
133+
>>> sub_adata = selector.extract_adata() # on a notebook, this has to be on a new jupyter cell
134+
```
135+
"""
136+
137+
def __init__(self, adata: AnnData) -> None:
138+
"""
139+
Args:
140+
adata: An `AnnData` object.
141+
"""
142+
self.adata = adata
143+
144+
def select(
145+
self, x: str, y: str, s: float = 0.05, max_cells_display: int = 100_000
146+
) -> None:
147+
"""Open a scatter plot on which you can draw a polygon to select cells.
148+
149+
Args:
150+
x: Column name of adata.obs used for the x-axis
151+
y: Column name of adata.obs used for the y-axis
152+
s: Size of the cells on the plot.
153+
"""
154+
_, ax = plt.subplots()
155+
156+
indices = np.arange(self.adata.n_obs)
157+
if max_cells_display is not None and max_cells_display < self.adata.n_obs:
158+
indices = np.random.choice(
159+
np.arange(self.adata.n_obs), size=max_cells_display, replace=False
160+
)
161+
162+
x = self.adata.obs_vector(x)
163+
y = self.adata.obs_vector(y)
164+
xy = np.stack([x, y], axis=1)
165+
166+
pts = ax.scatter(
167+
xy[indices, 0],
168+
xy[indices, 1],
169+
marker=".",
170+
rasterized=True,
171+
s=s,
172+
)
173+
174+
self.selector = _SelectFromCollection(ax, pts, xy)
175+
176+
log.info(
177+
f"Enclose cells within a polygon. Helper:\n - Click on the plot to add a polygon vertex\n - Press the 'esc' key to start a new polygon\n - Try holding the 'ctrl' key to move a single vertex\n - Once the polygon is finished and overlaid in red, you can close the window"
178+
)
179+
plt.show()
180+
181+
def save_selection(self, key_added: str = "scyan_selected"):
182+
"""Save the selected cells in `adata.obs[key_added]`.
183+
184+
Args:
185+
key_added: Column name used to save the selected cells in `adata.obs`.
186+
"""
187+
self.adata.obs[key_added] = "unselected"
188+
col_index = self.adata.obs.columns.get_loc(key_added)
189+
self.adata.obs.iloc[self.selector.ind, col_index] = "selected"
190+
self.adata.obs[key_added] = self.adata.obs[key_added].astype("category")
191+
192+
self.selector.disconnect()
193+
log.info(
194+
f"Selected {len(self.selector.ind)} cells and saved the selection in adata.obs['{key_added}']"
195+
)
196+
197+
def extract_adata(self) -> AnnData:
198+
"""Returns an anndata objects whose cells where inside the polygon"""
199+
log.info(f"Selected {len(self.selector.ind)} cells")
200+
self.selector.disconnect()
201+
202+
return self.adata[self.selector.ind]

0 commit comments

Comments
 (0)