├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── concordex ├── .DS_Store ├── __init__.py ├── neighbors │ ├── __init__.py │ └── _neighborhood.py ├── tools │ ├── __init__.py │ └── _concordex.py └── utils │ └── _labels.py ├── examples ├── concordex_py_pbmc_demo.ipynb ├── concordex_py_starmap_demo.ipynb └── data │ └── starmap_processed.h5ad ├── pyproject.toml ├── requirements.txt └── setup.cfg /.gitignore: -------------------------------------------------------------------------------- 1 | # Python build files 2 | __pycache__/ 3 | **/__pycache__/ 4 | 5 | *.egg-info/ 6 | 7 | /dist/ 8 | /build/ 9 | /*-env/ 10 | /env-*/ 11 | /environment.yml 12 | 13 | # IDEs and OS things 14 | .DS_Store 15 | /.vscode/ 16 | .ipynb_checkpoints/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Pachter Lab 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY : clean build upload 2 | 3 | clean: 4 | rm -rf build 5 | rm -rf dist 6 | rm -rf concordex.egg-info 7 | rm -rf docs/_build 8 | rm -rf docs/api 9 | rm -rf .coverage 10 | 11 | build: 12 | python -m build --wheel 13 | 14 | upload: 15 | twine upload dist/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # concordex 1.1.1 2 | 3 | The goal of `concordex` is to identify spatial homogeneous regions (SHRs) as defined in the recent manuscript, [“Identification of spatial homogenous regions in tissues with concordex”](https://doi.org/10.1101/2023.06.28.546949). Briefly, SHRs are are domains that are homogeneous with respect to cell type composition. `concordex` relies on the the k-nearest-neighbor (kNN) graph to representing similarities between cells and uses common clustering algorithms to identify SHRs. 4 | 5 | ## Installation 6 | 7 | `concordex` can be installed via pip 8 | ```bash 9 | pip install concordex 10 | ``` 11 | .... and from Github 12 | ```bash 13 | pip install git+https://github.com/pachterlab/concordex.git 14 | ``` 15 | 16 | ## Usage 17 | 18 | After installing, `concordex` can be run as follows: 19 | ``` 20 | import scanpy as sc 21 | from concordex.tools import calculate_concordex 22 | 23 | ad = sc.datasets.pbmc68k_reduced() 24 | 25 | # Compute concordex with discrete labels 26 | calculate_concordex(ad, 'louvain', n_neighbors=10) 27 | 28 | # Neighborhood consolidation information is stored in `adata.obsm` 29 | ad.obsm['X_nbc'][:3] 30 | 31 | # The column names are stored in `adata.uns` 32 | ad.uns['nbc_params']['nbc_colnames'] 33 | ``` 34 | 35 | ## Citation 36 | 37 | If you’d like to use the `concordex` package in your research, please 38 | cite our recent bioRxiv preprint: 39 | 40 | > Jackson, K.; Booeshaghi, A. S.; Gálvez-Merchán, Á.; Moses, L.; Chari, 41 | > T.; Kim, A.; Pachter, L. Identification of spatial homogeneous regions in tissues 42 | > with concordex. bioRxiv (Cold Spring Harbor Laboratory) 2023. 43 | > . 44 | 45 | @article {Jackson2023.06.28.546949, 46 | author = {Jackson, Kayla C. and Booeshaghi, A. Sina and G{'a}lvez-Merch{'a}n, {'A}ngel and Moses, Lambda and Chari, Tara and Kim, Alexandra and Pachter, Lior}, 47 | title = {Identification of spatial homogeneous regions in tissues with concordex}, 48 | year = {2024}, 49 | doi = {10.1101/2023.06.28.546949}, 50 | publisher = {Cold Spring Harbor Laboratory}, 51 | URL = {}, 52 | journal = {bioRxiv} 53 | } 54 | 55 | ## Maintainer 56 | 57 | [Kayla Jackson](https://github.com/kayla-jackson) 58 | -------------------------------------------------------------------------------- /concordex/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pachterlab/concordex/77ef3bf7382c461ae3c9a9362bf51a3283175347/concordex/.DS_Store -------------------------------------------------------------------------------- /concordex/__init__.py: -------------------------------------------------------------------------------- 1 | """Identification of spatial homogeneous regions""" 2 | 3 | __version__ = '1.1.1' 4 | -------------------------------------------------------------------------------- /concordex/neighbors/__init__.py: -------------------------------------------------------------------------------- 1 | from ._neighborhood import ( 2 | compute_neighbors, 3 | consolidate 4 | ) 5 | 6 | from ..utils._labels import Labels 7 | 8 | __all__ = [ 9 | 'compute_neighbors', 10 | 'consolidate' 11 | ] -------------------------------------------------------------------------------- /concordex/neighbors/_neighborhood.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import warnings 3 | 4 | from anndata import AnnData 5 | from sklearn.neighbors import NearestNeighbors 6 | 7 | from ..utils._labels import Labels 8 | 9 | def consolidate( 10 | adata: AnnData, 11 | labels, 12 | *, 13 | compute_similarity: bool = False, 14 | key_added: str | None = None, 15 | copy: bool = False 16 | ): 17 | """ 18 | Compute the neighborhood consolidation matrix. 19 | 20 | adata 21 | The AnnData object 22 | labels 23 | Observation labels used to compute the neighborhood 24 | consolidation matrix. Continuous or discrete labels are allowed, 25 | and typically, integer labels are assumed to be discrete. 26 | compute_similarity 27 | Whether to return the label similarity matrix. Only useful if 28 | discrete labels are provided. 29 | key_added 30 | If not specified, the neighborhood consolidation matrix is stored as 31 | :attr:`~anndata.AnnData.obsm`\\ `['X_nbc']`, and the parameters as 32 | :attr:`~anndata.AnnData.uns`\\ `['nbc_params']`. 33 | The index is assumed to be stored as 34 | :attr:`~anndata.AnnData.obsm`\\`['index']` 35 | 36 | If specified, ``[key_added]`` is prepended to the default keys. 37 | copy 38 | If ``copy=True``, return the neighborhood consolidation matrix instead 39 | of updating adata. 40 | """ 41 | 42 | if key_added is None: 43 | nbc_uns_key = "nbc_params" 44 | nbc_key = "X_nbc" 45 | index_key = "index" 46 | else: 47 | nbc_key = key_added + "_nbc" 48 | nbc_uns_key = key_added + "_nbc_params" 49 | index_key = key_added + "_index" 50 | 51 | if index_key in adata.obsm.keys(): 52 | Index = adata.obsm[index_key] 53 | else: 54 | raise ValueError("Must run ``concordex.neighbors.compute_neighbors()``") 55 | 56 | labels = Labels(labels) 57 | labels.extract(adata) 58 | 59 | if labels.labeltype != "discrete" and compute_similarity: 60 | compute_similarity = False 61 | warnings.warn("Expected discrete labels to compute similarity matrix") 62 | 63 | 64 | if compute_similarity: 65 | print("Computing neighborhood consolidation and similarity matrices...\n") 66 | nbc, sim = _consolidate(Index, labels, compute_similarity) 67 | else: 68 | print("Computing neighborhood consolidation matrix...\n") 69 | nbc = _consolidate(Index, labels, compute_similarity) 70 | 71 | adata.uns[nbc_uns_key] = {} 72 | nbc_index_dict = adata.uns[nbc_uns_key] 73 | 74 | nbc_index_dict = { 75 | "nbc_key": nbc_key, 76 | "labels": labels, 77 | "labels_found" : labels.labelnames, 78 | "nbc_colnames" : labels.nbccolumns, 79 | "params": { 80 | "compute_similarity": compute_similarity 81 | }, 82 | } 83 | 84 | if compute_similarity: 85 | nbc_index_dict['similarity'] = sim 86 | nbc_index_dict['labelorder'] = labels.discretelabelsunique 87 | 88 | if copy: 89 | return nbc, nbc_index_dict 90 | 91 | # Update adata 92 | adata.uns[nbc_uns_key] = nbc_index_dict 93 | adata.obsm[nbc_key] = nbc 94 | 95 | 96 | def _consolidate(X, labels, compute_similarity): 97 | 98 | def take_col_means(indices, take_from, take_by='row'): 99 | if take_by == 'row': 100 | axis=0 101 | else: 102 | axis=1 103 | sub = np.take(take_from, indices, axis=axis) 104 | 105 | return sub.mean(axis=0) 106 | 107 | labels_values = labels.values 108 | 109 | Nbc = np.apply_along_axis(take_col_means, 1, X, take_from=labels_values) 110 | 111 | if compute_similarity: 112 | nlab = labels.n_unique_labels 113 | labels_new = labels.discretelabelscollapsed 114 | 115 | labels_uniq = labels.discretelabelsunique 116 | 117 | Sim = np.empty((nlab, nlab), dtype=np.float64) 118 | for i, lab in enumerate(labels_uniq): 119 | m = np.isin(labels_new, lab) 120 | 121 | Sim[i,:] = Nbc[m, :].mean(axis=0) 122 | 123 | return Nbc, Sim 124 | 125 | return Nbc 126 | 127 | 128 | def compute_neighbors( 129 | adata: AnnData, 130 | *, 131 | use_rep: str | None = None, 132 | n_neighbors: int = 30, 133 | metric: str = "euclidean", 134 | metric_params: dict | None = None, 135 | n_jobs: int | None = None, 136 | key_added: str | None = None, 137 | recompute_index: bool = False, 138 | copy: bool = False, 139 | **kwargs 140 | ): 141 | """ 142 | A very thin wrapper around `sklearn.neighbors.NearestNeighbors` 143 | 144 | adata 145 | The adata object 146 | use_rep 147 | Key in adata.obsm to use for constructing the kNN graph 148 | n_neighbors 149 | Number of neighbors used to compute the kNN graph. Defaults to 30. 150 | metric 151 | Metric used to compute distance 152 | metric_params 153 | Additional params passed to metric function 154 | n_jobs 155 | Used to control parallel evaluation 156 | key_added 157 | Key which controls where the results are saved if ``copy = False``. 158 | recompute_index 159 | If a neighborhood graph exists at the specified key, should the 160 | data be overwritten? 161 | copy : bool 162 | If ``copy = True``, return the nearest neighbor graph instead of 163 | updating adata. 164 | **kwargs 165 | Additional keyword arguments passed to sklearn.neighbors.NearestNeighbors 166 | """ 167 | if use_rep is None or use_rep == 'X': 168 | X = adata.X 169 | else: 170 | if use_rep in adata.obsm.keys(): 171 | X = adata.obsm[use_rep] 172 | else: 173 | raise ValueError( 174 | f"Did not find {use_rep} in ``.obsm.keys()``. " 175 | ) 176 | 177 | nn_kwargs = {} 178 | if kwargs: 179 | nn_kwargs = kwargs 180 | if metric_params is not None: 181 | if 'p' in metric_params.keys(): 182 | p = metric_params.pop('p') 183 | nn_kwargs['p'] = p 184 | 185 | nn_kwargs['metric_params'] = metric_params 186 | 187 | if key_added is None: 188 | index_uns_key = "index_params" 189 | index_key = "index" 190 | else: 191 | index_uns_key = key_added + "_index_params" 192 | index_key = key_added + "_index" 193 | 194 | index_exists = index_key in adata.obsm.keys() 195 | 196 | print("Computing nearest neighbors...\n") 197 | if index_exists and not recompute_index: 198 | warnings.warn( 199 | f"A neighborhood graph already exists at ``adata.obsm[{index_key}]``. \ 200 | Set ``recompute_index = TRUE`` to overwrite the existing graph.") 201 | 202 | Index = adata.obsm[index_key] 203 | neighbors_index_dict = adata.uns[index_uns_key] 204 | 205 | if recompute_index or not index_exists: 206 | 207 | Index = _compute_neighbors( 208 | X, n_neighbors=n_neighbors, metric=metric, n_jobs=n_jobs, **nn_kwargs 209 | ) 210 | 211 | adata.uns[index_uns_key] = {} 212 | neighbors_index_dict = adata.uns[index_uns_key] 213 | 214 | neighbors_index_dict = { 215 | "index_key": index_key, 216 | "params": { 217 | "n_neighbors": n_neighbors, 218 | "metric": metric, 219 | } 220 | } 221 | 222 | if nn_kwargs: 223 | neighbors_index_dict['params']['nn_kwargs'] = nn_kwargs 224 | 225 | if use_rep is not None: 226 | neighbors_index_dict["params"]["use_rep"] = use_rep 227 | 228 | if copy: 229 | return Index 230 | 231 | # Update adata 232 | adata.uns[index_uns_key] = neighbors_index_dict 233 | adata.obsm[index_key] = Index 234 | 235 | def _compute_neighbors( 236 | X, 237 | *, 238 | n_neighbors: int = 30, 239 | include_self: bool = False, 240 | **kwargs 241 | ): 242 | 243 | N = X.shape[0] 244 | 245 | if include_self: 246 | index = 0 247 | else: 248 | index = 1 249 | n_neighbors = n_neighbors+1 250 | 251 | nbrs = NearestNeighbors(n_neighbors=n_neighbors, **kwargs) 252 | nbrs.fit(X) 253 | 254 | g = nbrs.kneighbors(X, return_distance=False) 255 | 256 | return g[:, index:] 257 | 258 | -------------------------------------------------------------------------------- /concordex/tools/__init__.py: -------------------------------------------------------------------------------- 1 | from ._concordex import calculate_concordex 2 | 3 | __all__ = [ 4 | 'calculate_concordex' 5 | ] -------------------------------------------------------------------------------- /concordex/tools/_concordex.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from anndata import AnnData 3 | 4 | from ..neighbors._neighborhood import ( 5 | compute_neighbors, 6 | consolidate 7 | ) 8 | 9 | 10 | def calculate_concordex( 11 | adata: AnnData, 12 | labels, 13 | *, 14 | n_neighbors: int = 30, 15 | use_rep: str | None = "X", 16 | metric: str = "euclidean", 17 | metric_params: dict | None = None, 18 | n_jobs: int | None = None, 19 | key_added: str | None = None, 20 | compute_similarity: bool = False, 21 | recompute_index: bool = False 22 | ): 23 | """ 24 | adata 25 | The AnnData object 26 | labels 27 | Observation labels used to compute the neighborhood 28 | consolidation matrix. Continuous or discrete labels are allowed, 29 | and typically, integer labels are assumed to be discrete. 30 | n_neighbors 31 | Number of neighbors used to compute the kNN graph. Defaults to 30. 32 | metric 33 | Metric used to compute distance 34 | metric_params 35 | Additional parameters passed to metric function 36 | n_jobs 37 | Used to control parallel evaluation 38 | key_added 39 | If not specified, the relevant results are stored as 40 | :attr:`~anndata.AnnData.obsm`\\ `['index']`, the neighborhood consolidation matrix as 41 | :attr:`~anndata.AnnData.obsm`\\ `['nbc']`, and the parameters as 42 | :attr:`~anndata.AnnData.uns`\\ `['index_params']` and 43 | :attr:`~anndata.AnnData.uns`\\ `['nbc_params']`. 44 | If specified, ``[key_added]`` is prepended to the default keys. 45 | compute_similarity 46 | Whether to return the label similarity matrix and stores this information in 47 | adata.uns['nbc_params']['similarity']. Only implemented for discrete labels. 48 | recompute_index 49 | If a neighborhood graph exists at the specified key, should the 50 | data be overwritten? 51 | """ 52 | 53 | # 1. Compute neighborhood graph 54 | compute_neighbors(adata, 55 | use_rep=use_rep, n_neighbors=n_neighbors, metric=metric, 56 | metric_params=metric_params, n_jobs=n_jobs, key_added=key_added, recompute_index=recompute_index) 57 | 58 | # 2. Then consolidate 59 | consolidate(adata, labels, key_added=key_added, compute_similarity=compute_similarity) 60 | 61 | 62 | -------------------------------------------------------------------------------- /concordex/utils/_labels.py: -------------------------------------------------------------------------------- 1 | import re 2 | from warnings import warn 3 | 4 | import numpy as np 5 | import pandas as pd 6 | 7 | from anndata import AnnData 8 | 9 | class Labels: 10 | def __init__(self, names): 11 | 12 | if names is None: 13 | raise ValueError("No labels to search for. Must provide labels.") 14 | 15 | self._lookup = names 16 | 17 | @property 18 | def labeltype(self) -> str: 19 | if not hasattr(self, "_labeltype"): 20 | raise AttributeError("`labeltype` has not been set. Call `extract(adata)` first") 21 | 22 | return self._labeltype 23 | 24 | @property 25 | def labelnames(self) -> str | list: 26 | if not hasattr(self, '_labelnames'): 27 | raise AttributeError("`labelnames` has not been set. Call `extract(adata)` first.") 28 | 29 | return self._labelnames 30 | 31 | @property 32 | def values(self): 33 | if not hasattr(self, '_values'): 34 | raise AttributeError("`values` has not been set. Call `extract(adata)` first.") 35 | return self._values 36 | 37 | @property 38 | def n_unique_labels(self): 39 | if not hasattr(self, '_labelshape'): 40 | raise AttributeError("Unable to determine number of labels. Call `extract(adata)` first.") 41 | return self._labelshape[1] 42 | 43 | @property 44 | def discretelabelscollapsed(self): 45 | if not hasattr(self, '_discretelabelscollapsed'): 46 | raise AttributeError("Ensure that labels are discrete and call `extract(adata)`.") 47 | return self._discretelabelscollapsed 48 | 49 | @property 50 | def discretelabelsunique(self): 51 | if not hasattr(self, '_discretelabelsunique'): 52 | raise AttributeError("Ensure that labels are discrete and call `extract(adata)`.") 53 | return self._discretelabelsunique 54 | 55 | @property 56 | def nbccolumns(self): 57 | if not hasattr(self, '_labelcolumns'): 58 | if not hasattr(self, '_values'): 59 | return [] 60 | else: 61 | _nattr = self._values.shape[1] 62 | self._labelcolumns = [f"X_{i}" for i in range(_nattr)] 63 | 64 | return self._labelcolumns 65 | 66 | def extract(self, adata: AnnData): 67 | """ 68 | Extract labels from adata.obs (or adata.obsm) and update 69 | """ 70 | if self._lookup is not None: 71 | 72 | obs_keys = adata.obs.keys() 73 | m = np.isin(obs_keys, self._lookup) 74 | 75 | if any(m): 76 | labels_sub = adata.obs[obs_keys[m]] 77 | types = [dt.name for dt in labels_sub.dtypes] 78 | 79 | self._labelnames = labels_sub.columns.tolist() 80 | self._labelcolumns = labels_sub.columns.tolist() 81 | 82 | self._validate(types) 83 | _values = labels_sub.values 84 | 85 | if self._labeltype == "discrete": 86 | self._discretelabelscollapsed = self.collapse(_values) 87 | _values_ohe = self.one_hot_encode(self._discretelabelscollapsed) 88 | 89 | self._discretelabelsunique = _values_ohe.columns.tolist() 90 | self._labelcolumns = _values_ohe.columns.tolist() 91 | 92 | _values = _values_ohe.values 93 | 94 | else: 95 | # Check if labels are in .obsm 96 | lookup_key = self._lookup 97 | if not isinstance(lookup_key, str): 98 | lookup_key = self._lookup[0] 99 | warn( 100 | f"Looking for labels in `adata.obsm`. Only the first key, {lookup_key}, will be used.", 101 | category=UserWarning 102 | ) 103 | 104 | if lookup_key in adata.obsm.keys(): 105 | self._labeltype = 'continuous' 106 | _values = adata.obsm[lookup_key] 107 | self._labelnames = lookup_key 108 | 109 | # Keep track of colnames for NBC 110 | _nattr = _values.shape[1] 111 | self._labelcolumns = [f"{lookup_key}_{i}" for i in range(_nattr)] 112 | 113 | else: 114 | raise KeyError( 115 | f"{lookup_key} not found in `adata`" 116 | ) 117 | self._labelshape = (_values.shape) 118 | self._values = _values 119 | 120 | return None 121 | 122 | 123 | def _validate(self, types) -> bool: 124 | """ 125 | Confirm that labels are either all discrete, or all continuous 126 | """ 127 | discrete_pattern = r"category|int|str" 128 | check_discrete = [bool(re.search(discrete_pattern, s)) for s in types] 129 | 130 | if all(check_discrete): 131 | self._labeltype='discrete' 132 | 133 | return True 134 | elif not any(check_discrete): 135 | self._labeltype='continuous' 136 | 137 | return True 138 | else: 139 | raise ValueError("Labels should be discrete or continous, not both.") 140 | 141 | @staticmethod 142 | def one_hot_encode(values): 143 | return pd.get_dummies(values) 144 | 145 | @staticmethod 146 | def collapse(values, sep="_"): 147 | return np.array(["_".join(row) for row in values]) -------------------------------------------------------------------------------- /examples/concordex_py_pbmc_demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Install concordex" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": null, 13 | "metadata": { 14 | "colab": { 15 | "base_uri": "https://localhost:8080/" 16 | }, 17 | "id": "pPgFOM-reZJN", 18 | "outputId": "2549548d-4b73-4740-d1a2-99b5a6a65da7" 19 | }, 20 | "outputs": [], 21 | "source": [ 22 | "# !pip install anndata\n", 23 | "# !pip3 install scanpy\n", 24 | "!pip3 install concordex" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 1, 30 | "metadata": { 31 | "id": "dpfhvyjafJo3" 32 | }, 33 | "outputs": [], 34 | "source": [ 35 | "# Import libraries\n", 36 | "import seaborn as sns\n", 37 | "import scanpy as sc\n", 38 | "\n", 39 | "from concordex.tools import calculate_concordex\n", 40 | "\n", 41 | "import session_info" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "## Load dataset\n", 49 | "\n", 50 | "For this demonstration of the nonspatial applications of concordex, we will be using the processed [PBMC dataset](https://www.10xgenomics.com/datasets/fresh-68-k-pbm-cs-donor-a-1-standard-1-1-0) from 10x Genomics. This dataset is available using the `scanpy.datasets` interface. " 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 2, 56 | "metadata": { 57 | "id": "KnzLTX-EhF9w" 58 | }, 59 | "outputs": [], 60 | "source": [ 61 | "ad = sc.datasets.pbmc68k_reduced()" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "## Compute `concordex`" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": {}, 74 | "source": [ 75 | "`concordex` computes a Neighborhood Consolidation Matrix (NBC) that quantifies the proportion of a given cell's neighbors sharing a specific label. This matrix helps capture the local structure of cell populations, reflecting how often cells with similar transcriptomic profiles are assigned the same discrete label. For this analysis, we will used the first 50 PCs to compute the k-nearest neighbor graph. The nodes of this graph will be colored by the cluster assignments derived from the Louvain community detection algorithm. \n", 76 | "\n", 77 | "The `compute_similarity=True` keyword argument summarizes the NBC into a cluster-by-cluster matrix. In this matrix, each entry reflects the average proportion of neighbors within a given cluster that share the same label. This provides a high-level view of the local similarity between cells across different clusters, revealing how homogenous or heterogeneous the neighborhoods are within each cluster. The similarity matrix provides a more intuitive understanding of the relationships between clusters based on their shared neighborhood structure." 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": 3, 83 | "metadata": {}, 84 | "outputs": [ 85 | { 86 | "name": "stdout", 87 | "output_type": "stream", 88 | "text": [ 89 | "Computing nearest neighbors...\n", 90 | "\n", 91 | "Computing neighborhood consolidation and similarity matrices...\n", 92 | "\n" 93 | ] 94 | } 95 | ], 96 | "source": [ 97 | "# Update `ad` in place\n", 98 | "calculate_concordex(\n", 99 | " ad,\n", 100 | " 'louvain', \n", 101 | " n_neighbors=30,\n", 102 | " use_rep=\"X_pca\",\n", 103 | " compute_similarity=True\n", 104 | ")" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "The NBC is added to `ad.obsm['X_nbc']` and the similarity information can be found in `ad.uns['nbc_params']['similarity']`" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 4, 117 | "metadata": {}, 118 | "outputs": [ 119 | { 120 | "data": { 121 | "text/plain": [ 122 | "AnnData object with n_obs × n_vars = 700 × 765\n", 123 | " obs: 'bulk_labels', 'n_genes', 'percent_mito', 'n_counts', 'S_score', 'G2M_score', 'phase', 'louvain'\n", 124 | " var: 'n_counts', 'means', 'dispersions', 'dispersions_norm', 'highly_variable'\n", 125 | " uns: 'bulk_labels_colors', 'louvain', 'louvain_colors', 'neighbors', 'pca', 'rank_genes_groups', 'index_params', 'nbc_params'\n", 126 | " obsm: 'X_pca', 'X_umap', 'index', 'X_nbc'\n", 127 | " varm: 'PCs'\n", 128 | " obsp: 'distances', 'connectivities'" 129 | ] 130 | }, 131 | "execution_count": 4, 132 | "metadata": {}, 133 | "output_type": "execute_result" 134 | } 135 | ], 136 | "source": [ 137 | "ad" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "We can easily plot the similarity matrix. To evaluate cluster assignments, we expect cell neighborhoods to be relatively homogeneous, meaning that, in most cases, a cell and its neighbors will share the same label." 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": 5, 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "sim = ad.uns['nbc_params']['similarity']" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": 6, 159 | "metadata": {}, 160 | "outputs": [ 161 | { 162 | "data": { 163 | "image/png": "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OqeVbgAAAIiI0047LU4++eT42te+FkcccUQ0NDREoVCI1atXR1NTU1xyySUxc+bMTp1TdAMAAEBEnH/++dG3b9+44oor4qyzzopCoRAREVmWxaBBg+Kcc86Js846q1PnFN0AAADwX84+++w4++yzY8WKFbF69eqIiBg0aFAMHz68S+fznm4AAAB4i+HDh8e4ceNi3LhxW4P7+eefj2nTpnXqPKIbAAAA3oWXX345br755k69xu3lAAAAEBH33HPP235/+fLlnT6n6AYAAICImDJlShQKhciyrMNj3ny42rvl9nIAAACIiMGDB8ddd90Vra2t7W6PP/54p88pugEAACAixowZ87Zh/U6r4O1xezkAAABExKxZs2LTpk0dfn/fffeNBx98sFPnFN0AAAAQERMmTHjb7/fr1y8mTpzYqXO6vRwAAAASEd0AAACQiOgGAACAREQ3AAAAJCK6AQAAIBHRDQAAAImIbgAAAEhEdAMAAEAiohsAAAASEd0AAACQiOgGAACAREQ3AAAAJCK6AQAAIBHRDQAAAImIbgAAAEhEdAMAAEAiohsAAAASEd0AAACQiOgGAACARApZlmV5DwG1qFfvPfMeoeb4w4J347Xf3533CDVnpwM/m/cIsN153071eY9Qc14tb8p7hJrzl5YteY9AjduyeVXeI1jpBgAAgFRENwAAACQiugEAACAR0Q0AAACJiG4AAABIRHQDAABAIqIbAAAAEhHdAAAAkIjoBgAAgERENwAAACQiugEAACAR0Q0AAACJiG4AAABIRHQDAABAIqIbAAAAEhHdAAAAkIjoBgAAgERENwAAACQiugEAACAR0Q0AAACJiG4AAABIRHQDAABAIqIbAAAAEhHdAAAAkIjoBgAAgERENwAAACQiugEAACAR0Q0AAACJiG4AAABIRHQDAABAIqIbAAAAEhHdAAAAkIjoBgAAoFsql8uxYcOGiq1cLnd4/Ouvvx4LFy6MpUuXtvneG2+8EbfcckunZxDdAAAAdEulUinq6+srtlKp1O6xy5YtixEjRsTHPvaxOPjgg+PjH/94NDc3b/3++vXr49RTT+30DKIbAACAbmn27Nmxfv36im327NntHnv22WfHwQcfHGvWrIlnnnkm+vfvH4ceemisXLmyqhl6VvVqAAAAqFHFYjGKxeK7OnbRokXxs5/9LAYOHBgDBw6M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UWrKbrvtll133XV5j5GrjRs3Zvvtt1/W1NSUTZw4MTv99NPzHik35513XjZq1Ki8x6gpZ599dnbYYYflPUZNO/3007MPfOADWWtra96j5Oaoo47Kpk2bVrHvc5/7XHbCCSfkNFG+Xnvttayuri77P//n/1TsHzVqVDZnzpycpqoNVrqhBm3evDkWL14ckyZNqtg/adKkWLRoUU5TUctWrFgRq1evrvg9UywWY+LEiTv075n169dHRMTuu++e8yS1oaWlJe64447YtGlTjBs3Lu9xcjVjxow46qij4vDDD897lJrw7LPPxpAhQ2L48OFx3HHHxfLly/MeKVf33HNPjB07Nv7u7/4u9thjj/ibv/mb+Ld/+7e8x6oZmzdvjltvvTWmTZsWhUIh73Fyc9hhh8V//Md/xLJlyyIi4re//W0sXLgwjjzyyJwny8eWLVuipaUl+vTpU7G/b9++sXDhwpymqg098x4AaGvt2rXR0tISDQ0NFfsbGhpi9erVOU1FLXvz90V7v2f++Mc/5jFS7rIsi8bGxjjssMNi5MiReY+TqyeffDLGjRsXb7zxRuy8885x9913xwc/+MG8x8rNHXfcEY8//rj3Gf6XQw45JG655ZbYf//948UXX4yLLrooxo8fH0899VQMGDAg7/FysXz58pg7d240NjbGueeeG7/5zW/iq1/9ahSLxTjppJPyHi93P/rRj+LVV1+NU045Je9RcnX22WfH+vXr48ADD4y6urpoaWmJiy++OP7+7/8+79Fyscsuu8S4cePi61//eowYMSIaGhri9ttvj1//+tex33775T1erkQ31LC3/utxlmU79L8o8878nvl/vvzlL8f//b//d4f/1/WIiAMOOCCeeOKJePXVV+Ouu+6Kk08+OR566KEdMryff/75OP300+OBBx5osxqzo5o8efLW/37wwQfHuHHj4gMf+EDcfPPN0djYmONk+WltbY2xY8fGJZdcEhERf/M3fxNPPfVUzJ07V3RHxPXXXx+TJ0+OIUOG5D1KrubPnx+33npr3HbbbXHQQQfFE088ETNnzowhQ4bEySefnPd4ufje974X06ZNiz333DPq6upi9OjRcfzxx8fjjz+e92i5Et1QgwYOHBh1dXVtVrXXrFnTZiUTImLrk4ZXr14dgwcP3rp/R/0985WvfCXuueeeePjhh2OvvfbKe5zc9e7dO/bdd9+IiBg7dmw8+uij8Z3vfCeuvfbanCd77y1evDjWrFkTY8aM2bqvpaUlHn744bjqqquiXC5HXV1djhPmr1+/fnHwwQfHs88+m/couRk8eHCbf5QaMWJE3HXXXTlNVDv++Mc/xs9+9rP44Q9/mPcouZs1a1acc845cdxxx0XEX//R6o9//GOUSqUdNro/8IEPxEMPPRSbNm2KDRs2xODBg2Pq1KkxfPjwvEfLlfd0Qw3q3bt3jBkzZuuTQd/U1NQU48ePz2kqatnw4cNj0KBBFb9nNm/eHA899NAO9Xsmy7L48pe/HD/84Q/j5z//+Q7/f/IdybIsyuVy3mPk4lOf+lQ8+eST8cQTT2zdxo4dG1/4whfiiSee2OGDOyKiXC7H008/XfEPeDuaQw89tM3HDS5btiyGDRuW00S148Ybb4w99tgjjjrqqLxHyd1rr70WPXpU5lRdXd0O/ZFhb+rXr18MHjw4Xnnllbj//vvjmGOOyXukXFnphhrV2NgYJ554YowdOzbGjRsX8+bNi5UrV8b06dPzHi03f/7zn+O5557b+vWKFSviiSeeiN133z2GDh2a42TvjXf6+WfOnBmXXHJJ7LfffrHffvvFJZdcEjvttFMcf/zxOU793poxY0bcdttt8eMf/zh22WWXrXeL1NfXR9++fXOeLh/nnntuTJ48Ofbee+/YuHFj3HHHHfGLX/wi7rvvvrxHy8Uuu+zS5j3+/fr1iwEDBuyw7/0/88wz4+ijj46hQ4fGmjVr4qKLLooNGzbssCt1ERFnnHFGjB8/Pi655JI49thj4ze/+U3Mmzcv5s2bl/douWptbY0bb7wxTj755OjZU0YcffTRcfHFF8fQoUPjoIMOiiVLlsQVV1wR06ZNy3u03Nx///2RZVkccMAB8dxzz8WsWbPigAMOiFNPPTXv0fKV56PTgbd39dVXZ8OGDct69+6djR49eof/2KMHH3wwi4g228knn5z3aO+Jd/r5W1tbs/POOy8bNGhQViwWs4997GPZk08+me/Q77H2rk9EZDfeeGPeo+Vm2rRpW/8ced/73pd96lOfyh544IG8x6opO/pHhk2dOjUbPHhw1qtXr2zIkCHZ5z73ueypp57Ke6zc/eQnP8lGjhyZFYvF7MADD8zmzZuX90i5u//++7OIyJ555pm8R6kJGzZsyE4//fRs6NChWZ8+fbJ99tknmzNnTlYul/MeLTfz58/P9tlnn6x3797ZoEGDshkzZmSvvvpq3mPlrpBlWfaelz4AAADsALynGwAAABIR3QAAAJCI6AYAAIBERDcAAAAkIroBAAAgEdENAAAAiYhuAAAASER0AwAAQCKiGwAAABIR3QAAAJCI6AYAAIBE/n/EfUg9tBeaYAAAAABJRU5ErkJggg==", 164 | "text/plain": [ 165 | "
" 166 | ] 167 | }, 168 | "metadata": {}, 169 | "output_type": "display_data" 170 | } 171 | ], 172 | "source": [ 173 | "# Make sure axes are properly labeled\n", 174 | "axlabs = ad.uns['nbc_params']['labelorder']\n", 175 | "cg = cg = sns.clustermap(\n", 176 | " sim,\n", 177 | " row_cluster=False,\n", 178 | " col_cluster=False,\n", 179 | " xticklabels=axlabs, yticklabels=axlabs\n", 180 | ")" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 7, 186 | "metadata": {}, 187 | "outputs": [ 188 | { 189 | "data": { 190 | "text/html": [ 191 | "
\n", 192 | "Click to view session information\n", 193 | "
\n",
194 |        "-----\n",
195 |        "anndata             0.10.9\n",
196 |        "concordex           1.1.0\n",
197 |        "scanpy              1.10.1\n",
198 |        "seaborn             0.13.2\n",
199 |        "session_info        1.0.0\n",
200 |        "-----\n",
201 |        "
\n", 202 | "
\n", 203 | "Click to view modules imported as dependencies\n", 204 | "
\n",
205 |        "CoreFoundation              NA\n",
206 |        "Foundation                  NA\n",
207 |        "PIL                         10.4.0\n",
208 |        "PyObjCTools                 NA\n",
209 |        "anyio                       NA\n",
210 |        "appnope                     0.1.4\n",
211 |        "arrow                       1.3.0\n",
212 |        "asttokens                   NA\n",
213 |        "attr                        24.2.0\n",
214 |        "attrs                       24.2.0\n",
215 |        "babel                       2.14.0\n",
216 |        "brotli                      1.1.0\n",
217 |        "certifi                     2024.08.30\n",
218 |        "cffi                        1.17.1\n",
219 |        "charset_normalizer          3.3.2\n",
220 |        "colorama                    0.4.6\n",
221 |        "comm                        0.2.2\n",
222 |        "cycler                      0.12.1\n",
223 |        "cython_runtime              NA\n",
224 |        "dateutil                    2.9.0\n",
225 |        "debugpy                     1.8.5\n",
226 |        "decorator                   5.1.1\n",
227 |        "defusedxml                  0.7.1\n",
228 |        "executing                   2.1.0\n",
229 |        "fastjsonschema              NA\n",
230 |        "fqdn                        NA\n",
231 |        "h5py                        3.11.0\n",
232 |        "idna                        3.8\n",
233 |        "igraph                      0.11.6\n",
234 |        "ipykernel                   6.29.5\n",
235 |        "isoduration                 NA\n",
236 |        "jedi                        0.19.1\n",
237 |        "jinja2                      3.1.4\n",
238 |        "joblib                      1.4.2\n",
239 |        "json5                       0.9.25\n",
240 |        "jsonpointer                 3.0.0\n",
241 |        "jsonschema                  4.23.0\n",
242 |        "jsonschema_specifications   NA\n",
243 |        "jupyter_events              0.10.0\n",
244 |        "jupyter_server              2.14.2\n",
245 |        "jupyterlab_server           2.27.3\n",
246 |        "kiwisolver                  1.4.7\n",
247 |        "legacy_api_wrap             NA\n",
248 |        "leidenalg                   0.10.2\n",
249 |        "llvmlite                    0.43.0\n",
250 |        "markupsafe                  2.1.5\n",
251 |        "matplotlib                  3.9.2\n",
252 |        "matplotlib_inline           0.1.7\n",
253 |        "mpl_toolkits                NA\n",
254 |        "natsort                     8.4.0\n",
255 |        "nbformat                    5.10.4\n",
256 |        "numba                       0.60.0\n",
257 |        "numpy                       2.0.2\n",
258 |        "objc                        10.3.1\n",
259 |        "overrides                   NA\n",
260 |        "packaging                   24.1\n",
261 |        "pandas                      2.2.2\n",
262 |        "parso                       0.8.4\n",
263 |        "patsy                       0.5.6\n",
264 |        "pickleshare                 0.7.5\n",
265 |        "platformdirs                4.3.2\n",
266 |        "prometheus_client           NA\n",
267 |        "prompt_toolkit              3.0.47\n",
268 |        "psutil                      6.0.0\n",
269 |        "pure_eval                   0.2.3\n",
270 |        "pydev_ipython               NA\n",
271 |        "pydevconsole                NA\n",
272 |        "pydevd                      2.9.5\n",
273 |        "pydevd_file_utils           NA\n",
274 |        "pydevd_plugins              NA\n",
275 |        "pydevd_tracing              NA\n",
276 |        "pygments                    2.18.0\n",
277 |        "pyparsing                   3.1.4\n",
278 |        "pythonjsonlogger            NA\n",
279 |        "pytz                        2024.1\n",
280 |        "referencing                 NA\n",
281 |        "requests                    2.32.3\n",
282 |        "rfc3339_validator           0.1.4\n",
283 |        "rfc3986_validator           0.1.1\n",
284 |        "rpds                        NA\n",
285 |        "scipy                       1.14.1\n",
286 |        "send2trash                  NA\n",
287 |        "six                         1.16.0\n",
288 |        "sklearn                     1.5.1\n",
289 |        "sniffio                     1.3.1\n",
290 |        "socks                       1.7.1\n",
291 |        "stack_data                  0.6.2\n",
292 |        "statsmodels                 0.14.2\n",
293 |        "texttable                   1.7.0\n",
294 |        "threadpoolctl               3.5.0\n",
295 |        "tornado                     6.4.1\n",
296 |        "traitlets                   5.14.3\n",
297 |        "uri_template                NA\n",
298 |        "urllib3                     2.2.2\n",
299 |        "wcwidth                     0.2.13\n",
300 |        "webcolors                   24.8.0\n",
301 |        "websocket                   1.8.0\n",
302 |        "yaml                        6.0.2\n",
303 |        "zmq                         26.2.0\n",
304 |        "zstandard                   0.23.0\n",
305 |        "
\n", 306 | "
\n", 307 | "
\n",
308 |        "-----\n",
309 |        "IPython             8.27.0\n",
310 |        "jupyter_client      8.6.2\n",
311 |        "jupyter_core        5.7.2\n",
312 |        "jupyterlab          4.2.5\n",
313 |        "-----\n",
314 |        "Python 3.12.5 | packaged by conda-forge | (main, Aug  8 2024, 18:31:54) [Clang 16.0.6 ]\n",
315 |        "macOS-15.1.1-x86_64-i386-64bit\n",
316 |        "-----\n",
317 |        "Session information updated at 2025-01-14 11:47\n",
318 |        "
\n", 319 | "
" 320 | ], 321 | "text/plain": [ 322 | "" 323 | ] 324 | }, 325 | "execution_count": 7, 326 | "metadata": {}, 327 | "output_type": "execute_result" 328 | } 329 | ], 330 | "source": [ 331 | "session_info.show()" 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": null, 337 | "metadata": {}, 338 | "outputs": [], 339 | "source": [] 340 | } 341 | ], 342 | "metadata": { 343 | "colab": { 344 | "authorship_tag": "ABX9TyPZmkptms3G6+WJ4RcIlU2N", 345 | "provenance": [] 346 | }, 347 | "interpreter": { 348 | "hash": "adaa19b3e1639a0b29506b5755c4bbe1fbe125a7ccca5eaffe8ceb5f98914033" 349 | }, 350 | "kernelspec": { 351 | "display_name": "Python 3 (ipykernel)", 352 | "language": "python", 353 | "name": "python3" 354 | }, 355 | "language_info": { 356 | "codemirror_mode": { 357 | "name": "ipython", 358 | "version": 3 359 | }, 360 | "file_extension": ".py", 361 | "mimetype": "text/x-python", 362 | "name": "python", 363 | "nbconvert_exporter": "python", 364 | "pygments_lexer": "ipython3", 365 | "version": "3.12.5" 366 | } 367 | }, 368 | "nbformat": 4, 369 | "nbformat_minor": 4 370 | } 371 | -------------------------------------------------------------------------------- /examples/data/starmap_processed.h5ad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pachterlab/concordex/77ef3bf7382c461ae3c9a9362bf51a3283175347/examples/data/starmap_processed.h5ad -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "concordex" 7 | version = "1.1.1" 8 | description = "Identification of spatial homogeneous regions with concordex" 9 | readme = "README.md" 10 | requires-python = ">=3.9" 11 | authors = [ 12 | {name = "Kayla Jackson", email = "kaylajac@caltech.edu"} , 13 | {name = "A. Sina Booeshaghi", email = "sinab@berkeley.edu"}, 14 | {name = "Angel Galvez-Merchan", email = "angelgalvez94@gmail.com"}, 15 | {name = "Alexandra Kim", email = "alexandrasuriya@gmail.com"} 16 | ] 17 | maintainers = [{name = "Kayla Jackson", email = "kaylajac@caltech.edu"}] 18 | license = {file = "LICENSE"} 19 | keywords = ["SingleCell", "Clustering", "Spatial", "Transcriptomics"] 20 | dependencies = [ 21 | "anndata>=0.8", 22 | "numpy>=1.23", 23 | "pandas>=1.5", 24 | "scikit-learn>=0.24" 25 | ] 26 | classifiers = [ 27 | "Environment :: Console", 28 | "Intended Audience :: Science/Research", 29 | "License :: OSI Approved :: MIT License", 30 | "Operating System :: OS Independent", 31 | "Programming Language :: Python :: 3.6", 32 | "Programming Language :: Python :: 3.7", 33 | "Programming Language :: Python :: 3.8", 34 | "Programming Language :: Python :: 3.9", 35 | "Programming Language :: Python :: 3.10", 36 | "Topic :: Scientific/Engineering :: Bio-Informatics", 37 | "Topic :: Utilities" 38 | ] 39 | 40 | [project.urls] 41 | Repository = "https://github.com/pachterlab/concordex" 42 | 43 | [tool.setuptools] 44 | packages = { find = {} } 45 | 46 | [tool.bumpversion] 47 | current_version = "1.1.1" 48 | commit = true 49 | tag = false 50 | files = [ 51 | "setup.cfg", 52 | "pyproject.toml", 53 | "concordex/__init__.py", 54 | "README.md" 55 | ] 56 | 57 | [tool.flake8] 58 | exclude = [".git", ".github", "__pycache__", "build", "dist"] 59 | statistics = true 60 | max-line-length = 88 61 | extend-ignore = ["E203", "E501"] 62 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pachterlab/concordex/77ef3bf7382c461ae3c9a9362bf51a3283175347/requirements.txt -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 1.1.1 3 | commit = True 4 | tag = False 5 | 6 | [metadata] 7 | name = concordex 8 | version = 1.1.1 9 | url = https://github.com/pachterlab/concordex 10 | description = Identification of spatial homogeneous regions with concordex 11 | long_description = file: README.md 12 | long_description_content_type = text/markdown 13 | author = Kayla Jackson, A. Sina Booeshaghi, Angel Galvez-Merchan, Alexandra Kim 14 | maintainer = Kayla Jackson 15 | maintainer_email = kaylajac@caltech.edu 16 | kewyords = SingleCell, Clustering, Spatial, Transcriptomics 17 | license = MIT 18 | classifiers = 19 | "Environment :: Console" 20 | "Intended Audience :: Science/Research" 21 | "License :: OSI Approved :: MIT License" 22 | "Operating System :: OS Independent" 23 | "Programming Language :: Python :: 3.6" 24 | "Programming Language :: Python :: 3.7" 25 | "Programming Language :: Python :: 3.8" 26 | "Programming Language :: Python :: 3.9" 27 | "Programming Language :: Python :: 3.10" 28 | "Topic :: Scientific/Engineering :: Bio-Informatics" 29 | "Topic :: Utilities" 30 | 31 | [options] 32 | zip_safe = False 33 | python_requires = >=3.9 34 | packages = find: 35 | install_requires = 36 | anndata>=0.8 37 | numpy>=1.23 38 | pandas>=1.5 39 | scikit-learn>=0.24 40 | 41 | [bumpversion:file:concordex/__init__.py] 42 | 43 | [bumpversion:file:README.md] 44 | 45 | [flake8] 46 | exclude = .git,.github,__pycache__,build,dist 47 | statistics = True 48 | max-line-length = 88 49 | extend-ignore = E203,E501 50 | --------------------------------------------------------------------------------